web-dev-qa-db-fra.com

Pourquoi de nombreux développeurs de logiciels violent-ils le principe ouvert / fermé?

Pourquoi de nombreux développeurs de logiciels violent-ils le principe ouvert/fermé en modifiant beaucoup de choses comme renommer des fonctions qui vont casser l'application après la mise à niveau?

Cette question me vient à l'esprit après les versions rapide et continue de la bibliothèque React .

Chaque courte période, je remarque de nombreux changements dans la syntaxe, les noms des composants, etc.

Exemple dans la prochaine version de React :

Nouveaux avertissements de dépréciation

Le plus grand changement est que nous avons extrait React.PropTypes et React.createClass dans leurs propres packages. Les deux sont toujours accessibles via l'objet principal React, mais l'utilisation de l'un ou l'autre enregistrera un avertissement de dépréciation unique sur la console en mode développement. Cela permettra de futures optimisations de la taille du code.

Ces avertissements n'affecteront pas le comportement de votre application. Cependant, nous réalisons qu'ils peuvent provoquer une certaine frustration, en particulier si vous utilisez un framework de test qui traite console.error comme un échec.


  • Ces changements sont-ils considérés comme une violation de ce principe?
  • En tant que débutant à quelque chose comme React , comment puis-je l'apprendre avec ces changements rapides dans la bibliothèque (c'est tellement frustrant)?
77
Anyname Donotcare

La réponse de IMHO JacquesB, bien que contenant beaucoup de vérité, montre une incompréhension fondamentale de l'OCP. Pour être honnête, votre question exprime déjà ce malentendu aussi - renommer les fonctions rompt compatibilité descendante, mais pas l'OCP. Si casser la compatibilité semble nécessaire (ou maintenir deux versions du même composant pour ne pas casser la compatibilité), l'OCP était déjà cassé avant!

Comme Jörg W Mittag l'a déjà mentionné dans ses commentaires, le principe ne dit pas "vous ne pouvez pas modifier le comportement d'un composant" - il dit, il faut essayer pour concevoir les composants de manière ouverte pour être réutilisé (ou étendu) de plusieurs manières, sans besoin pour modification. Cela peut être fait en fournissant les bons "points d'extension" ou, comme mentionné par @AntP, "en décomposant une structure de classe/fonction au point où chaque point d'extension naturel est là par défaut." IMHO suivant l'OCP n'a rien de commun avec "garder l'ancienne version inchangée pour une compatibilité ascendante" ! Ou, en citant le commentaire de @ DerekElkin ci-dessous:

L'OCP est un conseil sur la façon d'écrire un module, [...] pas sur la mise en œuvre d'un processus de gestion du changement qui ne permet jamais aux modules de changer.

