Pourquoi de nombreux développeurs de logiciels violent-ils le principe d'ouverture / fermeture?


74

Pourquoi de nombreux développeurs de logiciels enfreignent-ils le principe d'ouverture / fermeture en modifiant plusieurs éléments, tels que le changement de nom de fonctions, qui endommageront 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 de composants, etc.

Exemple dans la prochaine version de React :

Nouveaux avertissements de dépréciation

Le plus gros 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 leur utilisation consignera un avertissement de dépréciation unique sur la console en mode de développement. Cela permettra d’optimiser la taille du code à l’avenir.

Ces avertissements n'affecteront pas le comportement de votre application. Cependant, nous sommes conscients qu’ils peuvent être frustrants, 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)?

6
Ceci est clairement un exemple de l' observation , et votre affirmation "tellement" n'est pas fondée. Les projets Lucene et RichFaces sont des exemples notoires, de même que l’API du port Windows COMM, mais je ne peux en penser à aucun autre. Et React est-il vraiment un "gros développeur de logiciels"?
user207421

62
Comme tout principe, l'OCP a sa valeur. Mais cela nécessite que les développeurs aient une clairvoyance infinie. Dans le monde réel, les gens se trompent souvent de conception. À mesure que le temps passe, certains préfèrent se servir de leurs anciennes erreurs pour des raisons de compatibilité, d'autres préfèrent éventuellement les nettoyer afin de disposer d'un code compact et sans fardeau.
Theodoros Chatzigiannakis

1
À quand remonte la dernière fois que vous avez vu un langage orienté objet "comme prévu à l'origine"? Le principe de base était un système de messagerie qui voulait dire que chaque partie du système était extensible à l'infini par quiconque. Maintenant, comparez cela à votre langage typique de type POO - combien vous permettent d'étendre une méthode existante de l'extérieur? Combien le rendent assez facile pour être utile?
Luaan

Legacy est nul. 30 ans d'expérience ont montré que vous devez complètement abandonner votre héritage et recommencer à zéro. De nos jours, tout le monde est connecté partout et à tout moment. Par conséquent, l'héritage n'a aucune pertinence aujourd'hui. l'exemple ultime était "Windows versus Mac". Microsoft essayait traditionnellement de "prendre en charge l'héritage", cela se voit de plusieurs manières. Apple a toujours juste dit "F- - - Vous" aux anciens utilisateurs. (Cela s'applique à tout, des langues aux appareils en passant par les systèmes d'exploitation.) En fait, Apple était totalement correct et MSFT était totalement faux, clair et simple.
Fattie

4
Parce qu’il n’existe exactement aucun «principe» et «modèle de conception» qui fonctionne 100% du temps dans la vie réelle.
Matti Virkkunen

Réponses:


148

La réponse de IMHO JacquesB, bien que contenant beaucoup de vérité, montre une incompréhension fondamentale de l'OCP. Pour être juste, votre question exprime déjà ce malentendu. Les fonctions renommées annulent la compatibilité ascendante , mais pas l’OCP. Si la rupture de la compatibilité vous semble nécessaire (ou le maintien de deux versions du même composant pour ne pas rompre 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" - vous devez essayer de concevoir des composants de manière à ce qu'ils soient ouverts pour être réutilisés (ou étendus) de plusieurs façons, sans nécessiter de modification. Cela peut être fait en fournissant les "points d'extension" appropriés, ou, comme mentionné par @AntP, "en décomposant une structure classe / fonction au point où chaque point d'extension naturel est présent par défaut". IMHO suite à l'OCP n'a rien de commun avec "garder l'ancienne version inchangée pour des raisons de compatibilité ascendante" ! Ou, citant le commentaire de @ DerekElkin ci-dessous:

L'OCP donne des conseils sur la manière d'écrire un module [...] et non sur la mise en œuvre d'un processus de gestion des modifications qui ne permet jamais aux modules de changer.

Les bons programmeurs utilisent leur expérience pour concevoir des composants en gardant à l'esprit les "bons" points d'extension (ou - mieux - de manière qu'aucun point d'extension artificiel ne soit nécessaire). Toutefois, pour procéder correctement et sans ingénierie inutile, vous devez au préalable savoir à quoi peuvent 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. C’est pourquoi il est parfois nécessaire de ne pas respecter la compatibilité ascendante - quel que soit le nombre de points d’extension de votre composant ou la qualité de sa conformité avec l’OCP en ce qui concerne certains types d’exigences, il y aura toujours une exigence qui ne peut pas être mise en œuvre facilement sans modification. le composant.


