SOLIDE vs éviter l'abstraction prématurée


27

Je comprends ce que SOLID est censé accomplir et je l'utilise régulièrement dans des situations où la modularité est importante et ses objectifs sont clairement utiles. Cependant, deux choses m'empêchent de l'appliquer de manière cohérente dans ma base de code:

  • Je veux éviter une abstraction prématurée. D'après mon expérience, tracer des lignes d'abstraction sans cas d'utilisation concrets (du genre de ceux qui existent actuellement ou dans un avenir prévisible ) conduit à les tracer aux mauvais endroits. Lorsque j'essaie de modifier un tel code, les lignes d'abstraction gênent plutôt que d'aider. Par conséquent, j'ai tendance à me tromper en évitant de tracer des lignes d'abstraction jusqu'à ce que j'aie une bonne idée de l'endroit où elles seraient utiles.

  • Je trouve difficile de justifier l'augmentation de la modularité pour elle-même si cela rend mon code plus verbeux, plus difficile à comprendre, etc. et n'élimine aucune duplication. Je trouve que le code procédural ou objet Dieu simple et étroitement couplé est parfois plus facile à comprendre qu'un code ravioli très bien factorisé car le flux est simple et linéaire. C'est aussi beaucoup plus facile à écrire.

D'un autre côté, cet état d'esprit mène souvent à des objets divins. Je refaçonne généralement ces derniers de façon conservatrice, en ajoutant des lignes d'abstraction claires uniquement lorsque je vois des modèles clairs émerger. Qu'est-ce qui ne va pas avec les objets de Dieu et le code étroitement couplé si vous n'avez pas clairement besoin de plus de modularité, n'avez pas de duplication significative et le code est lisible?

EDIT: En ce qui concerne les principes SOLID individuels, je voulais souligner que la substitution de Liskov est à mon humble avis une formalisation du bon sens et devrait être appliquée partout, car les abstractions n'ont aucun sens si ce n'est pas le cas. En outre, chaque classe devrait avoir une responsabilité unique à un certain niveau d'abstraction, bien qu'il puisse s'agir d'un niveau très élevé avec les détails d'implémentation regroupés dans une énorme classe de 2 000 lignes. Fondamentalement, vos abstractions doivent avoir un sens là où vous choisissez d'abstraire. Les principes que je remets en question dans les cas où la modularité n'est pas clairement utile sont la fermeture ouverte, la ségrégation d'interfaces et surtout l'inversion de dépendance, car il s'agit de modularité, et pas seulement d'avoir des abstractions sensées.


10
@ S.Lott: Le mot clé est "plus". C'est précisément le genre de chose pour laquelle YAGNI a été inventé. Augmenter la modularité ou diminuer le couplage simplement pour lui-même est juste une programmation culte.
Mason Wheeler

3
@ S.Lott: Cela devrait être évident dans le contexte. Comme je l'ai lu, au moins, "plus que ce qui existe actuellement dans le code en question."
Mason Wheeler

3
@ S.Lott: Si vous insistez pour être pédant, "plus" fait référence à plus qu'il n'en existe actuellement. L'implication est que quelque chose, qu'il s'agisse de code ou d'une conception approximative, existe déjà et que vous réfléchissez à la façon de le refactoriser.
dsimcha

5
@ back2dos: D'accord, mais je suis sceptique quant à l'ingénierie de quelque chose d'extensibilité sans une idée claire de la façon dont il est susceptible d'être étendu. Sans ces informations, vos lignes d'abstraction finiront probablement au mauvais endroit. Si vous ne savez pas où les lignes d'abstraction seront probablement utiles, je suggère que vous écriviez simplement le code le plus concis qui fonctionne et qui soit lisible, même si vous avez quelques objets God jetés dedans.
dsimcha

2
@dsimcha: Vous avez de la chance si les exigences changent après avoir écrit un morceau de code, car elles changent généralement pendant que vous le faites. C'est pourquoi les implémentations d'instantanés ne conviennent pas pour survivre à un cycle de vie logiciel réel. De plus, SOLID ne vous oblige pas à faire un résumé à l'aveugle. Le contexte d'une abstraction est défini par l'inversion de dépendance. Vous réduisez la dépendance de chaque module à l'abstraction la plus simple et la plus claire des autres services sur lesquels il s'appuie. Il n'est pas arbitraire, artificiel ou complexe, mais bien défini, plausible et simple.
back2dos

Réponses:


8

