Pourquoi une classe devrait-elle être autre que «abstraite» ou «finale / scellée»?

29

Après plus de 10 ans de programmation java / c #, je me retrouve à créer soit:

  • classes abstraites : contrat non destiné à être instancié tel quel.
  • classes finales / scellées : l'implémentation n'est pas destinée à servir de classe de base à autre chose.

Je ne peux penser à aucune situation où une simple "classe" (c'est-à-dire ni abstraite ni finale / scellée) serait une "programmation sage".

Pourquoi une classe devrait-elle être autre que "abstraite" ou "finale / scellée"?

MODIFIER

Ce grand article explique mes préoccupations bien mieux que moi.

Nicolas Repiquet
la source
21
Parce que ça s'appelle le Open/Closed principle, pas leClosed Principle.
StuperUser
2
Quel genre de choses écrivez-vous professionnellement? Cela pourrait très bien influencer votre opinion à ce sujet.
Eh bien, je connais des développeurs de plate-forme qui scellent tout, car ils veulent minimiser l'interface d'héritage.
K ..
4
@StuperUser: Ce n'est pas une raison, c'est une platitude. Le PO ne demande pas quelle est la platitude, il demande pourquoi . S'il n'y a aucune raison derrière un principe, il n'y a aucune raison d'y prêter attention.
Michael Shaw
1
Je me demande combien de frameworks d'interface utilisateur briseraient en changeant la classe Window en scellé.
Reactgular

Réponses:

46

Ironiquement, je trouve le contraire: l'utilisation de classes abstraites est l'exception plutôt que la règle et j'ai tendance à froncer les sourcils sur les classes finales / scellées.

Les interfaces sont un mécanisme de conception par contrat plus typique car vous ne spécifiez aucun élément interne - vous ne vous en souciez pas. Il permet à chaque mise en œuvre de ce contrat d'être indépendante. Ceci est essentiel dans de nombreux domaines. Par exemple, si vous construisez un ORM, il serait très important que vous puissiez passer une requête à la base de données de manière uniforme, mais les implémentations peuvent être très différentes. Si vous utilisez des classes abstraites à cet effet, vous vous retrouvez à câbler des composants qui peuvent s'appliquer ou non à toutes les implémentations.

Pour les classes finales / scellées, la seule excuse que je puisse voir pour les utiliser est quand il est réellement dangereux d'autoriser la substitution - peut-être un algorithme de chiffrement ou quelque chose. En dehors de cela, vous ne savez jamais quand vous souhaitez étendre la fonctionnalité pour des raisons locales. Sceller une classe restreint vos options de gains inexistants dans la plupart des scénarios. Il est beaucoup plus flexible d'écrire vos classes de manière à pouvoir les étendre plus tard sur la ligne.

Cette dernière vision a été cimentée pour moi en travaillant avec des composants tiers qui ont scellé les classes, empêchant ainsi une intégration qui aurait rendu la vie beaucoup plus facile.

Michael
la source
17
+1 pour le dernier paragraphe. J'ai souvent trouvé que trop d'encapsulation dans les bibliothèques tierces (ou même les classes de bibliothèque standard!) Était une cause de douleur plus importante que trop peu d'encapsulation.
Mason Wheeler
5
L'argument habituel pour sceller des classes est que vous ne pouvez pas prédire les nombreuses manières différentes dont un client peut éventuellement remplacer votre classe, et donc vous ne pouvez pas garantir le comportement. Voir blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey
12
@RobertHarvey Je connais l'argument et cela sonne bien en théorie. En tant que concepteur d'objets, vous ne pouvez pas prévoir comment les gens peuvent prolonger vos cours - c'est précisément pourquoi ils ne doivent pas être scellés. Vous ne pouvez pas tout supporter - très bien. Non. Mais ne supprimez pas les options non plus.
Michael
6
@RobertHarvey n'est-ce pas que les personnes qui ne respectent pas le LSP obtiennent ce qu'elles méritent?
StuperUser
2
Je ne suis pas non plus un grand fan des classes scellées, mais je pouvais voir pourquoi certaines entreprises comme Microsoft (qui doivent souvent prendre en charge des choses qu'elles ne se sont pas cassées) les trouvent attrayantes. Les utilisateurs d'un framework ne sont pas nécessairement censés avoir une connaissance des internes d'une classe.
Robert Harvey
11

C'est un grand article par Eric Lippert, mais je ne pense pas qu'il prend en charge votre point de vue.

Il fait valoir que toutes les classes exposées pour une utilisation par d'autres devraient être scellées ou rendues non extensibles.

Votre prémisse est que toutes les classes doivent être abstraites ou scellées.

Grande différence.

L'article d'EL ne dit rien sur les (vraisemblablement nombreux) cours produits par son équipe dont vous et moi ne savons rien. En général, les classes exposées publiquement dans un framework ne sont qu'un sous-ensemble de toutes les classes impliquées dans l'implémentation de ce framework.

Martin
la source
Mais vous pouvez appliquer le même argument aux classes qui ne font pas partie d'une interface publique. L'une des principales raisons de rendre une classe finale est qu'elle agit comme une mesure de sécurité pour empêcher le code bâclé. Cet argument est tout aussi valable pour toute base de code partagée par différents développeurs au fil du temps, ou même si vous êtes le seul développeur. Il protège simplement la sémantique du code.
DPM
Je pense que l'idée que les classes devraient être abstraites ou scellées fait partie d'un principe plus large, à savoir qu'il faut éviter d'utiliser des variables de types de classe instanciables. Un tel évitement permettra de créer un type utilisable par les consommateurs d'un type donné, sans qu'il en hérite de tous les membres privés. Malheureusement, ces conceptions ne fonctionnent pas vraiment avec la syntaxe du constructeur public. Au lieu de cela, le code devrait être remplacé new List<Foo>()par quelque chose comme List<Foo>.CreateMutable().
supercat
5

Du point de vue java, je pense que les classes finales ne sont pas aussi intelligentes qu'il n'y paraît.

De nombreux outils (en particulier AOP, JPA, etc.) fonctionnent avec la suppression du temps de chargement, ils doivent donc étendre vos classes. L'autre façon serait de créer des délégués (pas ceux .NET) et de tout déléguer à la classe d'origine, ce qui serait beaucoup plus compliqué que d'étendre les classes d'utilisateurs.

Uwe Plonus
la source
5

Deux cas courants où vous aurez besoin de cours de vanille non scellés:

  1. Technique: si vous avez une hiérarchie de plus de deux niveaux et que vous souhaitez pouvoir instancier quelque chose au milieu.

  2. Principe: Il est parfois souhaitable d'écrire des classes explicites conçues pour être étendues en toute sécurité . (Cela se produit souvent lorsque vous écrivez une API comme Eric Lippert ou lorsque vous travaillez en équipe sur un grand projet). Parfois, vous voulez écrire une classe qui fonctionne bien seule, mais elle est conçue avec une extensibilité à l'esprit.

Les réflexions d'Eric Lippert sur le scellement ont du sens, mais il admet également qu'elles font du design pour l'extensibilité en laissant la classe "ouverte".

Oui, de nombreuses classes sont scellées dans la BCL, mais un grand nombre de classes ne le sont pas et peuvent être étendues de toutes sortes de façons merveilleuses. Un exemple qui me vient à l'esprit est dans Windows Forms, où vous pouvez ajouter des données ou un comportement à presque n'importe quel Controlvia l'héritage. Bien sûr, cela aurait pu être fait d'autres façons (motif décorateur, divers types de composition, etc.), mais l'héritage fonctionne très bien aussi.

Deux notes spécifiques à .NET:

  1. Dans la plupart des cas, les classes de scellage ne sont souvent pas critiques pour la sécurité, car les héritiers ne peuvent pas jouer avec votre non- virtualfonctionnalité, à l'exception des implémentations d'interface explicites.
  2. Parfois, une alternative appropriée consiste à créer le constructeur internalau lieu de sceller la classe, ce qui lui permet d'être hérité à l'intérieur de votre base de code, mais pas à l'extérieur.
Kevin McCormick
la source
4

Je crois que la vraie raison pour laquelle beaucoup de gens pensent que les classes devraient être final/ sealedest que la plupart des classes extensibles non abstraites ne sont pas correctement documentées .

Permettez-moi de développer. Partant de loin, certains programmeurs estiment que l'héritage en tant qu'outil dans la POO est largement surutilisé et abusé. Nous avons tous lu le principe de substitution de Liskov, bien que cela ne nous ait pas empêchés de le violer des centaines (voire des milliers) de fois.

Le fait est que les programmeurs aiment réutiliser le code. Même quand ce n'est pas une si bonne idée. Et l'héritage est un outil clé dans cette «réutilisation» du code. Retour à la question finale / scellée.

La documentation appropriée pour une classe finale / scellée est relativement petite: vous décrivez ce que fait chaque méthode, quels sont les arguments, la valeur de retour, etc. Toutes les choses habituelles.

Cependant, lorsque vous documentez correctement une classe extensible, vous devez inclure au moins les éléments suivants :

  • Dépendances entre les méthodes (quelle méthode appelle quoi, etc.)
  • Dépendances des variables locales
  • Contrats internes que la classe d'extension devrait honorer
  • Convention d'appel pour chaque méthode (par exemple, lorsque vous la remplacez, appelez-vous l' superimplémentation? L'appelez-vous au début de la méthode ou à la fin? Pensez constructeur vs destructeur)
  • ...

Ce sont juste au-dessus de ma tête. Et je peux vous donner un exemple de la raison pour laquelle chacun de ces éléments est important et le sauter entraînera une extension de la classe.

Maintenant, considérez combien d'efforts de documentation devraient être nécessaires pour documenter correctement chacune de ces choses. Je crois qu'une classe avec 7-8 méthodes (ce qui peut être trop dans un monde idéalisé, mais trop peu dans le réel) pourrait aussi bien avoir une documentation d'environ 5 pages, texte seulement. Donc, à la place, nous nous retirons à mi-chemin et ne scellons pas la classe, afin que d'autres personnes puissent l'utiliser, mais ne la documentent pas non plus, car cela prendra un temps gigantesque (et, vous savez, cela pourrait ne jamais être prolongé de toute façon, alors pourquoi s'embêter?).

Si vous concevez une classe, vous pourriez ressentir la tentation de la sceller afin que les gens ne puissent pas l'utiliser d'une manière que vous n'avez pas prévue (et préparée). D'un autre côté, lorsque vous utilisez le code de quelqu'un d'autre, parfois à partir de l'API publique, il n'y a aucune raison visible pour que la classe soit finale, et vous pourriez penser "Merde, cela m'a juste coûté 30 minutes à la recherche d'une solution de contournement".

Je pense que certains des éléments d'une solution sont:

  • D'abord, assurez-vous que l' extension est une bonne idée lorsque vous êtes client du code, et privilégiez vraiment la composition plutôt que l'héritage.
  • Ensuite, lisez le manuel dans son intégralité (encore une fois en tant que client) pour vous assurer que vous ne négligez pas quelque chose qui est mentionné.
  • Troisièmement, lorsque vous écrivez un morceau de code que le client utilisera, rédigez une documentation appropriée pour le code (oui, à long terme). À titre d'exemple positif, je peux donner les documents iOS d'Apple. Ils ne sont pas suffisants pour que l'utilisateur étende toujours correctement ses classes, mais ils incluent au moins quelques informations sur l'héritage. C'est plus que ce que je peux dire pour la plupart des API.
  • Quatrièmement, essayez en fait d'étendre votre propre classe pour vous assurer que cela fonctionne. Je suis un grand partisan de l'inclusion de nombreux échantillons et tests dans les API, et lorsque vous faites un test, vous pouvez aussi bien tester les chaînes d'héritage: elles font partie de votre contrat après tout!
  • Cinquièmement, dans les situations où vous avez un doute, indiquez que la classe n'est pas destinée à être prolongée et que le faire est une mauvaise idée (tm). Indiquez que vous ne devriez pas être tenu pour responsable d'une telle utilisation non intentionnelle, mais ne scellez pas la classe. Évidemment, cela ne couvre pas les cas où la classe doit être scellée à 100%.
  • Enfin, lorsque vous scellez une classe, fournissez une interface en tant que hook intermédiaire, afin que le client puisse «réécrire» sa propre version modifiée de la classe et contourner votre classe «scellée». De cette façon, il peut remplacer la classe scellée par son implémentation. Maintenant, cela devrait être évident, car il s'agit d'un couplage lâche dans sa forme la plus simple, mais cela vaut toujours la peine d'être mentionné.

Il convient également de mentionner la question "philosophique" suivante: est-ce qu'une classe fait sealed/ fait finalpartie du contrat pour la classe, ou un détail d'implémentation? Maintenant, je ne veux pas y aller, mais une réponse à cela devrait également influencer votre décision de sceller ou non une classe.

K.Steff
la source
3

Une classe ne doit être ni finale / scellée ni abstraite si:

  • C'est utile en soi, c'est-à-dire qu'il est avantageux d'avoir des instances de cette classe.
  • Il est avantageux que cette classe soit la sous-classe / classe de base des autres classes.

Par exemple, prenez la ObservableCollection<T>classe en C # . Il n'a qu'à ajouter le déclenchement d'événements aux opérations normales de a Collection<T>, c'est pourquoi il sous-classe Collection<T>. Collection<T>est une classe viable en soi, et donc elle ObservableCollection<T>.

FishBasketGordo
la source
Si j'étais en charge de cela, je ferais probablement CollectionBase(résumé), Collection : CollectionBase(scellé), ObservableCollection : CollectionBase(scellé). Si vous regardez attentivement Collection <T> , vous verrez que c'est une classe abstraite à demi-assed.
Nicolas Repiquet
2
Quel avantage tirez-vous d'avoir trois classes au lieu de deux? De plus, comment est Collection<T>une "classe abstraite à demi-cul"?
FishBasketGordo
Collection<T>expose de nombreux entrailles grâce à des méthodes et des propriétés protégées, et est clairement destiné à être une classe de base pour une collection spécialisée. Mais il n'est pas clair où vous êtes censé mettre votre code, car il n'y a pas de méthodes abstraites à implémenter. ObservableCollectionhérite Collectionet n'est pas scellé, vous pouvez donc en hériter à nouveau. Et vous pouvez accéder à la Itemspropriété protégée, ce qui vous permet d'ajouter des éléments dans la collection sans déclencher d'événements ... Sympa.
Nicolas Repiquet
@NicolasRepiquet: Dans de nombreux langages et frameworks, les moyens idiomatiques normaux de création d'un objet nécessitent que le type de la variable qui contiendra l'objet soit le même que le type de l'instance créée. Dans de nombreux cas, l'idéal serait de passer des références à un type abstrait, mais cela forcerait beaucoup de code à utiliser un type pour les variables et les paramètres et un type concret différent lors de l'appel des constructeurs. À peine impossible, mais un peu maladroit.
supercat
3

Le problème avec les classes finales / scellées est qu'elles essaient de résoudre un problème qui ne s'est pas encore produit. Ce n'est utile que lorsque le problème existe, mais c'est frustrant car un tiers a imposé une restriction. La classe de scellage résout rarement un problème actuel, ce qui rend difficile de discuter de son utilité.

Il y a des cas où une classe doit être scellée. Comme exemple; La classe gère les ressources / mémoire allouées d'une manière telle qu'elle ne peut pas prédire comment les changements futurs pourraient altérer cette gestion.

Au fil des ans, j'ai trouvé que l'encapsulation, les rappels et les événements étaient beaucoup plus flexibles / utiles que les classes abstraites. Je vois beaucoup de code avec une grande hiérarchie de classes où l'encapsulation et les événements auraient simplifié la vie du développeur.

Reactgular
la source
1
Le contenu scellé dans le logiciel résout le problème "Je ressens le besoin d'imposer ma volonté aux futurs responsables de ce code".
Kaz
N'a pas été quelque chose de final / sceau ajouté à OOP, parce que je ne me souviens pas que c'était quand j'étais plus jeune. Cela ressemble à une sorte de fonctionnalité après réflexion.
Reactgular
La finalisation a existé en tant que procédure de chaîne d'outils dans les systèmes OOP avant que OOP ne soit réduit à des langages comme C ++ et Java. Les programmeurs travaillent dans Smalltalk, Lisp avec une flexibilité maximale: tout peut être étendu, de nouvelles méthodes ajoutées tout le temps. Ensuite, l'image compilée du système est soumise à une optimisation: on suppose que le système ne sera pas étendu par les utilisateurs finaux, ce qui signifie que la répartition des méthodes peut être optimisée en fonction de l'inventaire des méthodes et les classes existent maintenant .
Kaz
Je ne pense pas que ce soit la même chose, car ce n'est qu'une fonctionnalité d'optimisation. Je ne me souviens pas avoir été scellé dans toutes les versions de Java, mais je peux me tromper car je ne l'utilise pas beaucoup.
Reactgular
Ce n'est pas parce qu'il se manifeste sous la forme de certaines déclarations que vous devez insérer dans le code que ce n'est pas la même chose.
Kaz
2

Le «scellement» ou la «finalisation» dans les systèmes d'objets permet certaines optimisations, car le graphe de répartition complet est connu.

C'est-à-dire que nous rendons difficile la transformation du système en quelque chose d'autre, en échange de performances. (C'est l'essence de la plupart des optimisations.)

À tous les autres égards, c'est une perte. Les systèmes doivent être ouverts et extensibles par défaut. Il devrait être facile d'ajouter de nouvelles méthodes à toutes les classes et de les étendre arbitrairement.

Nous n'obtenons aucune nouvelle fonctionnalité aujourd'hui en prenant des mesures pour empêcher une future extension.

Donc, si nous le faisons pour la prévention elle-même, ce que nous faisons, c'est essayer de contrôler la vie des futurs responsables. Il s'agit de l'ego. "Même quand je ne travaillerai plus ici, ce code sera maintenu à ma façon, bon sang!"

Kaz
la source
1

Les classes de test semblent me venir à l'esprit. Ce sont des classes qui sont appelées de manière automatisée ou "à volonté" en fonction de ce que le programmeur / testeur essaie d'accomplir. Je ne suis pas sûr d'avoir jamais vu ou entendu parler d'une classe de tests terminée et privée.

joshin4colours
la source
De quoi tu parles? En quoi les classes de test ne doivent-elles pas être finales / scellées / abstraites?
1

Le moment où vous avez besoin d'envisager une classe qui doit être prolongée, c'est lorsque vous faites une véritable planification pour l'avenir. Permettez-moi de vous donner un exemple réel de mon travail.

Je passe une grande partie de mon temps à écrire des outils d'interface entre notre produit principal et nos systèmes externes. Lorsque nous faisons une nouvelle vente, un gros composant est un ensemble d'exportateurs conçus pour être exécutés à intervalles réguliers qui génèrent des fichiers de données détaillant les événements qui se sont produits ce jour-là. Ces fichiers de données sont ensuite consommés par le système du client.

C'est une excellente opportunité pour prolonger les cours.

J'ai une classe d'exportation qui est la classe de base de chaque exportateur. Il sait comment se connecter à la base de données, savoir où il en était la dernière fois qu'il a été exécuté et créer des archives des fichiers de données qu'il génère. Il fournit également la gestion des fichiers de propriétés, la journalisation et une gestion simple des exceptions.

En plus de cela, j'ai un exportateur différent pour travailler avec chaque type de données, il y a peut-être des activités des utilisateurs, des données transactionnelles, des données de gestion de trésorerie, etc.

Au-dessus de cette pile, je place une couche spécifique au client qui implémente la structure de fichiers de données dont le client a besoin.

De cette façon, l'exportateur de base change très rarement. Les principaux exportateurs de types de données changent parfois mais rarement, et généralement uniquement pour gérer les modifications de schéma de base de données qui doivent être propagées à tous les clients de toute façon. Le seul travail que j'ai à faire pour chaque client est la partie du code qui lui est propre. Un monde parfait!

La structure ressemble donc à:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Mon point principal est qu'en architecturant le code de cette façon, je peux utiliser l'héritage principalement pour la réutilisation du code.

Je dois dire que je ne vois aucune raison de dépasser trois niveaux.

J'ai utilisé deux couches plusieurs fois, par exemple pour avoir une Tableclasse commune qui implémente des requêtes de table de base de données tandis que des sous-classes Tableimplémentent les détails spécifiques de chaque table en utilisant un enumpour définir les champs. Laisser l' enumimplémentation d'une interface définie dans la Tableclasse a toutes sortes de sens.

OldCurmudgeon
la source
1

J'ai trouvé les classes abstraites utiles mais pas toujours nécessaires, et les classes scellées deviennent un problème lors de l'application des tests unitaires. Vous ne pouvez pas vous moquer ou écraser une classe scellée, sauf si vous utilisez quelque chose comme Teleriks justmock.

Dan H
la source
1
C'est un bon commentaire, mais ne répond pas directement à la question. Veuillez envisager d'élargir vos réflexions ici.
1

Je partage votre avis. Je pense qu'en Java, par défaut, les classes devraient être déclarées "finales". Si vous ne le rendez pas définitif, préparez-le spécifiquement et documentez-le pour l'extension.

La principale raison à cela est de garantir que toutes les instances de vos classes respectent l'interface que vous avez initialement conçue et documentée. Sinon, un développeur utilisant vos classes peut potentiellement créer du code fragile et incohérent et, à son tour, le transmettre à d'autres développeurs / projets, rendant les objets de vos classes non fiables.

D'un point de vue plus pratique, il y a en effet un inconvénient à cela, car les clients de l'interface de votre bibliothèque ne pourront pas effectuer de réglages et utiliser vos classes de manière plus flexible que celle à laquelle vous pensiez à l'origine.

Personnellement, pour tout le code de mauvaise qualité qui existe (et puisque nous en discutons à un niveau plus pratique, je dirais que le développement Java est plus enclin à cela), je pense que cette rigidité est un petit prix à payer dans notre quête de plus facile à maintenir le code.

DPM
la source