En traitant avec SOLID, je suis souvent tombé sur le fait que ne pas suivre ces principes peut entraîner des problèmes. Les problèmes sont connus, mais mal formalisés. Cet article est écrit dans le but de formaliser les situations typiques qui surviennent dans le processus d'écriture du code des solutions possibles et les conséquences qui en découlent. Nous expliquerons pourquoi nous faisons face à un mauvais code et comment les problèmes augmentent avec la croissance du programme.
Malheureusement, dans la plupart des cas, l'évaluation se résume aux options «plusieurs» et «une», qui font allusion à l'insolvabilité de la notation O, mais même une telle analyse permettra de mieux comprendre quel code est vraiment dangereux pour le développement futur du système et quel code est tolérant.
Définition
Nous disons qu'un changement dans le programme nécessite «O» des actions f (n) si le programmeur n'a pas besoin de faire plus de f (n) changements logiquement séparés dans le programme pour implémenter le changement avec précision à un facteur constant, où n signifie la taille du programme.
Mesures
Examinez certaines des caractéristiques de conception de Robert Martin et évaluez-les en termes de notation O.
Une conception est difficile si un seul changement provoque une cascade d'autres changements dans les modules dépendants. Plus vous devez changer de modules, plus la conception est rigide.
La différence significative est les changements O (1) et O (n). C'est-à-dire notre conception permet un nombre constant de changements, ou à mesure que le programme se développe, le nombre de changements augmentera. Ensuite, nous devons considérer les changements eux-mêmes - eux aussi, peuvent se révéler rigides avec un certain comportement asymptotique. Ainsi, la rigidité peut être complexe jusqu'à O (nm). Le paramètre m sera appelé profondeur de rigidité. Même une estimation approximative de la profondeur de rigidité dans une conception qui autorise même la rigidité O (n) est trop compliquée pour une personne, car chacun des changements doit être vérifié pour des changements encore plus profonds.
La fragilité est la propriété d'un programme d'être endommagé à de nombreux endroits lors d'une seule modification. Souvent, de nouveaux problèmes surviennent dans des parties qui n'ont aucun lien conceptuel avec celui qui a été modifié.
Nous ne considérerons pas la question de la connexion logique des modules dans lesquels des changements se produisent. Du point de vue de la notation, il n'y a donc pas de différence entre fragilité et rigidité, et les arguments valables pour la rigidité s'appliquent à la fragilité.
Une conception est inerte si elle contient des pièces qui pourraient être utiles dans d'autres systèmes, mais les efforts et les risques associés à la tentative de séparer ces pièces du système d'origine sont trop importants.
Les risques et les efforts de cette définition peuvent être interprétés comme le nombre de changements qui se produisent dans le module lorsque vous essayez de l'abstraire du système d'origine aussi constant que la taille du module augmente. Cependant, comme le montre la pratique, cela vaut toujours la peine d'être résumé, car cela met de l'ordre dans le module lui-même et permet de le transférer vers d'autres projets. Très souvent, après le premier besoin de transférer le module vers un autre projet, d'autres similaires apparaissent.
La viscosité
Confronté à la nécessité d'apporter une modification, le développeur trouve généralement plusieurs façons de procéder. Certains préservent le design, d'autres non (c'est-à-dire qu'ils sont essentiellement un «hack»). Si les approches préservant la conception sont plus difficiles à mettre en œuvre qu'un hack, alors la viscosité de la conception est élevée. Résoudre le problème est mal facile, mais bien est difficile. Nous voulons concevoir nos programmes de sorte qu'il soit facile d'apporter des modifications qui préservent la conception.
La viscosité suivante peut être appelée myopie en termes de notation O. Oui, au début, le développeur a vraiment la possibilité de modifier O (1) au lieu de O (n) (en raison de la rigidité ou de la fragilité), mais souvent de tels changements conduisent à encore plus de rigidité et de fragilité, c'est-à-dire augmenter la profondeur de rigidité. Si vous ignorez une telle «cloche», les modifications ultérieures ne pourront plus être résolues avec un «hack» et vous devrez apporter des modifications aux conditions de rigidité (peut-être plus qu'avant la «cloche») ou mettre le système en bon état. Autrement dit, lorsque la viscosité est détectée, il est préférable de réécrire immédiatement le système correctement.
Cela se passe comme ceci: Ivan a besoin d'écrire du code qui recourbe son petit pied. En montant dans différentes parties du programme, où, comme il le soupçonne, le bokryad a été fumé plus d'une fois, il trouve un fragment approprié. Il le copie, le colle dans son module et effectue les modifications nécessaires.
Mais Ivan ne sait pas que le code qu'il a extrait avec la souris y a placé Peter, en le tirant du module écrit par Sveta. Sveta a été la première à fumer un petit trottoir, mais elle savait que ce processus était très similaire au tabagisme des marmottes. Elle a trouvé quelque part un code qui karmyachit karmaglot, l'a copié dans son module et modifié. Lorsque le même code apparaît encore et encore sous des formes légèrement différentes, les développeurs perdent l'idée de l'abstraction.
Dans cette situation, il devient évident que lorsqu'il est nécessaire de changer la base de l'action excavée, ce changement doit être effectué en n endroits. Étant donné la possibilité de modifications uniques dans chaque copie, cela peut également entraîner des modifications logiquement indépendantes. Dans ce cas, il est possible d'oublier simplement le changement dans un autre endroit, car il n'y a pas de protection au stade de la compilation. Cela peut donc également se transformer en O (n) itérations de test.
Application sur la notation SOLID
SRP (principe de responsabilité unique). Une entité logicielle ne devrait avoir qu'un seul motif de changement (responsabilité). En d'autres termes, par exemple, la classe ne doit pas suivre la logique métier et le mappage, car lors du changement d'une responsabilité, nous devons nous assurer que nous n'avons pas endommagé une autre responsabilité. C'est-à-dire que l'incohérence avec le principe SRP entraîne une rigidité et une fragilité. Suivre ce principe permet également de se débarrasser des modules d'inertie et de transfert d'un programme à un autre avec un nombre potentiellement plus faible de dépendances.
Le comportement asymptotique des changements reste fondamentalement le même que sans suivre le principe, mais le facteur constant diminue considérablement. Dans les deux cas, il faut vérifier l'intégralité du contenu de la classe et, en cas de changement d'interface de l'entité, les lieux où ils interagissent avec cette entité. Seul SRP suivant permet de réduire l'interface et la probabilité de son changement, ainsi que la quantité d'implémentation interne, qui après le changement peut être défectueuse. Un raisonnement similaire, tronqué à la discussion sur les interfaces, est valable pour ISP (Interface Segregation Principle).
OCP (Open Close Principle). Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour l'expansion et fermées pour la modification. Ceci doit être compris de manière à pouvoir modifier le comportement du module sans affecter son code source. En règle générale, cela est réalisé par héritage et polymorphisme. Étant donné que la violation du principe LSP est une violation d'OCP et que DIP est un moyen de maintenir OCP, les éléments suivants peuvent être appliqués à la fois au LSP et au DIP. Le non-respect du principe d'ouverture-ouverture oblige à effectuer des changements dans toutes les entités qui ne sont pas fermées concernant ce changement.
Une situation assez banale est, par exemple, la présence d'une chaîne de ifs qui détermine le type de variable dans la liste des classes enfants. Ces structures peuvent apparaître plusieurs fois dans le programme. Et chaque fois que vous ajoutez une nouvelle classe enfant, vous devez apporter les modifications appropriées dans chacune de ces chaînes. Des situations similaires peuvent survenir non seulement avec les classes d'enfants, mais aussi avec la prise en compte de tous les cas particuliers possibles [Il s'agit de cas non pas au moment de la rédaction, mais en général. Des cas peuvent apparaître plus tard].
Considérons maintenant la situation lorsque nous effectuons m changements du même type qui, en raison de la divergence avec le principe d'ouverture-proximité, nécessitent n opérations de notre part. Ensuite, si nous laissons tout tel quel, en prenant en charge l'architecture pour prendre en compte des cas spéciaux, et ne généralisons pas, nous obtiendrons la complexité globale pour tous les changements O (mn). Si nous fermons tous les m emplacements par rapport à ce changement, les changements ultérieurs prendront O (1) au lieu de O (m). Ainsi, la complexité globale est réduite à O (m + n). Cela signifie que le démarrage d'un OCP n'est jamais trop tard.
Martin dit à propos de cette situation que vous ne devriez pas deviner (si vous ne savez pas avec certitude, bien sûr) comment fermer le premier changement, mais après le premier changement, cela vaut la peine de fermer, car le premier changement était un marqueur que le système ne restera pas nécessairement dans l'état actuel. C'est logique, car nous faisons O (1 * n) actions en raison de la non-proximité, puis O (m) actions pour nous fermer des changements ultérieurs. Au total, nous obtenons la complexité globale O (n + m), mais en même temps, nous faisons toutes les actions exactement quand elles deviennent nécessaires et ne faisons rien à l'avance, sans savoir si cela sera nécessaire.
Motifs et notation O
Une autre analogie peut être établie entre la notation O dans la théorie du calcul et la notation O dans la conception. Cela consiste dans le fait que nous réduisons le nombre de calculs à l'aide d'algorithmes et de structures de données, tels que les arbres de recherche et les tas, qui résolvent les problèmes typiques plus rapidement que les solutions «frontales», et le nombre d'opérations d'un programmeur avec une bonne conception, dans lesquelles il peut également utiliser de bonnes solutions typiques. appelé modèles de conception. Vous pouvez évaluer l'effet des motifs dans le contexte des principes de SOLID et, par conséquent, dans le contexte de la notation O.
Par exemple, le modèle Mediator élimine la possibilité de casser quelque chose dans le programme lors du changement de la logique d'interaction entre deux objets, car il l'encapsule complètement et garantit la complexité constante d'un tel changement.
Le modèle d'adaptateur nous permet d'utiliser (lire et ajouter) des entités avec différentes interfaces, que nous utiliserons dans un but commun. À l'aide de ce modèle, vous pouvez incorporer un nouvel objet avec une interface incompatible dans le système pour le nombre d'opérations qui ne dépend pas de la taille du système.
Comme les structures de données peuvent prendre en charge certaines opérations avec de bonnes asymptotiques et d'autres avec de mauvaises, les modèles se comportent de manière flexible par rapport à certains changements et de manière rigide par rapport à d'autres.
Limites raisonnables
Lorsque nous traitons la notation O, en travaillant sur un problème d'optimisation, nous devons nous rappeler que pas toujours l'algorithme avec la meilleure asymptotique est le mieux adapté pour résoudre le problème. Il faut comprendre que le tri par bulle pour un tableau de 3 éléments fonctionnera plus rapidement que pyramidal, malgré le fait que le tri pyramidal présente une meilleure asymptotique. Pour les petites valeurs de n, un facteur constant joue un rôle important, que la notation O cache. La notation O dans la conception fonctionne de la même manière. Pour les petits projets, cela n'a aucun sens de clôturer un grand nombre de modèles, car les coûts de leur mise en œuvre dépassent le nombre de modifications qui devraient être apportées en raison d'une «mauvaise conception».