Voici des principes simples que vous pouvez appliquer pour vous aider à comprendre comment équilibrer la conception de votre système:

  • Principe de responsabilité unique: le Sin SOLID. Vous pouvez avoir un très grand objet en termes de nombre de méthodes ou de quantité de données tout en respectant ce principe. Prenons par exemple l'objet Ruby String. Cet objet a plus de méthodes que vous ne pouvez le faire, mais il n'a qu'une seule responsabilité: tenir une chaîne de texte pour l'application. Dès que votre objet commence à assumer une nouvelle responsabilité, réfléchissez bien à cela. Le problème de maintenance est de vous demander "où puis-je m'attendre à trouver ce code si j'ai eu des problèmes avec lui plus tard?"
  • La simplicité compte: Albert Einstein a déclaré: "Rendez tout aussi simple que possible, mais pas plus simple." Avez-vous vraiment besoin de cette nouvelle méthode? Pouvez-vous accomplir ce dont vous avez besoin avec les fonctionnalités existantes? Si vous pensez vraiment qu'il doit y avoir une nouvelle méthode, pouvez-vous changer une méthode existante pour satisfaire tout ce dont vous avez besoin? Essentiellement refactoriser lorsque vous construisez de nouvelles choses.

En substance, vous essayez de faire ce que vous pouvez pour ne pas vous tirer une balle dans le pied quand vient le temps de maintenir le logiciel. Si vos gros objets sont des abstractions raisonnables, il n'y a pas de raison de les diviser simplement parce que quelqu'un a proposé une métrique qui dit qu'une classe ne doit pas être plus de X lignes / méthodes / propriétés / etc. Si quoi que ce soit, ce sont des lignes directrices et non des règles dures et rapides.


3
Bon commentaire. Comme discuté dans d'autres réponses, un problème avec la «responsabilité unique» est que la responsabilité d'une classe peut être décrite à divers niveaux d'abstraction.
dsimcha

5

Je pense que vous avez pour la plupart répondu à votre propre question. SOLID est un ensemble de directives sur la façon de refactoriser votre code, lorsque les concepts doivent être remontés à des niveaux d'abstractions.

Comme toutes les autres disciplines créatives, il n'y a pas d'absolu - juste des compromis. Plus vous le faites, plus il devient «facile» de décider quand est suffisant pour votre domaine ou vos besoins actuels.

Cela dit - l'abstrait est au cœur du développement de logiciels - donc s'il n'y a pas de bonne raison de ne pas le faire, alors la pratique vous rendra meilleur et vous aurez une intuition pour les compromis. Privilégiez-la donc à moins qu'elle ne devienne préjudiciable.


5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

C'est partiellement vrai et partiellement faux.

Mauvais
Ce n'est pas une raison pour empêcher que l'on applique les principes OO / SOLID. Cela empêche seulement de les appliquer trop tôt.

Lequel est...

Droite
Ne refactorisez pas le code tant qu'il n'est pas refactorisable; lorsque les exigences sont remplies. Ou, lorsque les «cas d'utilisation» sont tous là comme vous le dites.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

Lorsque vous travaillez seul ou en silo
Le problème de ne pas faire d'OO n'est pas immédiatement évident. Après avoir écrit la première version d'un programme:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 de ces bonnes choses meurent rapidement en peu de temps.

Enfin, vous reconnaissez que vous avez des objets divins (objets avec de nombreuses fonctions), vous reconnaissez donc des fonctions individuelles. Encapsuler. Mettez-les dans des dossiers. Utilisez des interfaces de temps en temps. Faites-vous plaisir pour l'entretien à long terme. D'autant plus que les méthodes non refactorisées ont tendance à gonfler à l'infini et à devenir des méthodes divines.

En équipe
Les problèmes de non-OO sont immédiatement perceptibles. Un bon code OO est au moins quelque peu auto-documenté et lisible. Surtout lorsqu'il est combiné avec un bon code vert. De plus, le manque de structure rend difficile la séparation des tâches et l'intégration beaucoup plus complexe.


Droite. Je reconnais vaguement que mes objets font plus d'une chose, mais je ne sais pas plus concrètement comment les séparer utilement afin que les lignes d'abstraction soient aux bons endroits lorsque l'entretien doit être fait. Il semble être une perte de temps de refactoriser si le programmeur de maintenance devra probablement faire beaucoup plus de refactoring pour mettre les lignes d'abstraction au bon endroit de toute façon.
dsimcha