14
OMI, la principale raison de "violer" OCP est qu'il faut beaucoup d'efforts pour s'y conformer correctement. Eric Lippert a publié un excellent article de blog expliquant pourquoi de nombreuses classes du framework .NET semblent violer OCP.
BJ Myers

2
@BJMyers: merci pour le lien. Jon Skeet a publié un excellent article sur l'OCP, qui ressemble beaucoup à l'idée de variation protégée.
Doc Brown

8
CETTE! L'OCP dit que vous devriez écrire du code qui peut être changé sans être touché! Pourquoi? Il suffit donc de le tester, de le réviser et de le compiler une fois. Le nouveau comportement devrait provenir du nouveau code. Pas en vissant avec l'ancien code éprouvé. Qu'en est-il du refactoring? Eh bien, le refactoring est une violation flagrante d’OCP! C'est pourquoi c'est un péché d'écrire du code en pensant que vous allez simplement le refactoriser si vos hypothèses changent. Non! Mettez chaque hypothèse dans sa propre petite boîte. Quand c'est faux, ne corrige pas la boîte. Ecrire un nouveau. Pourquoi? Parce que vous pourriez avoir besoin de retourner à l'ancien. Quand tu le feras, ça serait bien si ça fonctionnait toujours.
candied_orange

7
@CandiedOrange: merci pour votre commentaire. Je ne vois pas le refactoring et OCP aussi contraires que vous le décrivez. L'écriture de composants qui suivent l'OCP nécessite souvent plusieurs cycles de refactoring. L’objectif doit être un élément qui n’a pas besoin de modifications pour résoudre toute une "famille" d’exigences. Néanmoins, il ne faut pas ajouter de points d'extension arbitraires à un composant "au cas où", ce qui conduit trop facilement à une ingénierie excessive. Compter sur la possibilité de refactoriser peut être la meilleure alternative à cela dans de nombreux cas.
Doc Brown

4
Cette réponse crie bien les erreurs de la première réponse (actuellement) - Je pense que l’essentiel pour réussir avec open / closed est d’ arrêter de penser en termes de "points d’extension" et de penser à décomposer votre structure de classe / fonction au point où chaque point d’extension naturel est présent par défaut. La programmation "dehors dans" est un très bon moyen d'y parvenir. Chaque scénario de votre méthode / fonction actuelle est alors transféré vers une interface externe, ce qui constitue un point de rallonge naturel pour les décorateurs, les adaptateurs, etc.
Ant P

67

Le principe d'ouverture / fermeture présente des avantages, mais il présente également de graves inconvénients.

En théorie, le principe résout le problème de la compatibilité ascendante en créant un code "ouvert à l'extension mais fermé à 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 une sous-classe qui remplace uniquement les membres appropriés nécessaires pour modifier le comportement. Tout le code écrit avec la version d'origine de la classe n'est donc pas affecté, vous pouvez donc être sûr que votre modification n'a pas altéré le code existant.

En réalité, vous vous retrouvez facilement avec du code gonflé et un désordre déroutant de classes obsolètes. S'il n'est pas possible de modifier certains comportements d'un composant par le biais d'une extension, vous devez fournir une nouvelle variante du composant avec le comportement souhaité et conserver l'ancienne version inchangée pour des raisons de compatibilité avec les versions antérieures.

Supposons que vous découvriez un défaut de conception fondamental dans une classe de base dont beaucoup de classes héritent. Supposons que l'erreur est due à un type de champ privé 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 s’étendre Objectpour fournir une classe de base alternative. Maintenant, vous devez également fournir des alternatives à toutes les sous-classes, aboutissant ainsi à une hiérarchie d’objets dupliquée, une hiérarchie défectueuse, une version améliorée. . Mais vous ne pouvez pas supprimer la hiérarchie défectueuse (puisque 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 "juste le concevoir correctement la première fois". Si le code est parfaitement décomposé, sans aucune faille ni erreur, et conçu avec des points d'extension préparés pour toutes les modifications éventuelles des exigences, vous évitez le désordre. Mais en réalité, tout le monde fait des erreurs et personne ne peut prédire l'avenir parfaitement.

Prenez quelque chose comme le framework .NET: il contient 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 certes un avantage en termes de compatibilité ascendante (vous pouvez mettre à niveau le framework sans rien réécrire), mais elle le gonfle également et offre aux développeurs un large éventail d’options dont beaucoup sont simplement obsolètes.

Apparemment, les développeurs de React ont estimé que le coût et la complexité du code coûtaient à eux seuls de ne pas suivre strictement le principe d'ouverture / fermeture.

