Retenir les vices de l'impératif

Le paradigme orienté objet est extrêmement pratique pour les entreprises: il vous permet de mettre en œuvre presque n'importe quelle idée, offrant des performances de produit acceptables. Dans ce cas, par produit, nous entendons une application iOS, donc, en conclusion, nous procéderons du développement spécifiquement sur cette plateforme.

En fermant les yeux sur les lacunes bien connues de ce paradigme populaire, la liste de ses inconvénients comprend son avantage le plus important - la flexibilité du développement. Pourquoi est-ce un inconvénient? Il est bien évident que la flexibilité, en plus de la capacité de base à résoudre des problèmes commerciaux, permet de le faire de différentes manières. Il est vrai qu'il existe une douzaine de fausses approches pour une approche correcte, malgré le fait que la tâche métier sera correctement résolue dans tous les cas, mais avec des différences de mise en œuvre, dont l'extensibilité et la transparence dépendront déjà de l'exactitude de l'approche appliquée.

En tenant compte de la loi de Murphy, la conclusion est qu'en l'absence de restrictions architecturales appropriées, il est plus susceptible de suivre la voie du chaos, c'est-à-dire que la qualité du code diminuera rapidement, uniquement parce que le paradigme le permet. Cet article discutera d'une des limitations architecturales possibles qui aideront à maintenir l'équilibre des forces du bien et du mal, en d'autres termes, la dynamique de la croissance d'entropie dans la base de code du projet. Il est important que cette dynamique soit directement corrélée avec le nombre de personnes impliquées dans la rédaction du code.Par conséquent, pour les grands projets, l'exactitude des restrictions sélectionnées est particulièrement critique. À quoi ça sert?

Le point est simple. Commençons par l'axiome suivant - plus il y a de propriétés dans un objet, plus il est «pire». Cette affirmation peut s'expliquer comme suit: avec une augmentation du nombre d'états internes, tous les indicateurs positifs de l'objet - extensibilité, modularité, transparence, testabilité - diminuent. On peut objecter, en disant que la complexité de l'objet et sa fonctionnalité sont interconnectées et que sans le premier, le second ne se produit pas. Tout est vrai, mais, suivant le chemin "avec état", la croissance de la complexité se produit de façon exponentielle, malgré le fait que la croissance devrait idéalement être linéaire, ou, plus simplement, une nouvelle fonctionnalité ne devrait pas compliquer les fonctionnalités existantes.

PS Ici, il convient de préciser que les fonctionnalités sont comprises comme des couches de logique métier sémantiquement liées, par conséquent, la décision est souvent prise de compliquer une classe existante, plutôt que d'en créer une nouvelle. Dans de tels cas, il est difficile de trouver des contradictions avec le premier principe SOLID.

Un exemple est un écran complètement standard avec une liste d'entités avec un choix. Certaines propriétés pourraient être rendues non stables, mais IB nous lie au constructeur par défaut du contrôleur et cela nous oblige à «faire de la saleté». L'accès à ces propriétés est implicitement obtenu par chaque méthode de classe dans le même fichier. Et le moment le plus désagréable est que leur mutabilité n'est également couverte par rien, ce qui entraîne en quelque sorte des conséquences irréversibles sous la forme d'une connexion forte et, par conséquent, au fait que la fixation de certains défauts provoque l'apparition de nouveaux:



Nous pouvons conclure qu'une augmentation linéaire de la complexité d'un objet avec un état commun est pratiquement inaccessible. La fonctionnalité avec cette approche devient monolithique et difficile à toute sorte de ségrégation. Un bon exemple est l'antipattern Massive-View-Controller, qui est assez courant et démontre clairement le résultat de l'absence de toute restriction sur le projet.



En utilisant UIKit comme exemple, vous pouvez voir exactement comment le style de développement impératif affecte la complexité du code et à quels points le framework «force» la création de propriétés dans les classes.
Le cas le plus simple - le traitement de l’appui sur un bouton - n’est normalement effectué qu’en définissant la «méthode sale», c’est-à-dire par une fonction qui ne peut rien recevoir d’utile, vous devrez donc aller à l’extérieur pour accéder aux propriétés:



