Programmation réactive, ça vaut le coup de tout laisser tomber et de se précipiter vers le rêve

Un autre article sur la programmation réactive. Et ne roulez pas les yeux sur cette ligne et parlez à haute voix d'une voix langoureuse - "Eh bien, que pouvez-vous me dire d'autre sur la programmation réactive ... hein?" C'est un peu différent d'un tas d'autres, écrit comme un papier carbone, donc certaines choses peuvent sembler ... étranges ou même complètement inappropriées, comme un humour trié.


Peu importe que vous connaissiez par cœur le manifeste réactif, qu'il y ait une contre-pression dans votre café du matin, si vous touchez toutes sortes d'éditeurs et d'abonnés avec ces stylos ou écrivez un bon vieux code de blocage synchrone. Ou peut-être seulement récemment, quelqu'un avec son rapport franchement publicitaire sur l'avenir brillant et l'orgasme en streaming (enfin, ou jet d'encre, ici les subtilités de la traduction décident de tout), bien sûr, en utilisant l'une des bibliothèques réactives, a suscité un intérêt pour la nouvelle technologie à vos yeux.


Ce sera intéressant.


Absolu


Et donc, imaginons que nous sommes complètement fous ... Bien que le mot sera présenté ici davantage pour le politiquement correct, car tous ceux qui connaissent l'auteur sont conscients du fait qu'il est assis toute la journée dans un appartement verrouillé. Il fonctionne la nuit, fume trois narguilés par jour et sort plusieurs fois par semaine. Cela se produit généralement pour rencontrer votre thérapeute, mais parfois la raison est complètement différente - IKEA.


Et donc, nous prenons l'ancien code synchrone, mais qui fonctionne:


int result = 1 + 2 * 4; 

Et nous essayons de le refactoriser, car il nous semble ... Bien que non, c'est trop, personne ne le fera dans leur bon sens. Personne. Dans votre bon sens.


 Mono<Integer> result = Flux.concat( Mono.just(1), Flux.concat(Mono.just(2), Mono.just(4)) .reduce((a, b) -> a * b)) .reduce((a, b) -> a + b); StepVerifier.create(result) .expectNext(9) .expectComplete() .verify(); 

Mais comme vous l'avez peut-être remarqué, je l'ai fait. Et notez bien! Je me suis retenu d'utiliser quelques microservices dans cet exemple, l'un pour l'addition et l'autre pour la multiplication, que je devrais communiquer via rSocket et kafka .


Au fait, c'était un échauffement. Avant de commencer, je voudrais donner deux conseils. Et donc, le premier conseil: soyez toujours préparé au fait que vos collègues peuvent être des psychopathes vraiment malades avec un tas de complexes pour enfants qui éclabousseront sous la forme d'un code, pas de la meilleure qualité, et des solutions architecturales très élégantes.


Astuce deux. J'ai décidé de le laisser à la fin de l'article. Mais si après tout ce que vous voyez, vous décidez que vous devez rester à l'écart de ce d% & @ a, alors vous savez où le chercher.


Un exemple plus réel


Prenons un exemple un peu plus réaliste. Par exemple ... des arbres. Pourquoi exactement eux? Parce que nous aimons tous les arbres à la fois comme objet dans la vie réelle et comme structure de données. C'est l'une des parties les plus importantes du monde qui nous entoure. Les arbres produisent de l'oxygène pour que nous puissions respirer.


De nombreux animaux vivent sur des arbres ou dans des lieux d'accumulation de ces objets (ces endroits sont appelés un parc ou une forêt, selon), mais les animaux, même les sans-abri, vivent dans des boîtes en carton qui étaient à l'origine des arbres.


Soit dit en passant, un fait intéressant, depuis les temps anciens, l'humanité extrait des arbres l'un des matériaux les plus couramment utilisés dans la vie humaine - le bois. Imaginez, tous ces bâtons que nous insérons dans nos roues toute notre vie, et en même temps blâmer tout le monde, également en bois.


Nous avons un arbre, un arbre ordinaire et banal qui a grandi sur la pelouse de notre code et rêvé un jour quand il grandira, se transformer en BST:


 public class TreeNode { public int val; public TreeNode left, right; public TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; } public TreeNode(int val) { this(val, null, null); } //       TreeNode invert() { // ... } int sumOfLeftLeaves() { // ... } TreeNode searchBST(int val) { // .. } public List<Integer> toList() { List<Integer> list = new ArrayList<>(); if (left != null) list.addAll(left.toList()); list.add(val); if (right != null) list.addAll(right.toList()); return list; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TreeNode treeNode = (TreeNode) o; return val == treeNode.val; } @Override public int hashCode() { return val; } @Override public String toString() { return toList().toString(); } } 

Et à un moment donné, par exemple, lundi à six heures du matin, après une nuit blanche, un programmeur a eu l'idée de le rendre réactif. Et il l'a fait. Parce que c'est un homme de travail.


 public class TreeNode { public final Mono<Integer> value; public final Mono<TreeNode> left; public final Mono<TreeNode> right; public TreeNode(Mono<Integer> value, Mono<TreeNode> left, Mono<TreeNode> right) { this.value = value; this.left = left; this.right = right; } public TreeNode(int value, TreeNode left, TreeNode right) { this(Mono.just(value), Mono.justOrEmpty(left), Mono.justOrEmpty(right)); } public TreeNode(int value) { this(Mono.just(value), Mono.empty(), Mono.empty()); } public Flux<TreeNode> flux() { return Flux.concat( left.flatMapMany(TreeNode::flux), Mono.just(this), right.flatMapMany(TreeNode::flux) ); } @Override public String toString() { return flux() .flatMap(n -> n.value) .collectList() .map(Object::toString) .block(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TreeNode treeNode = (TreeNode) o; return Objects.equals(value.block(), treeNode.value.block()); } @Override public int hashCode() { return value.block(); } 

Vous sentez la question suspendue dans l'air - "Pourquoi?!". Premièrement, il est beau et deuxièmement, il est très évolutif, car il est asynchrone, il est également sans danger pour l'environnement et Greta Tunberg ne viendra pas une fois dans un train avec un énorme gode ceinture ... vous savez ce que je veux dire. Et n'oubliez pas que nous avons maintenant une contre-presse. Et au fait, je vous rappelle qu'aucune refactorisation ne peut être effectuée sans couverture de test.


 public class TreeNodeTest { @Test public void testFlux() { TreeNode tree = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3)), new TreeNode(7, new TreeNode(6), new TreeNode(9))); StepVerifier.create(tree.flux().flatMap(n -> n.value)) .expectNext(1, 2, 3, 4, 6, 7, 9) .expectComplete() .verify(); } } 