L'alternative pragmatique à l'ouverture / la fermeture est une dépréciation contrôlée. Plutôt que de supprimer la compatibilité ascendante dans une seule version, les anciens composants sont conservés pendant un cycle de publication, 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 d' ouverture / fermeture de Robert C. Martin)


37
"Le principe dit en principe que vous ne pouvez pas modifier le comportement d'un composant. Au lieu de cela, vous devez fournir une nouvelle variante du composant avec le comportement souhaité et conserver l'ancienne version inchangée pour des raisons de compatibilité ascendante." - Je ne suis pas d'accord avec ça. Le principe dit que vous devez concevoir les composants de manière à ce qu'il ne soit pas nécessaire de changer de comportement, car vous pouvez les étendre pour faire ce que vous voulez. Le problème est que nous n’avons pas encore trouvé le moyen de le faire, en particulier avec les langues largement utilisées. Le problème d'expression est une partie de…
Jörg W Mittag

8
… Ça, par exemple. Ni Java ni C♯ n'ont de solution pour l'expression. Haskell et Scala font, mais leur base d'utilisateur est beaucoup plus petite.
Jörg W Mittag

1
@Giorgio: Dans Haskell, les solutions sont des classes de types. En Scala, la solution est implicite et objets. Désolé, je n'ai pas les liens sous la main, actuellement. Oui, les méthodes multiples (en fait, elles n'ont même pas besoin d'être "multi", c'est plutôt la nature "ouverte" des méthodes de Lisp qui sont requises) sont aussi une solution possible. Notez qu'il existe plusieurs formulations du problème d'expression, car typiquement les papiers sont écrits de telle manière que l'auteur ajoute une restriction au problème d'expression qui entraîne l'invalidité de toutes les solutions existantes, puis montre comment le sien ...
Jörg W Mittag

1
… La langue peut même résoudre cette version "plus difficile". Par exemple, Wadler avait à l'origine formulé le problème de l'expression de manière à traiter non seulement d'une extension modulaire, mais également d' une extension modulaire statiquement sûre. Les méthodes multiples Common Lisp ne sont cependant pas statiquement sûres, elles ne le sont que de manière dynamique. Odersky a ensuite renforcé ce point en affirmant qu'il devrait être modulaire, statiquement sûr, c'est-à-dire que la sécurité devrait pouvoir être vérifiée statiquement sans examiner l'ensemble du programme, mais uniquement en examinant le module d'extension. Cela ne peut en réalité pas être fait avec les classes de type Haskell, mais cela peut être fait avec Scala. Et dans le…
Jörg W Mittag Le

2
@ George: Exactement. Ce qui fait que les méthodes multiples de Common Lisp résolvent l’EP n’est en réalité pas une répartition multiple. C'est le fait que les méthodes sont ouvertes. Dans la PF typique (ou programmation procédurale), la discrimination de type est liée aux fonctions. Dans OO typique, les méthodes sont liées aux types. Les méthodes communes de Lisp sont ouvertes , elles peuvent être ajoutées aux classes après coup et dans un module différent. C'est la fonctionnalité qui les rend utilisables pour résoudre le EP. Par exemple, les protocoles de Clojure sont à répartition unique, mais résolvent également le PE (tant que vous n'insistez pas sur la sécurité statique).
Jörg W Mittag

20

J'appellerais le principe ouvert / fermé un idéal. Comme tous les idéaux, il accorde peu de considération aux réalités du développement logiciel. En outre, comme pour tous les idéaux, il est impossible de l’atteindre réellement dans la pratique - on s’efforce simplement d’approcher de cet idéal du mieux possible.

L’autre partie de l’histoire est connue sous le nom de menottes dorées. Les menottes dorées sont ce que vous obtenez lorsque vous vous esclave trop du principe d'ouverture / fermeture. Les menottes dorées sont ce qui se produit lorsque votre produit, qui ne rompt jamais avec la compatibilité en arrière, ne peut pas grandir parce que trop d'erreurs ont été commises.

Un exemple célèbre de cela se trouve dans le gestionnaire de mémoire Windows 95. Dans le cadre de la commercialisation de Windows 95, il était indiqué que toutes les applications Windows 3.1 fonctionneraient sous Windows 95. Microsoft a en fait acquis des licences pour des milliers de programmes afin de les tester sous Windows 95. L'un des problèmes était Sim City. En fait, Sim City avait un bogue qui l’écrivait dans une 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 assouplira les contraintes du gestionnaire de mémoire afin d'éviter toute erreur de segmentation!

Le véritable problème derrière cet idéal réside dans les concepts bien définis de produits et de services. Personne ne fait vraiment l'un ou l'autre. Tout se situe quelque part dans la zone grise entre les deux. Si vous envisagez une approche orientée produit, ouvrir / fermer semble être un idéal 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 l'infini de manière asymptotique, 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 actuels, en particulier ceux de source ouverte, suivent une version assouplie commune du principe ouvert / fermé. Il est très courant de voir ouvert / fermé suivre servilement pour les versions mineures, mais abandonné pour les versions majeures. Par exemple, Python 2.7 contient de nombreux "mauvais choix" des jours Python 2.0 et 2.1, mais Python 3.0 les a tous balayés. ( En outre, le passage de Windows 95 codebase pour Windows NT codebase quand ils ont sorti Windows 2000 a brisé toutes sortes de choses, mais il ne signifie que nous ne devons traiter avec un gestionnaire de contrôle de la mémoire du nom de l' application pour décider comportement!)