L'encapsulation et l'abstraction sont deux concepts différents. L'encapsulation est un concept OO de base qui signifie essentiellement que vous avez des classes. Les classes offrent une modularité et une lisibilité en elles-mêmes. C'est utile.
P.Brian.Mackey

L'abstraction peut diminuer l'entretien lorsqu'elle est appliquée correctement. Appliqué incorrectement, il peut augmenter l'entretien. Savoir quand et où tracer les lignes nécessite une solide compréhension de l'entreprise, du cadre, du client, en plus des aspects techniques de base de la façon «d'appliquer un modèle d'usine» en soi.
P.Brian.Mackey

3

Il n'y a rien de mal avec les gros objets et le code étroitement couplé lorsqu'ils sont appropriés et lorsque rien de mieux conçu n'est requis . Ceci est juste une autre manifestation d' une règle d'or qui se transforme en dogme.

Le couplage lâche et les petits objets simples ont tendance à fournir des avantages spécifiques dans de nombreux cas courants, c'est donc une bonne idée de les utiliser, en général. Le problème réside dans les personnes qui ne comprennent pas la raison d'être des principes qui essaient aveuglément de les appliquer universellement, même là où elles ne s'appliquent pas.


1
+1. Il faut encore une bonne définition de ce que signifie "approprié".
jprete

2
@jprete: Pourquoi? Essayer d'appliquer des définitions absolues fait partie de la cause de gâchis conceptuels comme celui-ci. Il faut beaucoup de savoir-faire pour créer de bons logiciels. L'IMO a vraiment besoin d'expérience et de bon jugement.
Mason Wheeler

4
Eh bien, oui, mais une définition large de quelque sorte est toujours utile.
jprete

1
Il serait utile d'avoir une définition de approprié, mais c'est plutôt le point. Il est difficile de cerner une bouchée de son, car elle ne s'applique qu'au cas par cas. La question de savoir si vous faites le choix approprié dans chaque cas dépend en grande partie de l'expérience. Si vous ne pouvez pas expliquer pourquoi vous êtes à haute cohésion, une solution monolithique est meilleure qu'une solution à faible cohésion et hautement modulaire, vous feriez "probablement" mieux d'aller à faible cohésion et modulaire. Il est toujours plus facile de refactoriser.
Ian

1
Je préfère que quelqu'un écrive aveuglément du code découplé au lieu d'écrire aveuglément du code de spaghetti :)
Boris Yankov

2

J'ai tendance à aborder cela plus du point de vue de You Ain't Gonna Need It. Ce message se concentrera sur un élément en particulier: l'héritage.

Dans ma conception initiale, je crée une hiérarchie de choses dont je sais qu'il y en aura deux ou plus. Il est probable qu'ils auront besoin d'une grande partie du même code, il vaut donc la peine de le concevoir dès le départ. Une fois que le code initial est en place et que j'ai besoin d'ajouter plus de fonctionnalités / fonctionnalités, je regarde ce que j'ai et je pense: "Est-ce que quelque chose que j'ai déjà implémente la même fonction ou une fonction similaire?" Si c'est le cas, c'est très probablement une nouvelle abstraction qui demande à être publiée.

Un bon exemple de ceci est un framework MVC. À partir de zéro, vous créez une grande page avec un code derrière. Mais alors vous voulez ajouter une autre page. Cependant, le code derrière implémente déjà une grande partie de la logique requise par la nouvelle page. Donc, vous résumez une Controllerclasse / interface qui implémentera la logique spécifique à cette page, laissant les trucs communs dans le code "dieu" d'origine.


1

Si VOUS savez en tant que développeur quand NE PAS appliquer les principes en raison de l'avenir du code, alors c'est bon pour vous.

J'espère que SOLID reste dans votre esprit pour savoir quand les choses doivent être abstraites pour éviter la complexité et améliorer la qualité du code s'il commence à se dégrader dans les valeurs que vous avez indiquées.

Mais plus important encore, considérez que la majorité des développeurs font le travail de jour et ne s'en soucient pas autant que vous. Si vous avez initialement défini des méthodes de longue durée, quels sont les autres développeurs qui entrent dans le code vont penser quand ils viennent pour le maintenir ou l'étendre? ... Oui, vous l'avez deviné, BIGGER fonctions, LONGER running classes et PLUS les objets divins, et ils n'ont pas les principes en tête pour pouvoir attraper le code croissant et l'encapsuler correctement. Votre code "lisible" devient maintenant un gâchis pourri.

