Pourquoi les cours ne devraient-ils pas être conçus pour être «ouverts»?

44

Lors de la lecture de diverses questions Stack Overflow et du code d'autres personnes, le consensus général sur la manière de concevoir des classes est fermé. Cela signifie que, par défaut, en Java et en C #, tout est privé, les champs sont finaux, certaines méthodes sont finales et parfois les classes sont même finales .

L'idée derrière ceci est de cacher les détails de l'implémentation, ce qui est une très bonne raison. Cependant, avec la protectedplupart des langages POO et du polymorphisme, cela ne fonctionne pas.

Chaque fois que je souhaite ajouter ou modifier des fonctionnalités à une classe, des obstacles privés et définitifs sont placés partout. Ici, les détails de l'implémentation importent: vous prenez l'implémentation et l'étendez, sachant parfaitement quelles en sont les conséquences. Cependant, étant donné que je ne peux pas accéder aux champs et méthodes privés et finaux, j'ai trois options:

  • Ne prolongez pas la classe, contournez simplement le problème conduisant à un code plus complexe
  • Copier et coller toute la classe, éliminant ainsi la réutilisation du code
  • Fork le projet

Ce ne sont pas de bonnes options. Pourquoi n'est-il pas protectedutilisé dans les projets écrits dans les langues qui le supportent? Pourquoi certains projets interdisent-ils explicitement l'héritage de leurs classes?

TheLQ
la source
1
Je suis d’accord, j’ai eu ce problème avec les composants Java Swing, c’est vraiment grave.
Jonas
3
Il y a de fortes chances que si vous rencontrez ce problème, les classes aient été mal conçues au départ - ou peut-être essayez-vous de les utiliser incorrectement. Vous ne demandez pas d'informations à une classe, vous lui demandez de faire quelque chose pour vous - vous ne devriez donc généralement pas avoir besoin de ses données. Bien que cela ne soit pas toujours vrai, si vous vous rendez compte que vous devez accéder à des données de classe, il y a de fortes chances que quelque chose ne tourne pas rond.
Bill K
2
"la plupart des langues POO?" Je sais beaucoup plus où les classes ne peuvent pas être fermées. Vous avez omis la quatrième option: changer de langue.
kevin cline
@Jonas L'un des problèmes majeurs de Swing est qu'il expose beaucoup trop de mises en œuvre. Cela tue vraiment le développement.
Tom Hawtin - tackline le

Réponses:

55

Concevoir des classes pour qu'elles fonctionnent correctement lorsqu'elles sont étendues, en particulier lorsque le programmeur chargé de l'extension ne comprend pas parfaitement le fonctionnement supposé de la classe, nécessite un effort supplémentaire considérable. Vous ne pouvez pas simplement prendre tout ce qui est privé et le rendre public (ou protégé) et appeler cela "ouvert". Si vous autorisez quelqu'un d'autre à modifier la valeur d'une variable, vous devez prendre en compte l'impact de toutes les valeurs possibles sur la classe. (Que se passe-t-il si la variable est définie sur null? Un tableau vide? Un nombre négatif?) Il en va de même lorsque vous autorisez d'autres personnes à appeler une méthode. Il faut bien réfléchir.

Ce n’est donc pas tellement que les classes ne devraient pas être ouvertes, mais que parfois, cela ne vaut pas la peine de les faire ouvrir.

Bien sûr, il est également possible que les auteurs de la bibliothèque soient simplement paresseux. Cela dépend de la bibliothèque dont vous parlez. :-)