Ainsi, la «fonction» du bouton compliquera tyranniquement le reste de la fonctionnalité de l'objet, puisque tous les résidents de la classe auront accès à ces données. En fait, presque tous les contrôles de l'interface utilisateur iOS fonctionnent de manière similaire. Par conséquent, il nous vient à l'esprit d'implémenter une sorte de wrapper sur des éléments d'interface utilisateur, par exemple, qui prend une fermeture, comme un traitement de «l'opération» d'un élément particulier, mais la difficulté est de trouver comment rendre un tel wrapper concis et fiable. Après tout, le problème ne se trouve pas seulement dans l'entrée, mais aussi dans la sortie des informations, par exemple, la table omniprésente avec les données fonctionne également "sale" et n'a aucune idée des données qu'elle affiche, vous devez donc l'enregistrer dans un objet:



Est-ce pratique? Idéalement, tout le monde a toujours accès aux données. Flexible, rapide, impératif. Mais tout cela se transforme au fil du temps, en règle générale, en un obélisque concret pour mille lignes de code et en larmes de ceux qui ont eu la tâche de travailler dans cette classe.

Pour en revenir aux limitations, le principe suivant peut être formé à partir du code ci-dessus - sans propriétés dans les classes est beaucoup plus propre, et plus rentable avec le support et l'extension. Idéalement, la logique de l'objet devrait être dans ses méthodes «pures», qui prennent toutes leurs dépendances en entrée et ont le résultat de leur activité en sortie.
L'idée est d'abaisser l'état privé de l'objet d'un niveau plus bas. Autrement dit, si le privé ferme des propriétés du monde extérieur, alors notre tâche est d'aller encore plus loin et de renforcer l'encapsulation au niveau des méthodes elles-mêmes. À première vue, malgré la nature asynchrone de la plate-forme et l'impérativité du cadre principal, il peut sembler que toute cette idée est non seulement impossible, mais au moins sa mise en œuvre ne semblera pas naturelle. Et, très probablement, il en sera ainsi, mais c'est l'un de ces problèmes qui peuvent être résolus en introduisant un niveau supplémentaire d'abstractions.

Si nous voulons intégrer toute la logique des classes dans leurs méthodes sans recourir aux propriétés, nous devons travailler en étroite collaboration avec les fermetures. IOS dispose d'outils standard pour gérer les opérations asynchrones, telles que GCD et OperationQueue. Il semble qu'ils suffiraient, mais lorsque vous essayez de donner vie à l'idée, tout se révélera loin d'être aussi rose que vous le souhaiteriez. Avec cette approche, outre le fait qu'il y a de grandes chances d'obtenir un enfer de rappel, le code lui-même se révélera lourd, il aura de nombreux trous logiques et il sera fortement connecté. Il est possible qu'un tel code soit encore plus compliqué que ce dont nous essayons si vite de nous échapper.

Si vous regardez autour de vous, vous pouvez voir qu'il existe des moyens beaucoup plus beaux et fonctionnels pour atteindre cet objectif. La programmation réactive est utilisée depuis longtemps dans le développement de logiciels commerciaux et est idéale pour le monde frontal asynchrone. Dans ce cas, nous considérerons une implémentation (plutôt réussie) du paradigme réactif pour Swift - Rx.



Il propose une entité simple appelée Observable. Il s'agit d'une sorte d'abstraction sur le flux d'événements, il peut être souscrit, et après cela, l'abonné recevra ces événements au fil du temps:



La façon la plus simple d'imaginer le déroulement des événements est de présenter un bouton régulier. L'événement de son fonctionnement est ici un flux, de sorte que tout objet peut s'abonner et recevoir des événements de son clic, et surtout, le bouton ne sait rien de ses propres abonnés. De façon pratique, presque n'importe quelle action peut être transformée en une séquence de valeurs similaire, ce qui est important, car Observable peut être combiné les uns avec les autres, car aucun cadre standard ne le permettra.