Vous pouvez donc affirmer qu'un mauvais développeur va le faire malgré tout, mais au moins vous avez un meilleur avantage si les choses restent simples et bien organisées dès le départ.

Cela ne me dérange pas particulièrement une "carte de registre" globale ou un "objet divin" si elles sont simples. Cela dépend vraiment de la conception du système, parfois vous pouvez vous en tirer et rester simple, parfois vous ne pouvez pas.

N'oublions pas non plus que les grandes fonctions et les objets divins peuvent s'avérer extrêmement difficiles à tester. Si vous voulez rester agile et vous sentir "en sécurité" lors de la refactorisation et de la modification du code, vous avez besoin de tests. Les tests sont difficiles à écrire lorsque les fonctions ou les objets font beaucoup de choses.


1
Fondamentalement, une bonne réponse. Mon seul reproche est que, si un développeur de maintenance arrive et fait un gâchis, c'est sa faute de ne pas avoir refactorisé, à moins que de tels changements semblent suffisamment probants pour justifier leur planification ou que le code soit si mal documenté / testé / etc. . ce refactoring aurait été une forme de folie.
dsimcha

C'est vrai dsimcha. C'est juste que je pensais dans un système où un développeur crée un dossier "RequestHandlers" et chaque classe sous ce dossier est un gestionnaire de requêtes, puis le prochain développeur qui arrive et pense: "J'ai besoin de mettre un nouveau gestionnaire de requêtes", l'infrastructure actuelle l'oblige presque à suivre la convention. Je pense que c'est ce que j'essayais de dire. La mise en place d'une convention évidente dès le départ peut guider même les développeurs les plus inexpérimentés à poursuivre le modèle.
Martin Blore

1

Le problème avec un objet divin est que vous pouvez généralement le briser en morceaux de manière rentable. Par définition, il fait plus d'une chose. Le but de le diviser en classes plus petites est de faire en sorte que vous puissiez avoir une classe qui fait bien une chose (et vous ne devriez pas non plus étirer la définition de "une chose"). Cela signifie que, pour une classe donnée, une fois que vous savez la seule chose qu'elle est censée faire, vous devriez être capable de la lire assez facilement et de dire si oui ou non elle fait correctement cette chose.

Je pense qu'il y a une telle chose comme avoir trop de modularité et trop de flexibilité, mais c'est plus un problème de sur-conception et de sur-ingénierie, où vous tenez compte d'exigences que vous ne savez même pas que le client veut. De nombreuses idées de conception visent à faciliter le contrôle des modifications de la façon dont le code est exécuté, mais si personne ne s'attend à ce changement, il est inutile d'inclure la flexibilité.


"Fait plus d'une chose" peut être difficile à définir, selon le niveau d'abstraction sur lequel vous travaillez. On pourrait faire valoir que toute méthode avec deux ou plusieurs lignes de code fait plus d'une chose. À l'autre extrémité du spectre, quelque chose de complexe comme un moteur de script va avoir besoin de beaucoup de méthodes qui font toutes des "choses" individuelles qui ne sont pas directement liées les unes aux autres, mais qui comprennent chacune une partie importante de la "méta- chose "qui permet à vos scripts de fonctionner correctement, et il n'y a que tant de séparation qui peut être faite sans casser le moteur de script.
Mason Wheeler

Il y a certainement un éventail de niveaux conceptuels, mais nous pouvons choisir un point à ce sujet: la taille d'une «classe qui fait une chose» devrait être telle que vous puissiez presque immédiatement comprendre la mise en œuvre. C'est-à-dire que les détails de mise en œuvre vous conviennent. Si elle grossit, alors vous ne pouvez pas comprendre toute la classe à la fois. Si elle est plus petite, alors vous abstenez trop. Mais comme vous l'avez dit dans un autre commentaire, il y a beaucoup de jugement et d'expérience impliqués.
jprete

À mon humble avis, le meilleur niveau d'abstraction à penser est le niveau que vous attendez des consommateurs de l'objet à utiliser. Supposons que vous ayez un objet appelé Queryerqui optimise les requêtes de base de données, interroge le SGBDR et analyse les résultats en objets à renvoyer. S'il n'y a qu'une seule façon de le faire dans votre application et qu'elle Queryerest hermétiquement scellée et encapsule cette façon, alors cela ne fait qu'une chose. S'il y a plusieurs façons de le faire et que quelqu'un peut se soucier des détails d'une partie, il fait plusieurs choses et vous voudrez peut-être le diviser.
dsimcha

1