Aaron
la source
13
Oui, c'est pourquoi les classes sont scellées par défaut. Comme vous ne pouvez pas prédire les nombreuses façons dont vos clients peuvent étendre la classe, il est plus prudent de la sceller que de s’engager à prendre en charge le nombre potentiellement illimité de façons dont la classe peut être étendue.
Robert Harvey
18
Vous n'avez pas à prédire comment votre classe sera annulée, vous devez simplement supposer que les personnes qui étendent votre classe savent ce qu'elles font. S'ils étendent la classe et définissent quelque chose sur null alors qu'elle ne devrait pas l'être, alors c'est leur faute, pas la vôtre. Cet argument n'a de sens que dans les applications super critiques où quelque chose ira terriblement mal si un champ est nul.
TheLQ
5
@TheLQ: C'est une très grosse hypothèse.
Robert Harvey
9
@TheLQ À qui la faute est une question hautement subjective. S'ils définissent une variable sur une valeur apparemment raisonnable, et que vous n'avez pas documenté le fait que cela n'était pas autorisé, et que votre code n'a pas généré d'argument ArgumentException (ou équivalent), mais votre serveur s'est déconnecté ou conduit à un exploit de sécurité, alors je dirais que c'est autant votre faute que la leur. Et si vous avez documenté les valeurs valides et mis en place une vérification des arguments, vous avez présenté exactement le type d’effort dont je parle, et vous devez vous demander si cet effort a vraiment été une bonne utilisation de votre temps.
Aaron
24

Rendre tout ce qui est privé par défaut a l'air dur, mais regardez de l'autre côté: quand tout est privé par défaut, rendre quelque chose public (ou protégé, ce qui est presque la même chose) est supposé être un choix conscient; c'est le contrat de l'auteur de la classe avec vous, le consommateur, sur la manière d'utiliser la classe. C'est une commodité pour vous deux: l'auteur est libre de modifier le fonctionnement interne de la classe, tant que l'interface reste inchangée; et vous savez exactement sur quelles parties de la classe vous pouvez compter et lesquelles sont susceptibles de changer.

L'idée sous-jacente est le «couplage faible» (également appelé «interfaces étroites»); sa valeur réside dans la maîtrise de la complexité. En réduisant le nombre de façons dont les composants peuvent interagir, la quantité de dépendance réciproque entre eux est également réduite; et la dépendance croisée est l’un des pires types de complexité en matière de maintenance et de gestion du changement.

Dans les bibliothèques bien conçues, les classes qui méritent d’être étendues par héritage ont protégé les membres publics aux bons endroits et cachent tout le reste.

tdammers
la source
Cela a du sens. Il y a toujours le problème quand vous avez besoin de quelque chose de très similaire mais avec 1 ou 2 choses qui ont changé dans l'implémentation (le fonctionnement interne de la classe). J'ai également du mal à comprendre que si vous annulez la classe, ne comptez-vous pas déjà beaucoup sur sa mise en œuvre?
TheLQ
5
+1 pour les avantages d'un couplage lâche. @TheLQ, c'est l' interface sur laquelle vous devriez être fortement tributaire, pas l'implémentation.
Karl Bielefeldt
19

Tout ce qui n'est pas privé est plus ou moins supposé exister avec un comportement inchangé dans toutes les versions futures de la classe. En fait, cela peut être considéré comme faisant partie de l'API, documenté ou non. Par conséquent, exposer trop de détails pose probablement des problèmes de compatibilité ultérieurement.

En ce qui concerne "final" resp. classes "scellées", un cas typique sont des classes immuables. Beaucoup de parties du framework dépendent de la nature immuable des chaînes. Si la classe String n'était pas finale, il serait facile (voire tentant) de créer une sous-classe String modifiable, qui cause toutes sortes de bugs dans de nombreuses autres classes lorsqu'elle est utilisée à la place de la classe String immuable.

utilisateur281377
la source
4
C'est la vraie réponse. D'autres réponses touchent des choses importantes, mais le vrai point pertinent est la suivante: tout ce qui n'est pas finalet / ou privateest verrouillé en place et ne peut jamais être changé .
Konrad Rudolph le
1
@ Konrad: Jusqu'à ce que les développeurs décident de tout réorganiser et de ne pas être compatibles avec les versions antérieures.
JAB
C'est vrai, mais il convient de noter que c'est une exigence beaucoup plus faible que celle imposée aux publicmembres; protectedles membres ne forment un contrat qu'entre la classe qui les expose et ses classes dérivées immédiates; en revanche, les publicmembres signent des contrats avec tous les consommateurs pour le compte de toutes les classes héritées possibles, présentes et futures.
Supercat
14

Dans OO, il existe deux manières d'ajouter des fonctionnalités au code existant.