Super. Le début a été fait, il ne nous reste plus qu'à refactoriser trois méthodes d' invert simples, sumOfLeftLeaves et searchBST dans l'ancien, ennuyeux, arbre synchrone et ajouter quelques tests.


Inverser


Et donc, nous avons déjà une méthode invert implémentée, mais, malheureusement, elle n'est pas réactive, regardez comme elle est triste, le seul avantage de cette méthode est qu'elle est facile à lire et il est facile de comprendre que si nous inversons l'arbre, alors nous parcourons récursivement tous ses nœuds, créons des copies et échangeons ses enfants. Autrement dit, dans l'enfant gauche du nouveau nœud, nous mettons le nœud inversé de l'enfant droit et vice versa. Et partout, ces contrôles sont nuls.


  public TreeNode invert() { return invert(this); } private TreeNode invert(TreeNode root) { if (root == null) return null; TreeNode swap = new TreeNode(root.val); swap.right = invert(root.left); swap.left = invert(root.right); return swap; } 

Regardez ce qui s'est passé lorsque nous l'avons réécrit, il a joué avec de nouvelles couleurs, et en prime, nous avons obtenu une sécurité nulle gratuitement.


 public Mono<TreeNode> invert() { return Mono.just(this) .map(n -> new TreeNode(n.value, n.right.flatMap(TreeNode::invert), n.left.flatMap(TreeNode::invert) )); } @Test public void testInvert() { TreeNode tree = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3)), new TreeNode(7, new TreeNode(6), new TreeNode(9))); Flux<Integer> inverted = tree.invert() .flatMapMany(TreeNode::flux) .flatMap(n -> n.value); StepVerifier.create(inverted) .expectNext(9, 7, 6, 4, 3, 2, 1) .expectComplete() .verify(); } 