J'utilise une directive plus simple: si vous pouvez écrire des tests unitaires pour cela, et ne pas avoir de duplication de code, c'est assez abstrait. De plus, vous êtes en bonne position pour une refactorisation ultérieure.

Au-delà de cela, vous devez garder SOLID à l'esprit, mais plus comme ligne directrice que comme règle.


0

Qu'est-ce qui ne va pas avec les objets de Dieu et le code étroitement couplé si vous n'avez clairement pas besoin de plus de modularité, n'avez pas de duplication significative et le code est lisible?

Si l'application est suffisamment petite, tout est maintenable. Dans les applications plus grandes, les objets divins deviennent rapidement un problème. Finalement, l'implémentation d'une nouvelle fonctionnalité nécessite de modifier 17 objets divins. Les gens ne réussissent pas très bien en suivant les procédures en 17 étapes. Les objets de Dieu sont constamment modifiés par plusieurs développeurs, et ces changements doivent être fusionnés à plusieurs reprises. Tu ne veux pas y aller.


0

Je partage vos préoccupations concernant l'abstraction excessive et inappropriée, mais je ne suis pas nécessairement aussi préoccupé par l'abstraction prématurée.

Cela ressemble probablement à une contradiction, mais l'utilisation d'une abstraction est peu susceptible de causer un problème tant que vous ne vous engagez pas trop tôt - c'est-à-dire tant que vous êtes disposé et capable de refactoriser si nécessaire.

Cela implique un certain degré de prototypage, d'expérimentation et de retour en arrière dans le code.

Cela dit, il n'y a pas de règle déterministe simple à suivre pour résoudre tous les problèmes. L'expérience compte beaucoup, mais vous devez acquérir cette expérience en faisant des erreurs. Et il y a toujours plus d'erreurs à faire que les erreurs précédentes ne vous auront pas préparé.

Néanmoins, traitez les principes du manuel comme un point de départ, puis apprenez à programmer beaucoup et voyez comment ces principes fonctionnent. S'il était possible de donner des directives meilleures, plus précises et plus fiables que des choses comme SOLID, quelqu'un l'aurait probablement fait maintenant - et même s'ils l'avaient fait, ces principes améliorés seraient toujours imparfaits et les gens se poseraient des questions sur les limites de ces .

Bon travail aussi - si quelqu'un pouvait fournir un algorithme clair et déterministe pour concevoir et coder des programmes, il n'y aurait qu'un seul programme à écrire pour les humains - le programme qui écrirait automatiquement tous les futurs programmes sans intervention humaine.


0

Tracer les lignes d'abstraction appropriées est quelque chose que l'on tire de l'expérience. Vous ferez des erreurs en le faisant, c'est indéniable.

SOLID est une forme concentrée de cette expérience qui vous est offerte par des personnes qui ont acquis cette expérience à la dure. Vous l'avez pratiquement cloué lorsque vous dites que vous vous abstiendriez de créer une abstraction avant d'en voir le besoin. Eh bien, l'expérience SOLID vous aidera à résoudre des problèmes que vous n'avez jamais connus . Mais croyez-moi ... il y en a de très réels.

SOLID concerne la modularité, la modularité concerne la maintenabilité, et la maintenabilité consiste à vous permettre de garder un horaire de travail humain une fois que le logiciel atteint la production et que le client commence à remarquer des bogues que vous n'avez pas.

Le plus grand avantage de la modularité est la testabilité. Plus votre système est modulaire, plus il est facile à tester et plus vite vous pouvez construire des fondations solides pour faire face aux aspects les plus difficiles de votre problème. Ainsi, votre problème peut ne pas exiger cette modularité, mais le fait qu'il vous permettra de créer un meilleur code de test plus rapidement est une évidence pour viser la modularité.

Être agile, c'est essayer de trouver l'équilibre délicat entre l'expédition rapide et la bonne qualité. Agile ne signifie pas que nous devons faire des économies, en fait, les projets agiles les plus réussis avec lesquels j'ai été impliqué sont ceux qui ont prêté une attention particulière à ces directives.


0

Un point important qui semble avoir été négligé est que SOLID est pratiqué avec TDD. TDD a tendance à mettre en évidence ce qui est "approprié" dans votre contexte particulier et à aider à atténuer une grande partie de l'équivoque qui semble être dans les conseils.


1
Veuillez développer votre réponse. À l'heure actuelle, vous réunissez deux concepts orthogonaux et on ne sait pas comment le résultat répond aux préoccupations du PO.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.