La première est par héritage: vous prenez une classe et en dérivez. Cependant, l'héritage doit être utilisé avec précaution. Vous devez utiliser l'héritage public principalement lorsque vous avez une relation isA entre la base et la classe dérivée (par exemple, un rectangle est une forme). Au lieu de cela, vous devez éviter l'héritage public pour la réutilisation d'une implémentation existante. En principe , l'héritage public est utilisé pour s'assurer que la classe dérivée a la même interface que la classe de base (ou une classe plus grande), afin que vous puissiez appliquer le principe de substitution de Liskov .

L'autre méthode pour ajouter des fonctionnalités consiste à utiliser la délégation ou la composition. Vous écrivez votre propre classe qui utilise la classe existante en tant qu'objet interne et vous lui déléguez une partie du travail d'implémentation.

Je ne sais pas quelle était l'idée derrière toutes ces finales de la bibliothèque que vous essayez d'utiliser. Les programmeurs voulaient probablement s'assurer que vous n'allez pas hériter d'une nouvelle classe, car ce n'est pas la meilleure façon d'étendre leur code.

knulp
la source
5
Excellent point avec la composition vs l'héritage.
Zsolt Török le
+1, mais je n'ai jamais aimé l'exemple "est une forme" pour l'héritage. Un rectangle et un cercle peuvent être deux choses très différentes. J'aime plus utiliser, un carré est un rectangle qui est contraint. Les rectangles peuvent être utilisés comme des formes.
Tylermac
@tylermac: en effet. L’affaire Square / Rectangle est en réalité l’un des contre-exemples du LSP. Je devrais probablement commencer à penser à un exemple plus efficace.
knulp
3
Square / Rectangle n'est qu'un contre-exemple si vous autorisez la modification.
starblue
11

La plupart des réponses ont raison: les classes doivent être scellées (final, NotOverridable, etc.) lorsque l'objet n'est pas conçu ni destiné à être étendu.

Cependant, je n’envisagerais pas simplement de fermer tout le code de conception approprié. Le "O" dans SOLID est pour "Principe d'ouverture-fermeture", indiquant qu'une classe doit être "fermée" à la modification, mais "ouverte" à l'extension. L'idée est que vous avez du code dans un objet. Cela fonctionne très bien. L'ajout de fonctionnalités ne doit pas obliger à ouvrir ce code et à effectuer des modifications chirurgicales susceptibles de rompre un comportement antérieur au travail. Au lieu de cela, on devrait pouvoir utiliser l'injection d'héritage ou de dépendance pour "étendre" la classe afin qu'elle fasse des choses supplémentaires.

Par exemple, une classe "ConsoleWriter" peut obtenir du texte et l'afficher sur la console. Il fait ça bien. Cependant, dans certains cas, vous avez également besoin de la même sortie écrite dans un fichier. Ouvrir le code de ConsoleWriter, changer son interface externe pour ajouter un paramètre "WriteToFile" aux fonctions principales, et placer le code d'écriture de fichier supplémentaire à côté du code d'écriture de console serait généralement considéré comme une mauvaise chose.

Au lieu de cela, vous pouvez effectuer l'une des opérations suivantes: vous pouvez dériver de ConsoleWriter pour former ConsoleAndFileWriter et étendre la méthode Write () pour appeler d'abord l'implémentation de base, puis écrire dans un fichier. Vous pouvez également extraire une interface IWriter de ConsoleWriter et la réimplémenter pour créer deux nouvelles classes. un FileWriter et un "MultiWriter" auxquels d'autres IWriters peuvent être donnés et qui "diffuseront" tout appel de ses méthodes à tous les rédacteurs donnés. Le choix que vous choisirez dépendra de ce dont vous aurez besoin. La dérivation simple est, bien, plus simple, mais si vous avez l’impression d’avoir éventuellement besoin d’envoyer le message à un client réseau, trois fichiers, deux canaux nommés et la console, continuez et extrayez l’interface et créez le "Adaptateur en Y"; il'