Les bons programmeurs utilisent leur expérience pour concevoir des composants avec les "bons" points d'extension à l'esprit (ou - mieux encore - d'une manière aucun point d'extension artificiel n'est nécessaire). Cependant, pour le faire correctement et sans ingénierie inutile, vous devez savoir à l'avance à quoi pourraient ressembler les futurs cas d'utilisation de votre composant. Même les programmeurs expérimentés ne peuvent pas regarder vers l'avenir et connaître à l'avance toutes les exigences à venir. Et c'est pourquoi parfois compatibilité descendante doit être violé - peu importe le nombre de points d'extension de votre composant, ou la façon dont il suit l'OCP en ce qui concerne certains types d'exigences, il y aura toujours une exigence qui ne peut pas être implémenté facilement sans modifier le composant.

147
Doc Brown

Le principe ouvert/fermé présente des avantages, mais il présente également de sérieux inconvénients.

En théorie le principe résout le problème de la compatibilité descendante en créant du code qui est "ouvert pour l'extension mais fermé pour la modification". Si une classe a de nouvelles exigences, vous ne modifiez jamais le code source de la classe elle-même mais créez plutôt une sous-classe qui remplace uniquement les membres appropriés nécessaires pour changer le comportement. Tout le code écrit par rapport à la version originale de la classe n'est donc pas affecté, vous pouvez donc être sûr que votre modification n'a pas cassé le code existant.

En réalité vous vous retrouvez facilement avec une surcharge de code et un désordre déroutant de classes obsolètes. S'il n'est pas possible de modifier certains comportements d'un composant via l'extension, vous devez alors fournir une nouvelle variante du composant avec le comportement souhaité et conserver l'ancienne version inchangée pour une compatibilité ascendante.

Supposons que vous découvriez une faille de conception fondamentale dans une classe de base dont de nombreuses classes héritent. Supposons que l'erreur est due à un champ privé de type incorrect. Vous ne pouvez pas résoudre ce problème en remplaçant un membre. Fondamentalement, vous devez remplacer la classe entière, ce qui signifie que vous finissez par étendre Object pour fournir une classe de base alternative - et maintenant vous devez également fournir des alternatives à toutes les sous-classes, vous retrouvant ainsi avec une hiérarchie d'objets dupliquée, une hiérarchie défectueuse, une améliorée. Mais vous ne pouvez pas supprimer la hiérarchie défectueuse (car la suppression de code est une modification), tous les futurs clients seront exposés aux deux hiérarchies.

Maintenant, la réponse théorique à ce problème est "il suffit de le concevoir correctement la première fois". Si le code est parfaitement décomposé, sans défauts ni erreurs, et conçu avec des points d'extension préparés pour toutes les futures modifications possibles des exigences, alors vous évitez le désordre. Mais en réalité, tout le monde fait des erreurs, et personne ne peut prédire parfaitement l'avenir.

Prenons quelque chose comme le framework .NET - il transporte toujours l'ensemble des classes de collection qui ont été conçues avant l'introduction des génériques il y a plus de dix ans. C'est certainement une aubaine pour la compatibilité descendante (vous pouvez mettre à niveau le framework sans avoir à réécrire quoi que ce soit), mais cela gonfle également le framework et présente aux développeurs un large éventail d'options où beaucoup sont tout simplement obsolètes.

Apparemment, les développeurs de React ont estimé que cela ne valait pas le coût en complexité et en fardeau de code de suivre strictement le principe ouvert/fermé.

L'alternative pragmatique à l'ouverture/la fermeture est la dépréciation contrôlée. Plutôt que de briser la compatibilité descendante dans une seule version, les anciens composants sont conservés pendant un cycle de version, mais les clients sont informés via des avertissements du compilateur que l'ancienne approche sera supprimée dans une version ultérieure. Cela donne aux clients le temps de modifier le code. Cela semble être l'approche de React dans ce cas.

(Mon interprétation du principe est basée sur Le principe ouvert-fermé par Robert C. Martin)

68
JacquesB

J'appellerais le principe ouvert/fermé un idéal. Comme tous les idéaux, il accorde peu d'attention aux réalités du développement logiciel. De même que tous les idéaux, il est impossible de l'atteindre dans la pratique - on s'efforce simplement d'approcher cet idéal du mieux qu'on peut.

L'autre côté de l'histoire est connu sous le nom de menottes d'or. Les menottes dorées sont ce que vous obtenez lorsque vous vous asservissez trop au principe ouvert/fermé. Les menottes dorées sont ce qui se produit lorsque votre produit qui ne casse jamais la compatibilité descendante ne peut pas croître car trop d'erreurs passées ont été commises.

Un exemple célèbre de cela se trouve dans le gestionnaire de mémoire de Windows 95. Dans le cadre de la commercialisation de Windows 95, il a été déclaré que toutes les applications Windows 3.1 fonctionneraient dans Windows 95. Microsoft a en fait acquis des licences pour des milliers de programmes pour les tester dans Windows 95. L'un des problèmes était Sim City. Sim City avait en fait un bug qui l'a fait écrire dans la mémoire non allouée. Dans Windows 3.1, sans un "bon" gestionnaire de mémoire, il s'agissait d'un faux pas mineur. Toutefois, dans Windows 95, le gestionnaire de mémoire intercepterait cela et provoquerait une erreur de segmentation. La solution? Sous Windows 95, si le nom de votre application est simcity.exe, le système d'exploitation va en fait assouplir les contraintes du gestionnaire de mémoire pour éviter le défaut de segmentation!

Le véritable enjeu de cet idéal réside dans les concepts épurés des produits et services. Personne ne fait vraiment l'un ou l'autre. Tout s'aligne quelque part dans la région grise entre les deux. Si vous pensez d'une approche orientée produit, ouvrir/fermer sonne comme un grand idéal. Vos produits sont fiables. Cependant, en ce qui concerne les services, l'histoire change. Il est facile de montrer qu'avec le principe ouvert/fermé, la quantité de fonctionnalités que votre équipe doit prendre en charge doit approcher asymptotiquement l'infini, car vous ne pouvez jamais nettoyer les anciennes fonctionnalités. Cela signifie que votre équipe de développement doit prendre en charge de plus en plus de code chaque année. Finalement, vous atteignez un point de rupture.

La plupart des logiciels d'aujourd'hui, en particulier l'open source, suivent une version détendue commune du principe ouvert/fermé. Il est très courant de voir ouvert/fermé suivi servilement pour les versions mineures, mais abandonné pour les versions majeures. Par exemple, Python 2.7 contient de nombreux "mauvais choix" des Python 2.0 et 2.1 jours, mais Python 3.0 balayé (En outre, le passage de la base de code Windows 95 à la base de code Windows NT lors de la sortie de Windows 2000 a cassé toutes sortes de choses, mais cela l'a fait signifie que nous n'avons jamais à gérer une mémoire gestionnaire vérifiant le nom de l'application pour décider du comportement!)