sumOfLeftLeaves


Habituellement, les gens sont divisés en deux types, le premier type est ceux qui comprennent ce que cette méthode fera de son nom, et toutes les autres personnes qui n'ont pas quitté leur chambre depuis des années et n'ont jamais vu d'arbres.


Je peux aider le deuxième type de personnes avec les conseils de mon psychothérapeute, qui sera probablement en mesure d'élaborer un schéma thérapeutique. Et dites le sens de cette méthode. Un nœud est considéré comme une leaf s'il n'a pas d'enfants, et laissé s'il se développe à son parent à partir du pointeur left . Ceci est écrit au nom de la méthode.


Mais pourquoi les mots, passons aux choses sérieuses, voyons comment c'était ...


  public int sumOfLeftLeaves() { return sumOfLeftLeaves(false, this); } public int sumOfLeftLeaves(boolean left, TreeNode root) { if (root == null) return 0; if (root.left == null && root.right == null && left) return root.val; return sumOfLeftLeaves(true, root.left) + sumOfLeftLeaves(false, root.right); } 

... et comment c'est devenu


  public Mono<Integer> sumOfLeftLeaves() { return sumOfLeftLeaves(Mono.just(this), false) .flatMap(n -> n.value) .reduce(Integer::sum); } private Flux<TreeNode> sumOfLeftLeaves(Mono<TreeNode> node, boolean left) { return node .flux() .concatMap(n -> Flux.concat( sumOfLeftLeaves(n.left, true), Flux.first(n.left, n.right) .map(x -> new TreeNode(0)) .switchIfEmpty( Mono.just(n) ) .filter(x -> left), sumOfLeftLeaves(n.right, false) )); } @Test public void testSumOfLeftLeaves() { TreeNode tree = new TreeNode(3, new TreeNode(9, new TreeNode(11), null), new TreeNode(20, new TreeNode(15), new TreeNode(7)) ); StepVerifier.create(tree.sumOfLeftLeaves()) .expectNext(26) .expectComplete() .verify(); } 

Ouah! Si réactif. Beaucoup asynchrone. Trop génial. Sécurité donc nulle. Beaucoup de contre-pression. Si évolutif ...


Soit dit en passant, nous avons encore une méthode de plus, dans laquelle, je vous le promets, nous dirigerons tout ce pouvoir dans la bonne direction.


searchBST


Chaque arbre rêve de devenir un arbre de recherche binaire, et cette méthode nous aidera à le rechercher. Cela dit tout, alors je montre immédiatement à quel point c'était terrible et combien c'était merveilleux.


  public TreeNode searchBST(int val) { return searchBST(this, val); } public TreeNode searchBST(TreeNode root, int val) { if (root == null) return null; if (val < root.val) return searchBST(root.left, val); else if (val > root.val) return searchBST(root.right, val); else return root; } 

Ressentez-le:


  public Mono<TreeNode> testSearchBST(int val) { return searchBST(Mono.just(this), val); } private Mono<TreeNode> searchBST(Mono<TreeNode> root, int val) { return root.flatMap(node -> node.value .filter(v -> v > val) .flatMap(v -> searchBST(node.left, val)) .switchIfEmpty(node.value .filter(v -> v < val) .flatMap(v -> searchBST(node.right, val)) .switchIfEmpty(node.value .filter(v -> v == val) .flatMap(v -> root) ) )); } @Test public void searchBST() { TreeNode tree = new TreeNode(4, new TreeNode(2, new TreeNode(1), new TreeNode(3)), new TreeNode(7)); StepVerifier.create(tree.searchBST(3).flatMap(n -> n.value)) .expectNext(3) .expectComplete() .verify(); } 