Maintenant, si la fonction Write () n'a jamais été déclarée virtuelle (ou si elle était scellée), vous avez maintenant des problèmes si vous ne contrôlez pas le code source. Parfois même si vous le faites. Il s’agit généralement d’une mauvaise position, et les utilisateurs d’API à source fermée ou à source limitée sont sans cesse frustrés. Mais, il existe une raison légitime pour laquelle Write (), ou toute la classe ConsoleWriter, est scellé; il peut y avoir des informations sensibles dans un champ protégé (substituées à leur tour par une classe de base pour fournir lesdites informations). La validation peut être insuffisante; lorsque vous écrivez une API, vous devez supposer que les programmeurs qui utilisent votre code ne sont ni plus intelligents ni bienveillants que "l'utilisateur final" moyen du système final; De cette façon, vous ne serez jamais déçu.

KeithS
la source
Bon point. L'héritage peut être bon ou mauvais, il est donc faux de dire que chaque classe devrait être scellée. Cela dépend vraiment du problème à résoudre.
knulp
2

Je pense que cela vient probablement de toutes les personnes utilisant un langage oo pour la programmation en c. Je te regarde, java. Si votre marteau a la forme d'un objet, mais que vous souhaitez un système procédural et / ou ne comprend pas vraiment les classes, votre projet devra alors être verrouillé.

Fondamentalement, si vous allez écrire dans une langue oo, votre projet doit suivre le paradigme oo, qui inclut l'extension. Bien sûr, si quelqu'un a écrit une bibliothèque dans un paradigme différent, peut-être que vous n'êtes pas censé l'étendre de toute façon.

Spencer Rathbun
la source
7
BTW, OO et procédural ne s’excluent absolument pas mutuellement. Vous ne pouvez pas avoir OO sans procédures. En outre, la composition est autant un concept OO qu'une extension.
Michael K
@ Michael bien sûr que non. J'ai déclaré que vous souhaitiez peut-être un système procédural , c'est-à-dire sans concept OO. J'ai vu des gens utiliser Java pour écrire du code purement procédural, et cela ne fonctionne pas si vous essayez d'interagir avec cela de manière OO.
Spencer Rathbun le
1
Cela pourrait être résolu si Java disposait de fonctions de première classe. Meh
Michael K
Il est difficile d’enseigner les langages Java ou DOTNet à de nouveaux étudiants. J'ai l'habitude d'enseigner procédural avec Pascal procédural ou "Plain C", puis de passer à OO avec Object Pascal ou "C ++". Et, quittez Java pour plus tard. Enseigner la programmation avec un programme procédura ressemble à un seul objet (singleton) fonctionne bien.
umlcat
@Michael K: Dans la programmation orientée objet, une fonction de première classe est simplement un objet avec exactement une méthode.
Giorgio
2

Parce qu'ils ne savent pas mieux.

Les auteurs originaux s’attachent probablement à une incompréhension des principes SOLID, qui trouve son origine dans un monde C ++ confus et compliqué.

J'espère que vous remarquerez que les mondes ruby, python et perl n'ont pas les problèmes que les réponses prétendent être la raison de la fermeture. Notez que c'est orthogonal au typage dynamique. Les modificateurs d'accès sont faciles à utiliser dans la plupart des langues (toutes?). Les champs C ++ peuvent être mélangés avec un autre type (le C ++ est plus faible). Java et C # peuvent utiliser la réflexion. Les modificateurs d'accès rendent les choses suffisamment difficiles pour vous empêcher de le faire à moins que vous ne le souhaitiez VRAIMENT.

Le fait de sceller des classes et de marquer tout membre privé enfreint explicitement le principe selon lequel les choses simples doivent être simples et les choses difficiles doivent être possibles. Soudain, les choses qui devraient être simples ne le sont pas.

Je vous encourage à essayer de comprendre le point de vue des auteurs originaux. Cela provient en grande partie d'une idée académique d'encapsulation qui n'a jamais démontré un succès absolu dans le monde réel. Je n'ai jamais vu un framework ou une bibliothèque où un développeur, quelque part, ne souhaitait pas fonctionner de manière légèrement différente et n'avait aucune raison de le modifier. Il y a deux possibilités qui ont pu nuire aux développeurs de logiciels originaux qui ont scellé et rendu leurs membres privés.

  1. Arrogance - ils croyaient vraiment qu'ils étaient ouverts à la prolongation et fermés à la modification
  2. Complaisance - ils savaient qu'il pourrait y avoir d'autres cas d'utilisation mais ont décidé de ne pas écrire pour ces cas d'utilisation