Par exemple, vous pouvez envoyer plusieurs requêtes en appuyant sur le bouton (filtrage double tap), attendre que chacune d'elles réponde, puis combiner la réponse avec ce que l'utilisateur a entré à l'écran, exécuter une autre requête et passer à l'écran suivant, en lui transférant le résultat, lorsque ce Rx permettra de gérer succinctement les erreurs et de terminer cette chaîne (annuler les requêtes) en sortant de l'écran, et toute cette logique prendra deux douzaines de lignes de code tapé:



Il vaut la peine de parler de la seule propriété de disposeBag. Comme le montrent les captures d'écran, chaque abonnement y est placé, ce qui est nécessaire pour contrôler leur durée de vie. Autrement dit, les abonnements sont valables tant que le «sac» vit dans lequel ils ont été placés, dans ce cas, pendant que le contrôleur vit.

En plus de la compacité, il est difficile de faire une erreur dans le code ci-dessus, car chaque fermeture renvoie quelque chose et ne contient pas d'effets secondaires. C'est le pouvoir que nous recherchions.

Vous pouvez remarquer un autre point important: comme la classe n'a pas de propriétés, il n'est pas nécessaire d'écrire [self faible], ce qui affecte positivement la lisibilité du code. Toutes les fonctions peuvent et mieux être définies localement dans la méthode où elles sont utilisées, ou supprimées dans des classes distinctes. Par ailleurs, les liens vers les dépendances (ViewModel, Presenter, etc.) dans ce cas peuvent être passés comme argument à la méthode du contrôleur, dans ce cas, il n'est pas nécessaire de les enregistrer dans les propriétés. C'est correct.

Après examen, il est temps d'utiliser Observable pour simplifier le développement. Comment exactement? Revenons à l'idée de méthodes «propres» et essayons d'implémenter la logique d'un petit écran dans sa seule méthode. Pour plus de clarté, nous choisirons la méthode pour terminer le chargement de la vue (viewDidLoad). Naturellement, si l'écran est composé en IB, nous devrons créer des propriétés pour les points de vente, mais ce n'est pas effrayant, car les éléments eux-mêmes ne représentent pas la logique métier, donc ils n'affecteront pas grandement la complexité de l'écran. Mais si l'écran est composé de code, vous pouvez vous passer de propriétés (à l'exception du disposeBag), en créant des éléments dans notre méthode et en les utilisant directement. Qu'en est-il de l'impérativité des éléments UIKit décrits précédemment? Rx, en plus de l'approche elle-même, fournit des wrappers réactifs pour les composants d'interface utilisateur standard, de sorte que dans la plupart des cas, vous pouvez obtenir la séquence d'événements nécessaire sur place. Ou, inversement, liez l'Observable existant à, par exemple, une table - apportez-lui une demande afin qu'il mette à jour son contenu immédiatement après son achèvement:



La liaison aux collections est assez flexible, mais par défaut, elle ne fonctionne que via reloadData. Pour les mises à jour ponctuelles, il existe une merveilleuse bibliothèque déboguée du même auteur - RxDataSources. Avec lui, vous pouvez oublier les plantages lors des mises à jour par lots.

Que se passera-t-il ensuite? Une méthode à écran unique se développera et dans un cas assez compliqué, elle deviendra difficile à maintenir. Et lorsque cela se produit, il devient soudain clair que cette méthode ne dépend pas d'autre chose que d'elle-même, et l'approche réactive a divisé le code en blocs logiques qui peuvent facilement être placés dans des objets séparés en les concevant de manière similaire. Mais cette fois, les méthodes prendront déjà certaines dépendances à l'écran et retourneront un résultat. Le point positif est que la signature dans ce cas est obtenue le plus possible, on peut voir que la fonction nécessite son travail et quel est son résultat. Cela peut ressembler à ceci:



Des structures distinctes aident à garder l'en-tête de la méthode lisible et net, car il peut y avoir de nombreuses dépendances.

Il est important de comprendre que la méthode n'a pas à être la seule dans tout l'objet, l'essence de leur indépendance les uns par rapport aux autres. Grâce à Rx, leurs entrées et sorties peuvent être asynchrones et représenter un ou plusieurs observables, fournissant une autre dimension pour la manipulation des données.

Cette approche délie vos mains et vous permet d'implémenter des écrans de presque n'importe quelle complexité, tout en maintenant leur code explicite et faiblement couplé.

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


All Articles