20
Cort Ammon

La réponse de Doc Brown est la plus proche de la précision, les autres réponses illustrent des malentendus sur le principe ouvert et fermé.

Pour articuler explicitement le malentendu, il semble y avoir une croyance que l'OCP signifie que vous ne devriez pas faire de changements incompatibles en arrière (ou même aucun changement ou quelque chose le long ces lignes.) L'OCP consiste à concevoir des composants afin que vous n'ayez pas besoin de les modifier pour étendre leurs fonctionnalités, que ces modifications sont rétrocompatibles ou non. Outre l'ajout de fonctionnalités, il existe de nombreuses autres raisons pour lesquelles vous pouvez apporter des modifications à un composant, qu'elles soient rétrocompatibles (par exemple, refactorisation ou optimisation) ou rétrocompatibles (par exemple, dépréciation et suppression de fonctionnalités). Le fait que vous puissiez effectuer ces modifications ne signifie pas que votre composant a violé l'OCP (et ne signifie certainement pas que vous violez l'OCP).

Vraiment, ce n'est pas du tout du code source. Une déclaration plus abstraite et pertinente de l'OCP est: "un composant doit permettre l'extension sans avoir besoin de violer ses limites d'abstraction". J'irais plus loin et dirais qu'un rendu plus moderne est: "un composant devrait appliquer ses limites d'abstraction mais permettre l'extension". Même dans l'article sur l'OCP de Bob Martin alors qu'il "décrit" "fermé à la modification" comme "le code source est inviolable", il commence plus tard à parler d'encapsulation qui n'a rien à voir avec la modification du code source et tout à voir avec l'abstraction limites.

Ainsi, la prémisse erronée dans la question est que l'OCP est (conçu comme) une directive sur les évolutions d'une base de code. L'OCP est généralement slogané comme "un composant doit être ouvert aux extensions et fermé aux modifications par les consommateurs". Fondamentalement, si un consommateur d'un composant souhaite ajouter une fonctionnalité à la composant, ils devraient être en mesure d'étendre l'ancien composant dans un nouveau avec la fonctionnalité supplémentaire, mais ils ne devraient pas être en mesure de modifier l'ancien composant.

L'OCP ne dit rien sur le créateur d'un composant changeant ou suppression de la fonctionnalité . L'OCP ne préconise pas de maintenir compatibilité de bogue pour toujours. En tant que créateur, vous ne violez pas l'OCP en modifiant ou même en supprimant un composant. Vous, ou plutôt les composants que vous avez écrits, violez l'OCP si la seule façon dont les consommateurs peuvent ajouter des fonctionnalités à vos composants est de les muter, par exemple par patch de singe ou avoir accès au code source et recompiler. Dans de nombreux cas, aucune de ces options n'est pour le consommateur, ce qui signifie que si votre composant n'est pas "ouvert à l'extension", il n'a pas de chance. Ils ne peuvent tout simplement pas utiliser votre composant pour leurs besoins. L'OCP soutient de ne pas mettre les consommateurs de votre bibliothèque dans cette position, du moins en ce qui concerne une classe identifiable d '"extensions". Même lorsque des modifications peuvent être apportées au code source ou même à la copie principale du code source, il est préférable de "faire semblant" que vous ne pouvez pas modifier car il y a de nombreuses conséquences négatives potentielles à le faire.

Donc, pour répondre à vos questions: Non, ce ne sont pas des violations de l'OCP. Aucune modification apportée par un auteur ne peut constituer une violation de l'OCP car l'OCP n'est pas proportionnel aux modifications. Cependant, les modifications peuvent créer des violations de l'OCP, et elles peuvent être motivées par des défaillances de l'OCP dans les versions antérieures de la base de code. L'OCP est une propriété d'un morceau de code particulier, pas l'histoire évolutive d'une base de code.

En revanche, la compatibilité descendante est une propriété d'un changement de code. Cela n'a aucun sens de dire qu'un morceau de code est ou n'est pas rétrocompatible. Il est logique de parler de la compatibilité descendante de certains codes par rapport à certains codes plus anciens. Par conséquent, cela n'a jamais de sens de parler de la première coupure d'un code qui soit rétrocompatible ou non. La première coupe de code peut satisfaire ou ne pas satisfaire l'OCP, et en général, nous pouvons déterminer si un certain code satisfait l'OCP sans se référer à des versions historiques du code.

En ce qui concerne votre dernière question, il est sans doute hors sujet pour StackExchange en général comme étant principalement basé sur l'opinion, mais le court terme est le bienvenu dans la technologie et en particulier JavaScript où, au cours des dernières années, le phénomène que vous décrivez a été appelé Fatigue JavaScript . (N'hésitez pas à google pour trouver une variété d'autres articles, certains satiriques, en parlant de plusieurs points de vue.)

11