Je pense que dans le cadre institutionnel, le n ° 2 est probablement le cas. Ces frameworks C ++, Java et .NET doivent être "terminés" et doivent respecter certaines directives. Ces instructions désignent généralement les types scellés, à moins que le type n'ait été explicitement conçu comme faisant partie d'une hiérarchie de types et de membres privés pour de nombreux éléments pouvant être utiles à d'autres utilisateurs .. mais comme ils ne sont pas directement liés à cette classe, ils ne sont pas exposés. . Extraire un nouveau type serait trop coûteux à prendre en charge, documenter, etc.

L'idée derrière les modificateurs d'accès est que les programmeurs devraient être protégés d'eux-mêmes. "La programmation en C est mauvaise car elle vous permet de vous tirer une balle dans le pied." Ce n'est pas une philosophie avec laquelle je suis d'accord en tant que programmeur.

Je préfère de loin l'approche du nom python. Vous pouvez facilement (beaucoup plus facile que la réflexion) remplacer des soldats si vous en avez besoin. Un bon article est disponible ici: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Le modificateur privé de Ruby ressemble en réalité davantage à une protection en C # et n'a pas de modificateur privé en tant que C #. Protégé est un peu différent. Il y a une grande explication ici: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

N'oubliez pas que votre langage statique ne doit pas nécessairement se conformer aux styles désuets du code écrit dans ce langage précédent.

jrwren
la source
5
"l'encapsulation n'a jamais démontré le succès absolu". J'aurais pensé que tout le monde conviendrait que le manque d'encapsulation a causé beaucoup de problèmes.
Joh
5
-1. Les imbéciles se précipitent là où les anges ont peur de marcher. Célébrer la possibilité de modifier des variables privées révèle une grave lacune dans votre style de codage.
Riwalk
2
En plus d'être discutable et probablement erronée, cette publication est également assez arrogante et insultante. Bien fait.
Konrad Rudolph le
1
Il existe deux écoles de pensée dont les partisans ne semblent pas bien s'entendre. Les dactylographes statiques veulent qu'il soit facile de raisonner sur le logiciel, les gens dynamiques veulent qu'il soit facile d'ajouter des fonctionnalités. Laquelle est la meilleure dépend essentiellement de la durée de vie prévue du projet.
Joh
1

EDIT: Je crois que les cours devraient être conçus pour être ouverts. Avec certaines restrictions, mais ne devrait pas être fermé pour héritage.

Pourquoi: "classes finales" ou "classes scellées" me semble étrange. Je n'ai jamais à marquer une de mes propres classes comme "finale" ("scellée"), car je pourrais devoir hériter de ces classes, plus tard.

J'ai acheté / téléchargé des bibliothèques tierces avec des classes (la plupart des contrôles visuels), et je suis vraiment reconnaissant qu'aucune de ces bibliothèques n'utilise "final", même si elle est supportée par le langage de programmation dans lequel elles ont été codées, car j'ai fini d'étendre ces classes, même si j'ai compilé des bibliothèques sans le code source.

Parfois, je dois traiter avec certaines classes "scellées" occasionnelles, et j'ai fini par créer une nouvelle classe "wrapper" contenant la classe donnée, qui a des membres similaires.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Les classificateurs Scope permettent de "sceller" certaines fonctionnalités sans restreindre l'héritage.

Quand j'ai changé de programme structuré. À la programmation orientée objet, j'ai commencé à utiliser des membres de classe privés , mais après de nombreux projets, j'ai fini par utiliser protected et, éventuellement, je fais la promotion de ces propriétés ou méthodes au public , si nécessaire.

PS Je n'aime pas le mot-clé "final" en C #, mais plutôt "scellé" comme Java ou "stérile", selon la métaphore de l'héritage. En outre, "final" est un mot ambigu utilisé dans plusieurs contextes.

Umlcat
la source
-1: Vous avez dit que vous aimez les conceptions ouvertes, mais cela ne répond pas à la question, qui était pourquoi les classes ne devraient pas être conçues pour être ouvertes.
Joh
@ Joh Désolé, je ne me suis pas bien expliqué, j'ai essayé d'exprimer l'opinion contraire, il ne devrait pas être scellé. Merci de décrire pourquoi vous n'êtes pas d'accord.
umlcat