Ces derniers temps, je m'inquiète de l'utilisation de classes abstraites.
Parfois, une classe abstraite est créée à l'avance et fonctionne comme un modèle de fonctionnement des classes dérivées. Cela signifie, plus ou moins, qu'ils fournissent des fonctionnalités de haut niveau mais omettent certains détails à implémenter par les classes dérivées. La classe abstraite définit la nécessité de ces détails en mettant en place des méthodes abstraites. Dans de tels cas, une classe abstraite fonctionne comme un plan directeur, une description de haut niveau de la fonctionnalité ou comme vous voulez l'appeler. Il ne peut pas être utilisé seul, mais doit être spécialisé pour définir les détails qui ont été omis de la mise en œuvre de haut niveau.
D'autres fois, il arrive que la classe abstraite soit créée après la création de certaines classes "dérivées" (puisque la classe parent / abstraite n'est pas là, elles n'ont pas encore été dérivées, mais vous savez ce que je veux dire). Dans ces cas, la classe abstraite est généralement utilisée comme un endroit où vous pouvez mettre tout type de code commun que contiennent les classes dérivées actuelles.
Ayant fait les observations ci-dessus, je me demande laquelle de ces deux affaires devrait être la règle. Devrait-on faire bouillir des détails dans la classe abstraite simplement parce qu'ils se trouvent actuellement communs à toutes les classes dérivées? Un code commun qui ne fait pas partie d'une fonctionnalité de haut niveau devrait-il être présent?
Le code qui peut ne pas avoir de sens pour la classe abstraite elle-même devrait-il être là simplement parce qu'il se trouve être commun aux classes dérivées?
Permettez-moi de donner un exemple: la classe abstraite A a une méthode a () et une méthode abstraite aq (). La méthode aq (), dans les deux classes dérivées AB et AC, utilise une méthode b (). Faut-il déplacer b () vers A? Si oui, dans le cas où quelqu'un ne regarde que A (supposons que AB et AC ne soient pas là), l'existence de b () n'aurait pas beaucoup de sens! Est-ce une mauvaise chose? Est-ce que quelqu'un devrait pouvoir regarder dans une classe abstraite et comprendre ce qui se passe sans visiter les classes dérivées?
Pour être honnête, au moment de poser cette question, j'ai tendance à croire que l'écriture d'une classe abstraite qui a du sens sans avoir à regarder dans les classes dérivées est une question de code propre et d'architecture propre. Ι n'aime pas vraiment l'idée d'une classe abstraite qui agit comme un vidage pour tout type de code se trouve être commune dans toutes les classes dérivées.
Que pensez-vous / pratiquez-vous?
la source
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- Pourquoi? N'est-il pas possible qu'au cours de la conception et du développement d'une application, je découvre que plusieurs classes ont des fonctionnalités communes qui peuvent naturellement être refactorisées en une classe abstraite? Dois-je être suffisamment clairvoyant pour toujours anticiper cela avant d'écrire un code pour les classes dérivées? Si je ne parviens pas à être aussi astucieux, est-il interdit d'effectuer une telle refactorisation? Dois-je jeter mon code et recommencer?Réponses:
Ce que vous demandez, c'est où placer
b()
, et, dans un autre sens, la question est de savoir siA
le meilleur choix est la super classe immédiate pourAB
etAC
.Il semble qu'il y ait trois choix:
b()
dans les deuxAB
etAC
ABAC-Parent
qui hérite deA
et qui introduitb()
, puis est utilisée comme super classe immédiate pourAB
etAC
b()
enA
(ne pas savoir si une autre classe d'avenirAD
voudrab()
ou non)Jusqu'à ce qu'une autre classe
AD
qui ne veuille pas seb()
présente, (3) semble être le bon choix.Au moment où un
AD
cadeau, nous pouvons refactoriser l'approche en (2) - après tout, c'est un logiciel!la source
b()
aucune classe. Faites-en une fonction libre qui prend toutes ses données comme arguments et ayez les deuxAB
etAC
appelez-la. Cela signifie que vous n'avez pas besoin de le déplacer ou de créer d'autres classes lorsque vous ajoutezAD
.b()
n'a pas besoin d'un état de longue durée d'instance.ac
qui appelleb
au lieu d'ac
être abstraite?Une classe abstraite n'est pas censée être le dépotoir pour diverses fonctions ou données qui sont jetées dans la classe abstraite parce qu'elle est pratique.
La seule règle empirique qui semble fournir l'approche orientée objet la plus fiable et la plus extensible est « Préférez la composition à l'héritage ». Une classe abstraite est probablement mieux considérée comme une spécification d'interface qui ne contient aucun code.
Si vous avez une méthode qui est une sorte de méthode de bibliothèque qui va de pair avec la classe abstraite, une méthode qui est le moyen le plus probable d'exprimer certaines fonctionnalités ou comportements dont les classes dérivées d'une classe abstraite ont généralement besoin, alors il est logique de créer un classe entre la classe abstraite et les autres classes où cette méthode est disponible. Cette nouvelle classe fournit une instanciation particulière de la classe abstraite définissant une alternative ou un chemin d'implémentation particulier en fournissant cette méthode.
L'idée d'une classe abstraite est d'avoir un modèle abstrait de ce que les classes dérivées qui implémentent réellement la classe abstraite sont censées fournir en termes de service ou de comportement. L'héritage est facile à sur-utiliser et donc souvent les classes les plus utiles sont composées de différentes classes utilisant un modèle de mixage .
Cependant, il y a toujours des questions de changement, ce qui va changer et comment cela changera-t-il et pourquoi changera-t-il.
L'héritage peut conduire à des corps de source cassants et fragiles (voir aussi Héritage: arrêtez de l'utiliser déjà! ).
la source
b()
soit une fonction pure. Ce pourrait être un fonctoid ou un modèle ou autre chose. J'utilise l'expression «méthode de bibliothèque» comme signifiant un composant, qu'il s'agisse d'une fonction pure, d'un objet COM ou de tout ce qui est utilisé pour la fonctionnalité qu'il fournit à la solution.Votre question suggère une approche soit des classes abstraites. Mais je pense que vous devriez les considérer comme juste un autre outil dans votre boîte à outils. Et puis la question devient: Pour quels emplois / problèmes les classes abstraites sont-elles le bon outil?
Un excellent cas d'utilisation consiste à implémenter le modèle de méthode de modèle . Vous mettez toute la logique invariante dans la classe abstraite et la logique variante dans ses sous-classes. Notez que la logique partagée en elle-même est incomplète et non fonctionnelle. La plupart du temps, il s'agit d'implémenter un algorithme, où plusieurs étapes sont toujours les mêmes, mais au moins une étape varie. Mettez cette étape comme une méthode abstraite qui est appelée à partir d'une des fonctions à l'intérieur de la classe abstraite.
Je pense que votre premier exemple est essentiellement une description du modèle de méthode de modèle (corrigez-moi si je me trompe), donc je considérerais cela comme un cas d'utilisation parfaitement valide des classes abstraites.
Pour votre deuxième exemple, je pense que l'utilisation de classes abstraites n'est pas le choix optimal, car il existe des méthodes supérieures pour gérer la logique partagée et le code dupliqué. Disons que vous avez une classe abstraite
A
, des classes dérivéesB
etC
que les deux classes dérivées partagent une certaine logique sous forme de méthodes()
. Pour décider de la bonne approche pour se débarrasser de la duplication, il est important de savoir si la méthodes()
fait partie de l'interface publique ou non.Si ce n'est pas le cas (comme dans votre propre exemple concret avec méthode
b()
), le cas est assez simple. Il suffit de créer une classe distincte de celle-ci, en extrayant également le contexte nécessaire pour effectuer l'opération. Il s'agit d'un exemple classique de composition sur héritage . S'il y a peu ou pas de contexte nécessaire, une simple fonction d'aide pourrait déjà suffire, comme suggéré dans certains commentaires.Si
s()
fait partie de l'interface publique, cela devient un peu plus délicat. Dans l'hypothèse quis()
n'a rien à voir avecB
et avec laquelle vous êtesC
liéA
, vous ne devriez pas mettre à l's()
intérieurA
. Alors, où le mettre alors? Je plaiderais pour déclarer une interface distincteI
, qui définits()
. Là encore, vous devez créer une classe distincte qui contient la logique d'implémentations()
et les deuxB
et enC
dépendre.Enfin, voici un lien vers une excellente réponse à une question intéressante sur SO qui pourrait vous aider à décider quand aller pour une interface et quand pour une classe abstraite:
la source
Il me semble que vous manquez le point d'orientation de l'objet, à la fois logiquement et techniquement. Vous décrivez deux scénarios: regroupement du comportement commun des types dans une classe de base et polymorphisme. Ce sont deux applications légitimes d'une classe abstraite. Mais si vous devez rendre la classe abstraite ou non, cela dépend de votre modèle analytique et non des possibilités techniques.
Vous devez reconnaître un type qui n'a aucune incarnation dans le monde réel, tout en jetant les bases de types spécifiques qui existent. Exemple: un animal. Un animal n'existe pas. C'est toujours un chien ou un chat ou autre mais il n'y a pas de véritable animal. Pourtant, Animal les encadre tous.
Ensuite, vous parlez de niveaux. Les niveaux ne font pas partie de l'accord en matière d'héritage. La reconnaissance de données ou de comportements communs n'est pas non plus une approche technique qui n'aidera probablement pas. Vous devez reconnaître un stéréotype, puis insérer la classe de base. S'il n'y a pas un tel stéréotype, vous pouvez être mieux avec quelques interfaces implémentées par plusieurs classes.
Le nom de votre classe abstraite avec ses membres devrait avoir un sens. Elle doit être indépendante de toute classe dérivée, à la fois techniquement et logiquement. Comme Animal pourrait avoir une méthode abstraite Eat (qui serait polymorphe) et des propriétés abstraites booléennes IsPet et IsLifestock, qui ont du sens sans connaître les chats, les chiens ou les porcs. Toute dépendance (technique ou logique) ne doit aller que dans un sens: du descendant à la base. Les classes de base elles-mêmes ne devraient pas non plus avoir connaissance des classes descendantes.
la source
Premier cas , à partir de votre question:
Deuxième cas:
Ensuite, votre question :
OMI, vous avez deux scénarios différents, vous ne pouvez donc pas dessiner une seule règle pour décider quel design doit être appliqué.
Pour le premier cas, nous avons des choses comme le modèle de méthode de modèle déjà mentionné dans d'autres réponses.
Pour le second cas, vous avez donné l'exemple de la classe abstraite A recevant la méthode ab (); IMO, la méthode b () ne doit être déplacée que si elle a du sens pour TOUTES les autres classes dérivées. En d'autres termes, si vous le déplacez parce qu'il est utilisé à deux endroits, ce n'est probablement pas un bon choix, car demain il pourrait y avoir une nouvelle classe dérivée concrète sur laquelle b () n'a aucun sens. En outre, comme vous l'avez dit, si vous regardez la classe A séparément et que b () n'a également aucun sens dans ce contexte, alors c'est probablement un indice que ce n'est pas un bon choix de conception également.
la source
J'ai eu exactement les mêmes questions il y a quelque temps!
Ce à quoi je suis arrivé, c'est que chaque fois que je tombe sur ce problème, je trouve qu'il y a un concept caché que je n'ai pas trouvé. Et ce concept devrait probablement être exprimé avec un objet de valeur . Vous avez un exemple très abstrait, donc je crains de ne pas pouvoir démontrer ce que je veux dire en utilisant votre code. Mais voici un cas de ma propre pratique. J'avais deux classes qui représentaient un moyen d'envoyer une demande à une ressource externe et d'analyser la réponse. Il y avait des similitudes dans la façon dont les demandes ont été formulées et comment les réponses ont été analysées. Voilà à quoi ressemblait ma hiérarchie:
Un tel code indique que j'utilise simplement l'héritage à mauvais escient. Voilà comment cela pourrait être refactorisé:
Depuis lors, je traite l'héritage très soigneusement car j'ai été blessé à plusieurs reprises.
Depuis lors, je suis arrivé à une autre conclusion sur l'héritage. Je suis fortement contre l' héritage basé sur la structure interne . Ça casse l'encapsulation, c'est fragile, c'est procédural après tout. Et lorsque je décompose mon domaine de la bonne façon , l'héritage ne se produit tout simplement pas très souvent.
la source