Rien de plus à ajouter. C'est une programmation réactive, c'est le plus.


Pas encore fini


Lorsque vous faites des choses vraiment cool, il est très difficile de s'arrêter. Vous êtes fatigué, vos yeux se ferment, vous voulez dormir, mais à l'intérieur, vous ressentez un énorme flux de motivation. Vous faites tout correctement, vous ne pouvez pas vous arrêter, ressentir ce sentiment, continuer à rendre le monde meilleur.


Par conséquent, après avoir supprimé les arbres, vous passez sans hésitation aux bonnes vieilles listes simplement connectées. Leur temps n'est pas encore écoulé, encore des milliers de personnes lors des entretiens techniques vérifient leur fixation. Il est temps de le faire de manière réactive.


 public class ListNode { public int val; public ListNode next; public ListNode(int val) { this.val = val; } public ListNode(int val, ListNode next) { this.val = val; this.next = next; } public static ListNode of(int... array) { if (array.length < 1) return null; ListNode head = new ListNode(array[0]); ListNode tail = head; for (int i = 1; i < array.length; i++) { ListNode next = new ListNode(array[i]); tail.next = next; tail = next; } return head; } public ListNode last() { if (next != null) return next.last(); return this; } } public class ListTestNode { public Mono<Boolean> hasCycle(ListNode head) { return Mono.justOrEmpty(head) .flatMapMany(node -> { Flux<ListNode> flux = Flux.generate(() -> head, (n, sink) -> { if (n == null) { sink.complete(); return null; } sink.next(n); return n.next; }); Flux<ListNode> fast = flux.skip(1); Flux<ListNode> slow = flux.flatMap(n -> Flux.just(n, n)); return fast.zipWith(slow); }) .any(objects -> objects.getT1() == objects.getT2()) .defaultIfEmpty(false); } @Test public void hasCycle() { StepVerifier.create(hasCycle(null)) .expectNext(false) .expectComplete() .verify(); ListNode withoutCycle = ListNode.of(1, 2, 3, 4, 5, 6); StepVerifier.create(hasCycle(withoutCycle)) .expectNext(false) .expectComplete() .verify(); ListNode withCycle = ListNode.of(1, 2, 3, 4, 5, 6); withCycle.last().next = withCycle.next.next; StepVerifier.create(hasCycle(withCycle)) .expectNext(true) .expectComplete() .verify(); } 

Puisque nous avons abordé le sujet des entretiens techniques, nous ne devons pas oublier la tâche bien-aimée de vérifier l'enceinte des supports. Le spectacle doit continuer!


  public Mono<Boolean> isValidParentheses(String s) { return Flux.range(0, s.length()) .map(s::charAt) .reduceWith(() -> "", (str, c) -> { if (c == '{' || c == '[' || c == '(') return str + c; char last = str.charAt(str.length() - 1); if (c == '}' && last != '{') return str; if (c == ']' && last != '[') return str; if (c == ')' && last != '(') return str; return str.substring(0, str.length() - 1); }) .map(String::isEmpty); } @Test public void testIsValidParentheses() { StepVerifier.create(isValidParentheses("()")) .expectNext(true) .expectComplete() .verify(); StepVerifier.create(isValidParentheses("()[]{}")) .expectNext(true) .expectComplete() .verify(); StepVerifier.create(isValidParentheses("{()[]()}")) .expectNext(true) .expectComplete() .verify(); StepVerifier.create(isValidParentheses("()")) .expectNext(true) .expectComplete() .verify(); StepVerifier.create(isValidParentheses("(]")) .expectNext(false) .expectComplete() .verify(); StepVerifier.create(isValidParentheses("([)]")) .expectNext(false) .expectComplete() .verify(); } 

Ouah. Tellement immuable ... Beaucoup réactif.


Astuce numéro deux


Soit dit en passant, comme je l'ai promis, à la fin de l'article se trouve le conseil numéro deux: . Parfois, cela aura l'air au moins idiot et peu pratique.

Source: https://habr.com/ru/post/fr481716/


All Articles