C'est une belle histoire sur SimCity. Avez-vous une source?
BJ Myers

5
@BJMyers C'est une vieille histoire, Joel Spoleky la mentionne vers la fin de cet article . Je l'avais initialement lu dans le cadre d'un livre sur le développement de jeux vidéo, il y a plusieurs années.
Cort Ammon

1
@BJMyers: Je suis à peu près sûr qu'ils avaient des "piratages" de compatibilité similaires pour des dizaines d'applications populaires.
Doc Brown

3
@BJMyers Il y a beaucoup de choses comme ça, si vous voulez une bonne lecture, allez sur le blog The Old New Thing de Raymond Chen , parcourez la balise Historique ou recherchez "compatibilité". On se souvient de beaucoup de contes, y compris de quelque chose de tout à fait proche de l'affaire SimCity susmentionnée - Addentum: Chen n'aime pas appeler les noms responsables.
Théraot

2
Très peu de choses se sont cassées dans la transition 95-> NT. La version originale de SimCity pour Windows fonctionne toujours parfaitement sous Windows 10 (32 bits). Même les jeux DOS fonctionnent toujours parfaitement si vous désactivez le son ou utilisez quelque chose comme VDMSound pour permettre au sous-système de la console de gérer correctement l'audio. Microsoft prend très au sérieux la compatibilité avec les versions antérieures et ne prend pas non plus de raccourcis "Mettons-le dans une machine virtuelle". Cela nécessite parfois une solution de contournement, mais c'est quand même assez impressionnant, surtout en termes relatifs.
Luaan

11

La réponse de Doc Brown est la plus exacte, les autres réponses illustrent des malentendus concernant le principe de fermeture.

Pour exprimer explicitement le malentendu, il semble y avoir une croyance que l'OCP signifie que vous ne devriez pas faire des changements en arrière incompatibles (ou même des changements ou quelque chose le long de ces lignes.) L'OCP est sur la conception de composants de sorte que vous n'avez pas besoin de apportez-leur des modifications pour étendre leurs fonctionnalités, que ces modifications soient compatibles avec les versions antérieures ou non. Outre l'ajout de fonctionnalités, vous pouvez apporter des modifications à un composant, qu'il soit rétro-compatible (par exemple, refactoring ou optimisation) ou rétro-compatible (par exemple, en dépréciant et en supprimant des fonctionnalités). Le fait que vous apportiez ces modifications ne signifie pas que votre composant a violé l’OCP (et ne signifie certainement pas que vous violent l’OCP).

En réalité, il ne s'agit pas du tout de code source. Une déclaration plus abstraite et pertinente de l'OCP est la suivante: "un composant devrait permettre une extension sans qu'il soit nécessaire de violer ses limites d'abstraction". J'irais plus loin en disant qu'une interprétation plus moderne est: "un composant doit imposer ses limites d'abstraction tout en permettant une extension". Même dans l'article de Bob Martin sur l'OCP, alors qu'il décrit "le code source comme étant inviolable", il commence ensuite à parler d'encapsulation, qui n'a rien à voir avec la modification du code source et tout ce qui concerne l'abstraction. limites.

La prémisse erronée de la question est donc que l'OCP est (censé être) un guide sur les évolutions d'une base de code. L'OCP est généralement sloganisé comme "un composant doit être ouvert aux extensions et fermé aux modifications par les consommateurs". Fondamentalement, si un consommateur d'un composant souhaite ajouter des fonctionnalités au composant, il devrait pouvoir étendre l'ancien composant à un nouveau avec les fonctionnalités supplémentaires, mais ne devrait pas pouvoir modifier l'ancien composant.

