Refactorisation et principe ouvert / fermé

12

J'ai récemment lu un site Web sur le développement de code propre (je ne mets pas de lien ici car il n'est pas en anglais).

Un des principes annoncés par ce site est le principe ouvert fermé : chaque composant logiciel doit être ouvert pour extension et fermé pour modification. Par exemple, lorsque nous avons implémenté et testé une classe, nous ne devons la modifier que pour corriger des bogues ou ajouter de nouvelles fonctionnalités (par exemple de nouvelles méthodes qui n'influencent pas celles existantes). La fonctionnalité et l'implémentation existantes ne doivent pas être modifiées.

J'applique normalement ce principe en définissant une interface Iet une classe d'implémentation correspondante A. Lorsque la classe Aest devenue stable (implémentée et testée), je ne la modifie normalement pas trop (éventuellement, pas du tout), c'est-à-dire

  1. Si de nouvelles exigences arrivent (par exemple des performances ou une implémentation totalement nouvelle de l'interface) qui nécessitent de gros changements dans le code, j'écris une nouvelle implémentation Bet continue à l'utiliser Atant qu'elle Bn'est pas mature. Une fois Barrivé à maturité, il suffit de changer la façon dont il Iest instancié.
  2. Si les nouvelles exigences suggèrent également une modification de l'interface, je définis une nouvelle interface I'et une nouvelle implémentation A'. Alors I, Asont congelés et restent la mise en œuvre du système de production aussi longtemps que I'et A'ne sont pas assez stables pour les remplacer.

Ainsi, au vu de ces observations, j'ai été un peu surpris que la page Web suggère alors l'utilisation de refactorings complexes , "... car il n'est pas possible d'écrire du code directement dans sa forme finale".

N'y a-t-il pas une contradiction / conflit entre l'application du principe ouvert / fermé et la suggestion de l'utilisation de refactorings complexes comme meilleure pratique? Ou l'idée ici est que l'on peut utiliser des refactorings complexes pendant le développement d'une classe A, mais quand cette classe a été testée avec succès, elle devrait être gelée?

Giorgio
la source

Réponses:

9

Je pense au principe Open-Closed comme un objectif de conception . Si vous finissez par le violer, cela signifie que votre conception initiale a échoué, ce qui est certainement possible, et même probable.

La refactorisation signifie que vous changez la conception sans changer la fonctionnalité. Vous modifiez probablement votre conception, car elle présente un problème. Le problème est peut-être qu'il est difficile de suivre le principe d'ouverture-fermeture lors de la modification du code existant, et vous essayez de résoudre ce problème.

Vous pourriez faire une refactorisation pour permettre d'implémenter votre prochaine fonctionnalité sans violer l'OCP lorsque vous le faites.

Scott Whitlock
la source
Vous ne devriez certainement pas penser comme un principe comme un objectif de conception . Ce sont des outils - vous ne faites pas que le logiciel soit joli et théoriquement correct à l'intérieur, vous essayez de produire de la valeur pour votre client. C'est une ligne directrice , rien de plus.
T. Sar
@ T.Sar Un principe est une ligne directrice, quelque chose que vous recherchez, ils sont orientés vers la maintenabilité et l'évolutivité. Cela ressemble à un objectif de conception pour moi. Je ne peux pas voir un principe comme un outil dans la façon dont je vois un modèle de conception ou un cadre comme un outil.
Tulains Córdova
@ TulainsCórdova Maintenabilité, performance, exactitude, évolutivité - ce sont des objectifs. Le principe Open-Closed est un moyen pour eux - juste un parmi tant d'autres. Vous n'avez pas besoin de pousser quelque chose vers le principe ouvert-fermé si cela ne lui est pas applicable ou si cela nuirait aux objectifs réels du projet. Vous ne vendez pas "Open-closedness" à un client. À titre indicatif , ce n'est pas mieux qu'une règle de base qui peut être écartée si vous finissez par trouver un moyen de faire votre chose d'une manière plus lisible et claire. Les directives sont des outils, après tout, rien de plus.
T. Sar
@ T.Sar Il y a tellement de choses que vous ne pouvez pas vendre à un client ... En revanche, je suis d'accord avec vous en ce qu'il ne faut pas faire des choses qui nuisent aux objectifs du projet.
Tulains Córdova
9

Le principe Open-Closed est davantage un indicateur de la qualité de la conception de votre logiciel ; pas un principe à suivre littéralement. C'est également un principe qui nous aide à ne pas modifier accidentellement les interfaces existantes (classes et méthodes que vous appelez et comment vous vous attendez à ce qu'elles fonctionnent).

L'objectif est d'écrire des logiciels de qualité. L'une de ces qualités est l'extensibilité. Cela signifie qu'il est facile d'ajouter, de supprimer et de modifier le code, ces changements ayant tendance à être limités à autant de classes existantes que possible. L'ajout de nouveau code est moins risqué que le changement de code existant, donc à cet égard, Open-Closed est une bonne chose à faire. Mais de quel code parlons-nous exactement? Le crime de violation de l'OC est beaucoup moins important lorsque vous pouvez ajouter de nouvelles méthodes à une classe au lieu de devoir modifier celles existantes.

OC est fractal . Il s'applique à toutes les profondeurs de votre conception. Tout le monde suppose qu'il n'est appliqué qu'au niveau de la classe. Mais elle est également applicable au niveau méthode ou au niveau assemblage.

Une violation trop fréquente du CO au niveau approprié suggère qu'il est peut-être temps de refactoriser . Le «niveau approprié» est un appel au jugement qui a tout à voir avec votre conception globale.

Suivre Open-Closed signifie littéralement que le nombre de classes va exploser. Vous allez créer des «I» majuscules inutilement. Vous vous retrouverez avec des bits de fonctionnalité répartis entre les classes et vous devrez ensuite écrire beaucoup plus de code pour tout câbler. À un moment donné, vous comprendrez qu'il aurait été préférable de changer la classe d'origine.

radarbob
la source
2
"Le crime de violation de l'OC est beaucoup moins important lorsque vous pouvez ajouter de nouvelles méthodes à une classe au lieu de devoir modifier celles existantes.": Pour autant que je comprends, l'ajout de nouvelles méthodes ne viole pas du tout le principe de l'OC (ouvert pour l'extension) . Le problème est de changer les méthodes existantes qui implémentent une interface bien définie et ont donc déjà une sémantique bien définie (fermée pour modification). En principe, le refactoring ne change pas la sémantique, donc le seul risque que je puisse voir est l'introduction de bugs dans du code déjà stable et bien testé.
Giorgio
1
Voici la réponse CodeReview qui illustre ouvert pour l'extension . Cette conception de classe est extensible. En revanche, l'ajout d'une méthode modifie la classe.
radarbob
L'ajout de nouvelles méthodes viole LSP, pas OCP.
Tulains Córdova
1
L'ajout de nouvelles méthodes ne viole pas le LSP. Si vous ajoutez une méthode, vous avez introduit une nouvelle interface @ TulainsCórdova
RubberDuck
6

Le principe Open-Closed semble être un principe qui est apparu avant que TDD ne soit plus répandu. L'idée étant qu'il est risqué de refactoriser le code parce que vous pourriez casser quelque chose, il est donc plus sûr de laisser le code existant tel quel et de simplement l'ajouter. En l'absence de tests, cela a du sens. L'inconvénient de cette approche est l'atrophie du code. Chaque fois que vous étendez une classe plutôt que de la refactoriser, vous vous retrouvez avec une couche supplémentaire. Vous boulonnez simplement le code sur le dessus. Chaque fois que vous ajoutez plus de code, vous augmentez les risques de duplication. Imaginer; il y a un service dans ma base de code que je veux utiliser, je trouve qu'il n'a pas ce que je veux donc je crée une nouvelle classe pour l'étendre et inclure mes nouvelles fonctionnalités. Un autre développeur arrive plus tard et souhaite également utiliser le même service. Malheureusement, ils ne le font pas t réaliser que ma version étendue existe. Ils codent par rapport à l'implémentation d'origine, mais ils ont également besoin d'une des fonctionnalités que j'ai codées. Au lieu d'utiliser ma version, ils étendent désormais également l'implémentation et ajoutent la nouvelle fonctionnalité. Nous avons maintenant 3 classes, l'original et deux nouvelles versions qui ont des fonctionnalités dupliquées. Suivez le principe ouvert / fermé et cette duplication continuera de s'accumuler pendant la durée de vie du projet, menant à une base de code inutilement complexe.

Avec un système bien testé, il n'est pas nécessaire de subir cette atrophie de code, vous pouvez refactoriser en toute sécurité le code permettant à votre conception d'assimiler de nouvelles exigences plutôt que de devoir continuellement boulonner le nouveau code. Ce style de développement est appelé conception émergente et conduit à des bases de code qui sont capables de rester en bonne forme pendant toute leur durée de vie plutôt que de collecter progressivement la croûte.

opsb
la source
1
Je ne suis pas un partisan du principe ouvert-fermé ni du TDD (en ce sens que je ne les ai pas inventés). Ce qui m'a surpris, c'est que quelqu'un a proposé le principe ouvert-fermé ET l'utilisation du refactoring ET du TDD en même temps. Cela me semblait contradictoire et j'essayais donc de trouver un moyen de rassembler toutes ces lignes directrices dans un processus cohérent.
Giorgio
"L'idée étant qu'il est risqué de refactoriser le code parce que vous pourriez casser quelque chose, il est donc plus sûr de laisser le code existant tel quel et de simplement y ajouter.": En fait, je ne le vois pas de cette façon. L'idée est plutôt d'avoir de petites unités autonomes que vous pouvez remplacer ou étendre (permettant ainsi au logiciel d'évoluer), mais vous ne devez pas toucher chaque unité une fois qu'elle a été soigneusement testée.
Giorgio
Vous devez penser que la classe ne sera pas seulement utilisée dans votre base de code. La bibliothèque que vous écrivez peut être utilisée dans d'autres projets. OCP est donc important. De plus, un nouveau programmeur ne connaissant pas une classe d'extension avec les fonctionnalités dont il / elle a besoin est un problème de communication / documentation, pas un problème de conception.
Tulains Córdova
@ TulainsCórdova dans le code d'application, cela n'est pas pertinent. Pour le code de bibliothèque, je dirais que le versionnage sémantique était mieux adapté pour communiquer les changements de rupture.
opsb
1
@ TulainsCórdova avec la stabilité de l'API de code de bibliothèque est beaucoup plus important car il n'est pas possible de tester le code client. Avec le code d'application, votre couverture de test vous informera immédiatement de toute rupture. Autrement dit, le code d'application est capable de faire des changements de rupture sans risque tandis que le code de bibliothèque doit gérer le risque en maintenant une API stable et en signalant les ruptures en utilisant par exemple le versioning sémantique
opsb
6

Dans les mots du profane:

A. Le principe d'O / C signifie que la spécialisation doit être effectuée en étendant, et non en modifiant une classe pour répondre à des besoins spécialisés.

B. L'ajout de fonctionnalités manquantes (non spécialisées) signifie que la conception n'était pas terminée et que vous devez l'ajouter à la classe de base, évidemment sans violer le contrat. Je pense que cela ne viole pas le principe.

C. La refactorisation ne viole pas le principe.

Lorsqu'un dessin arrive à maturité , disons, après un certain temps de production:

  • Il devrait y avoir très peu de raisons de le faire (point B), tendant vers zéro au fil du temps.
  • (Point C) sera toujours possible bien que plus rare.
  • Toute nouvelle fonctionnalité est censée être une spécialisation, ce qui signifie que les classes doivent être étendues (héritées de) (point A).
Tulains Córdova
la source
Le principe ouvert / fermé est très mal compris. Vos points A et B sont parfaitement exacts.
gnasher729
1

Pour moi, le principe ouvert-fermé est une ligne directrice, pas une règle stricte et rapide.

En ce qui concerne la partie ouverte du principe, les classes finales en Java et les classes en C ++ avec tous les constructeurs déclarés privés violent la partie ouverte du principe ouvert-fermé. Il existe de bons cas d'utilisation solides (remarque: solide, pas SOLIDE) pour les classes finales. La conception d'extensibilité est importante. Cependant, cela demande beaucoup de prévoyance et d'efforts, et vous contournez toujours la ligne de violation de YAGNI (vous n'en aurez pas besoin) et injectez l'odeur de code de généralité spéculative. Les composants logiciels clés doivent-ils être ouverts pour extension? Oui. Tout? Non. C'est en soi une généralité spéculative.

En ce qui concerne la partie fermée, lors du passage de la version 2.0 à 2.1 à 2.2 à 2.3 de certains produits, ne pas modifier le comportement est une bonne idée. Les utilisateurs n'aiment vraiment pas que chaque version mineure casse leur propre code. Cependant, en cours de route, on constate souvent que l'implémentation initiale de la version 2.0 a été fondamentalement rompue ou que les contraintes externes qui limitaient la conception initiale ne s'appliquent plus. Êtes-vous souriant et le supportez-vous et maintenez-vous cette conception dans la version 3.0, ou rendez-vous 3.0 non rétrocompatible à certains égards? La rétrocompatibilité peut être une énorme contrainte. Les limites des versions majeures sont l'endroit où la rupture de la compatibilité descendante est acceptable. Vous devez être conscient que cela pourrait perturber vos utilisateurs. Il doit y avoir de bonnes raisons pour lesquelles cette rupture avec le passé est nécessaire.

David Hammen
la source
0

La refactorisation, par définition, change la structure du code sans changer le comportement. Ainsi, lorsque vous refactorisez, vous n'ajoutez pas de nouvelles fonctionnalités.

Ce que vous avez fait comme exemple pour le principe Open Close semble OK. Ce principe consiste à étendre le code existant avec de nouvelles fonctionnalités.

Cependant, ne vous trompez pas. Je n'implique pas que vous ne devriez faire que des fonctionnalités ou ne refactoriser que de gros morceaux de données. La façon la plus courante de programmer consiste à faire un peu de fonctionnalité plutôt qu'à faire immédiatement un peu de refactoring (combiné avec des tests bien sûr pour vous assurer que vous n'avez changé aucun comportement). Un refactoring complexe ne signifie pas un «gros» refactoring, cela signifie appliquer des techniques de refactoring compliquées et bien pensées.

À propos des principes SOLID. Ce sont vraiment de bonnes directives pour le développement de logiciels, mais ce ne sont pas des règles religieuses à suivre aveuglément. Parfois, plusieurs fois, après avoir ajouté une deuxième et une troisième et une nième fonctionnalités, vous réalisez que votre conception initiale, même si elle respecte l'Open-Close, ne respecte pas d'autres principes ou exigences logicielles. Il y a des points dans l'évolution d'une conception et d'un logiciel où des changements plus complexes doivent être effectués. Il s'agit de trouver et de réaliser ces problèmes le plus rapidement possible et d'appliquer au mieux les techniques de refactoring.

La conception parfaite n'existe pas. Il n’existe pas une telle conception qui puisse et devrait respecter tous les principes ou modèles existants. C'est coder l'utopie.

J'espère que cette réponse vous a aidé dans votre dilemme. N'hésitez pas à demander des clarifications si besoin.

Patkos Csaba
la source
1
"Donc, quand vous refactorisez, vous n'ajoutez pas de nouvelles fonctionnalités.": Mais je pourrais introduire des bogues dans un logiciel testé.
Giorgio
"Parfois, plusieurs fois, après avoir ajouté une deuxième et une troisième et une nième fonctionnalités, vous réalisez que votre conception initiale, même si elle respecte l'Open-Close, ne respecte pas d'autres principes ou exigences logicielles.": C'est à ce moment que je voudrais commencer à écrire une nouvelle implémentation Bet, lorsque celle-ci est prête, remplacer l'ancienne implémentation Apar la nouvelle implémentation B(c'est une utilisation des interfaces). ALe code de '' peut servir de base au Bcode de '' et ensuite je peux utiliser le refactoring sur le Bcode pendant son développement, mais je pense que le Acode déjà testé devrait rester figé.
Giorgio
@Giorgio Lorsque vous refactorisez vous pouvez introduire des bugs, c'est pourquoi vous écrivez des tests (ou encore mieux faites TDD). La façon la plus sûre de refactoriser est de changer le code lorsque vous savez que cela fonctionne. Vous le savez en ayant un ensemble de tests qui réussissent. Après avoir modifié votre code de production, les tests doivent encore réussir, vous savez donc que vous n'avez pas introduit de bogue. Et rappelez-vous que les tests sont aussi importants que le code de production, vous leur appliquez donc la même règle qu'au code de production, les gardez propres et les refactorisez périodiquement et fréquemment.
Patkos Csaba
@Giorgio Si le code Best construit sur du code Aen tant qu'évolution de A, alors, lorsqu'il Best publié, il Adoit être supprimé et ne plus jamais être utilisé. Les clients qui utilisaient auparavant utiliseront Asimplement Bsans connaître le changement, car l'interface In'a pas été modifiée (peut-être un peu du principe de substitution de Liskov ici? ... le L de SOLID)
Patkos Csaba
Oui, c'est ce que j'avais à l'esprit: ne jetez pas le code de travail avant d'avoir un remplacement valide (bien testé).
Giorgio
-1

Selon ma compréhension - si vous ajoutez de nouvelles méthodes à la classe existante, cela ne cassera pas l'OCP. cependant je suis un peu confus avec l'ajout de nouvelles variables dans la classe. Mais si vous modifiez la méthode et les paramètres existants dans la méthode existante, cela cassera sûrement l'OCP, car le code est déjà testé et passé si nous changeons intentionnellement la méthode [lorsque les exigences changent], ce sera un problème.

Narender Parmar
la source