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.
object-oriented
programming-practices
Nicolas Repiquet
la source
la source
Open/Closed principle
, pas leClosed Principle.
Réponses:
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.
la source
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.
la source
new List<Foo>()
par quelque chose commeList<Foo>.CreateMutable()
.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.
la source
Deux cas courants où vous aurez besoin de cours de vanille non scellés:
Technique: si vous avez une hiérarchie de plus de deux niveaux et que vous souhaitez pouvoir instancier quelque chose au milieu.
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
Control
via 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:
virtual
fonctionnalité, à l'exception des implémentations d'interface explicites.internal
au 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.la source
Je crois que la vraie raison pour laquelle beaucoup de gens pensent que les classes devraient être
final
/sealed
est 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 :
super
implé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:
Il convient également de mentionner la question "philosophique" suivante: est-ce qu'une classe fait
sealed
/ faitfinal
partie 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.la source
Une classe ne doit être ni finale / scellée ni abstraite si:
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 aCollection<T>
, c'est pourquoi il sous-classeCollection<T>
.Collection<T>
est une classe viable en soi, et donc elleObservableCollection<T>
.la source
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.Collection<T>
une "classe abstraite à demi-cul"?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.ObservableCollection
hériteCollection
et n'est pas scellé, vous pouvez donc en hériter à nouveau. Et vous pouvez accéder à laItems
propriété protégée, ce qui vous permet d'ajouter des éléments dans la collection sans déclencher d'événements ... Sympa.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.
la source
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!"
la source
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.
la source
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 à:
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
Table
classe commune qui implémente des requêtes de table de base de données tandis que des sous-classesTable
implémentent les détails spécifiques de chaque table en utilisant unenum
pour définir les champs. Laisser l'enum
implémentation d'une interface définie dans laTable
classe a toutes sortes de sens.la source
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.
la source
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.
la source