L'OCP ne dit rien sur le créateur d'un composant qui modifie ou supprime des fonctionnalités. L'OCP ne préconise pas de maintenir la compatibilité des bogues pour toujours. En tant que créateur, vous ne violez pas le panneau de commande en modifiant ou même en supprimant un composant. Vous, ou plutôt les composants que vous avez écrits, violez le panneau de commande si le seul moyen pour les utilisateurs d’ajouter des fonctionnalités à vos composants est de le muter, par exemple en appliquant des correctifs monkeyou avoir accès au code source et recompiler. Dans de nombreux cas, aucune de ces options ne constitue une option 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 recommande de ne pas placer les consommateurs de votre bibliothèque dans cette position, du moins en ce qui concerne une catégorie 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 "prétendre" que vous ne pouvez pas le modifier car cela pourrait avoir de nombreuses conséquences négatives.

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 du PCO car ce dernier n'est pas une propriété des changements. Toutefois, les modifications peuvent créer des violations du protocole OCP et peuvent être motivées par des défaillances du protocole OCP dans les versions précédentes 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é ascendante est une propriété d’un changement de code. Cela n'a aucun sens de dire qu'une partie du code est ou non compatible avec les versions antérieures. Il est logique de parler de la compatibilité ascendante de certains codes par rapport à des codes plus anciens. Par conséquent, il n’a jamais de sens de parler de la première version d’un code compatible ou non avec la version antérieure. La première copie de code peut satisfaire ou ne pas satisfaire à l'OCP, et en général, nous pouvons déterminer si un code satisfait à l'OCP sans faire référence à aucune version historique du code.

En ce qui concerne votre dernière question, on peut dire que StackExchange est généralement hors sujet en tant qu’opinion, mais les technologies, et en particulier JavaScript, sont les bienvenues. En effet, ces dernières années, le phénomène que vous décrivez a été appelé fatigue JavaScript . (N'hésitez pas à chercher sur Google plusieurs autres articles, certains satiriques, qui abordent ce sujet sous plusieurs angles.)


3
"En tant que créateur, vous ne violez pas l'OCP en modifiant ou même en supprimant un composant." - pouvez-vous fournir une référence pour cela? Aucune des définitions du principe que j'ai vu ne dit que "le créateur" (quel que soit son sens) est exempté de ce principe. Supprimer un composant publié est clairement un changement décisif.
JacquesB

1
@JacquesB Les personnes et même les modifications de code ne violent pas l'OCP, les composants (c'est-à-dire les morceaux de code) ne le font pas. (Et pour être parfaitement clair, cela signifie que le composant ne parvient pas à se conformer à l'OCP lui-même, non pas qu'il viole le OCP d'un autre composant.) Le but de ma réponse est que l'OCP ne parle pas de modifications de code. , rupture ou autre. Un composant est soit ouvert à l’extension, soit fermé à la modification, ou non, tout comme une méthode peut l’être privateou non. Si un auteur crée une privateméthode publicplus tard, cela ne signifie pas qu'il a violé le contrôle d'accès, (1/2)
Derek Elkins

2
... cela ne signifie pas non plus que la méthode n'était pas vraiment privateavant. "Supprimer un composant publié est clairement un changement décisif", est un non séquentiel. Que les composants de la nouvelle version satisfassent l'OCP ou non, vous n'avez pas besoin de l'historique de la base de code pour le déterminer. Par votre logique, je ne pourrais jamais écrire de code qui satisfasse à l'OCP. Vous associez une compatibilité ascendante, une propriété de modifications de code, à l'OCP, une propriété de code. Votre commentaire a autant de sens que de dire que quicksort n'est pas rétrocompatible. (2/2)
Derek Elkins

3
@JacquesB Tout d'abord, notez à nouveau qu'il s'agit d'un module conforme à l'OCP. L'OCP donne des conseils sur la manière d' écrire un module afin que, compte tenu de la contrainte que le code source ne puisse pas être modifié, le module puisse néanmoins être étendu. Plus tôt dans le document, il parle de la conception de modules qui ne changent jamais, pas de la mise en œuvre d'un processus de gestion du changement qui ne permet jamais aux modules de changer. En vous référant à l'édition de votre réponse, vous ne "rompez pas l'OCP" en modifiant le code du module. Au lieu de cela, si "étendre" le module vous oblige à modifier le code source, (1/3)
Derek Elkins

2
"L'OCP est une propriété d'un morceau de code particulier, pas l'historique de l'évolution d'une base de code." - excellent!
Doc Brown
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.