Considérez le design suivant
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
Pensez-vous qu'il y a quelque chose qui ne va pas ici? Pour moi, les classes Karl et John doivent être des instances plutôt que des classes car elles sont exactement les mêmes que:
Person karl = new Person("Karl");
Person john = new Person("John");
Pourquoi devrais-je créer de nouvelles classes lorsque les instances sont suffisantes? Les classes n'ajoutent rien à l'instance.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler Garcia
la source
la source
Réponses:
Il n'est pas nécessaire d'avoir des sous-classes spécifiques pour chaque personne.
Vous avez raison, ce devraient être des exemples à la place.
But des sous-classes: étendre les classes parentes
Les sous-classes sont utilisées pour étendre les fonctionnalités fournies par la classe parente. Par exemple, vous pouvez avoir:
Une classe parente
Battery
qui peutPower()
quelque chose et peut avoir uneVoltage
propriété,Et une sous-classe
RechargeableBattery
, qui peut hériterPower()
etVoltage
, mais peut aussi êtreRecharge()
d.Notez que vous pouvez passer une instance de
RechargeableBattery
classe en tant que paramètre à toute méthode qui accepte unBattery
en tant qu'argument. C'est ce qu'on appelle le principe de substitution de Liskov , l'un des cinq principes SOLID. De même, dans la réalité, si mon lecteur MP3 accepte deux piles AA, je peux les remplacer par deux piles AA rechargeables.Notez qu'il est parfois difficile de déterminer si vous devez utiliser un champ ou une sous-classe pour représenter une différence entre deux éléments. Par exemple, si vous devez manipuler des piles AA, AAA et 9 Volts, créez-vous trois sous-classes ou utilisez-vous une énumération? «Remplacer la sous-classe par des champs» dans Refactoring par Martin Fowler, page 233, peut vous donner quelques idées et vous expliquer comment passer d'une base à l'autre.
Dans votre exemple,
Karl
etJohn
ne prolongez rien, ils ne fournissent aucune valeur supplémentaire: vous pouvez avoir exactement les mêmes fonctionnalités en utilisantPerson
directement la classe. Avoir plus de lignes de code sans valeur supplémentaire n'est jamais bon.Un exemple d'analyse de rentabilisation
Que pourrait-il être une analyse de rentabilisation où il serait logique de créer une sous-classe pour une personne spécifique?
Disons que nous construisons une application qui gère les personnes travaillant dans une entreprise. L'application gère également les autorisations. Helen, le comptable, ne peut donc pas accéder au référentiel SVN, mais Thomas et Mary, deux programmeurs, ne peuvent pas accéder aux documents liés à la comptabilité.
Jimmy, le grand patron (fondateur et PDG de la société) a des privilèges très spécifiques que personne ne possède. Il peut, par exemple, arrêter tout le système ou licencier une personne. Vous avez eu l'idée.
Le modèle le plus pauvre pour une telle application consiste à avoir des classes telles que:
parce que la duplication de code va se produire très rapidement. Même dans l'exemple très basique de quatre employés, vous dupliquez le code entre les classes Thomas et Mary. Cela vous poussera à créer une classe parent commune
Programmer
. Étant donné que vous pouvez avoir plusieurs comptables, vous allez probablement créer uneAccountant
classe également.Maintenant, vous remarquez qu’avoir la classe
Helen
n’est pas très utile, ainsi que de garderThomas
etMary
: la plupart de votre code fonctionne de toute façon au niveau supérieur, au niveau des comptables, des programmeurs et de Jimmy. Le serveur SVN ne se soucie pas de savoir si c'est Thomas ou Mary qui doit accéder au journal. Il lui suffit de savoir s'il s'agit d'un programmeur ou d'un comptable.Donc, vous finissez par supprimer les classes que vous n'utilisez pas:
"Mais je peux garder Jimmy en l'état, car il n'y aurait toujours qu'un seul PDG, un seul grand patron - Jimmy", vous pensez. De plus, Jimmy est beaucoup utilisé dans votre code, qui ressemble en réalité davantage à ceci, et non à l'exemple précédent:
Le problème avec cette approche est que Jimmy peut toujours être frappé par un bus et qu'il y aurait un nouveau PDG. Ou bien le conseil d'administration peut décider que Mary est si géniale qu'elle devrait devenir un nouveau PDG, et que Jimmy serait rétrogradé au poste de vendeur. Vous devez donc maintenant parcourir tout votre code et tout changer.
la source
Il est idiot d'utiliser ce type de structure de classe uniquement pour faire varier les détails, qui doivent évidemment être des champs réglables sur une instance. Mais cela est particulier à votre exemple.
Créer des
Person
classes différentes est probablement une mauvaise idée. Vous devez modifier votre programme et recompiler chaque fois qu'une nouvelle personne entre dans votre domaine. Cependant, cela ne signifie pas qu'hériter d'une classe existante pour qu'une chaîne différente soit renvoyée quelque part n'est parfois pas utile.La différence réside dans la manière dont vous vous attendez à ce que les entités représentées par cette classe varient. Les gens sont presque toujours supposés aller et venir, et presque toutes les applications sérieuses traitant avec des utilisateurs devraient pouvoir ajouter et supprimer des utilisateurs au moment de l'exécution. Mais si vous modélisez des algorithmes de chiffrement différents, prendre en charge un nouvel algorithme est probablement un changement majeur, et il est donc logique d’inventer une nouvelle classe dont la
myName()
méthode retourne une chaîne différente (et dont laperform()
méthode fait probablement autre chose).la source
Dans la plupart des cas, vous ne le feriez pas. Votre exemple est vraiment un bon cas où ce comportement n'ajoute pas de valeur réelle.
Cela viole également le principe Open Closed , dans la mesure où les sous-classes ne sont fondamentalement pas une extension mais modifient le fonctionnement interne du parent. De plus, le constructeur public du parent est maintenant irritant dans les sous-classes et l’API est donc devenu moins compréhensible.
Cependant, si vous ne disposez que d'une ou deux configurations spéciales souvent utilisées dans le code, il est parfois plus pratique et plus rapide de sous-classer un parent doté d'un constructeur complexe. Dans de tels cas particuliers, je ne vois aucun inconvénient à une telle approche. Appelons cela constructeur currying j / k
la source
Si telle est l'étendue de la pratique, alors je conviens que c'est une mauvaise pratique.
Si John et Karl ont des comportements différents, les choses changent un peu. Il se peut que John
person
ait une méthodecleanRoom(Room room)
et qu'il nettoie très efficacement, mais Karl ne le fait pas et ne nettoie pas beaucoup la pièce.Dans ce cas, il serait logique de les placer dans leur propre sous-classe avec le comportement défini. Il existe de meilleurs moyens d’obtenir la même chose (par exemple, une
CleaningBehavior
classe), mais au moins, cette façon n’est pas une violation terrible des principes de OO.la source
Dans certains cas, vous savez qu'il n'y aura qu'un nombre défini d'instances et qu'il serait alors OK (bien que moche à mon avis) d'utiliser cette approche. Il est préférable d’utiliser une énumération si la langue le permet.
Exemple Java:
la source
Beaucoup de gens ont déjà répondu. Je pensais donner mon point de vue personnel.
Il était une fois, j'ai travaillé (et je le fais encore) sur une application qui crée de la musique.
L'application avait un résumé
Scale
classe avec plusieurs sous - classes:CMajor
,DMinor
, etc.Scale
avait l' air quelque chose comme ceci:Les générateurs de musique ont travaillé avec une
Scale
instance spécifique pour générer de la musique. L'utilisateur sélectionnerait une balance dans une liste pour générer de la musique.Un jour, une idée géniale m'est venue à l'esprit: pourquoi ne pas permettre à l'utilisateur de créer ses propres balances? L'utilisateur sélectionnerait des notes dans une liste, appuierait sur un bouton et une nouvelle échelle serait ajoutée à la liste des échelles disponibles.
Mais je n'ai pas pu faire ça. En effet, toutes les échelles sont déjà définies au moment de la compilation, car elles sont exprimées sous forme de classes. Puis ça m'a frappé:
Il est souvent intuitif de penser en termes de «superclasses et sous-classes». Presque tout peut être exprimé à travers ce système: superclasse
Person
et sousJohn
- classes etMary
; superclasseCar
et sousVolvo
- classes etMazda
; superclasseMissile
et sousSpeedRocked
- classes ,LandMine
etTrippleExplodingThingy
.C'est très naturel de penser de cette façon, surtout pour la personne relativement nouvelle en OO.
Mais nous devrions toujours nous rappeler que les classes sont des modèles et que les objets sont du contenu versé dans ces modèles . Vous pouvez verser le contenu que vous voulez dans le modèle, créant ainsi d'innombrables possibilités.
Ce n'est pas le travail de la sous-classe de remplir le modèle. C'est le travail de l'objet. Le travail de la sous-classe consiste à ajouter une fonctionnalité réelle ou à développer le modèle .
Et c’est pourquoi j’aurais dû créer une
Scale
classe concrète , avec unNote[]
champ, et laisser les objets remplir ce modèle ; éventuellement par le constructeur ou quelque chose. Et finalement, alors j'ai fait.Chaque fois que vous concevez un modèle dans une classe (par exemple, un
Note[]
membre vide qui doit être rempli ou unString name
champ auquel une valeur doit être affectée), rappelez-vous qu'il appartient aux objets de cette classe de remplir le modèle ( ou éventuellement ceux qui créent ces objets). Les sous-classes sont conçues pour ajouter des fonctionnalités et non pour remplir des modèles.Vous pourriez être tenté de créer une "super classe
Person
, des sousJohn
- classes etMary
" un système, comme vous l'avez fait, car vous aimez la formalité que cela vous procure.De cette façon, vous pouvez simplement dire
Person p = new Mary()
, au lieu dePerson p = new Person("Mary", 57, Sex.FEMALE)
. Cela rend les choses plus organisées et plus structurées. Mais comme nous l’avons dit, créer une nouvelle classe pour chaque combinaison de données n’est pas une bonne approche, car cela alourdit le code pour rien et vous limite en termes de capacités d’exécution.Voici donc une solution: utiliser une usine de base, voire statique. Ainsi:
De cette façon, vous pouvez facilement utiliser les "préréglages" du "venir avec le programme", comme suit:,
Person mary = PersonFactory.createMary()
mais vous vous réservez également le droit de concevoir de nouvelles personnes de manière dynamique, par exemple dans le cas où vous souhaitez autoriser l'utilisateur à le faire. . Par exemple:Ou même mieux: faites quelque chose comme ça:
Je me suis emporté. Je pense que vous avez l'idée.
Les sous-classes ne sont pas destinées à renseigner les modèles définis par leurs super-classes. Les sous-classes sont destinées à ajouter des fonctionnalités . Les objets sont destinés à remplir les modèles, c'est à quoi ils servent.
Vous ne devriez pas créer une nouvelle classe pour toutes les combinaisons possibles de données. (Tout comme je n'aurais pas dû créer une nouvelle
Scale
sous-classe pour toutes les combinaisons possibles deNote
s).Her'es une directive: chaque fois que vous créez une nouvelle sous-classe, demandez-vous si elle ajoute une nouvelle fonctionnalité qui n'existe pas dans la super-classe. Si la réponse à cette question est "non", vous pourriez peut-être essayer de "remplir le modèle" de la superclasse, auquel cas il suffit de créer un objet. (Et éventuellement une Factory avec des 'presets', pour rendre la vie plus facile).
J'espère que ça t'as aidé.
la source
Ceci est une exception aux "devrait être des instances" ici - bien que légèrement extrême.
Si vous souhaitez que le compilateur impose des autorisations pour accéder aux méthodes en fonction de Karl ou de John (dans cet exemple) plutôt que de demander à l'implémentation de la méthode de vérifier quelle instance a été transmise, vous pouvez utiliser différentes classes. En appliquant différentes classes plutôt que l'instanciation, vous effectuez des vérifications de différenciation entre 'Karl' et 'John' au moment de la compilation plutôt que de l'exécution, ce qui peut être utile, en particulier dans un contexte de sécurité où le compilateur est plus fiable que l'exécution. contrôles de code des bibliothèques externes, par exemple.
la source
La duplication du code est considérée comme une mauvaise pratique .
Ceci est au moins partiellement dû au fait que les lignes de codes et donc la taille de la base de code sont corrélées aux coûts de maintenance et aux défauts .
Voici la logique de bon sens pertinente pour moi sur ce sujet particulier:
1) Si je devais changer cela, combien d’endroits devrais-je visiter? Moins c'est mieux ici.
2) Y a-t-il une raison pour que cela se fasse de cette façon? Dans votre exemple - cela ne pourrait pas être le cas - mais dans certains exemples, il pourrait y avoir une raison de le faire. Je peux penser tout d’abord à certains frameworks, comme les premiers Grails, où l’héritage ne jouait pas nécessairement avec certains plugins de GORM. Rares.
3) Est-ce plus propre et plus facile à comprendre de cette façon? Encore une fois, ce n'est pas dans votre exemple - mais il existe certains modèles d'héritage qui peuvent sembler être plus difficiles à gérer, car les classes séparées sont en fait plus faciles à gérer (certaines utilisations des modèles de commande ou de stratégie vous viennent à l'esprit).
la source
Il existe un cas d'utilisation: former une référence à une fonction ou à une méthode en tant qu'objet normal.
Vous pouvez voir cela souvent dans le code de l'interface graphique Java:
Le compilateur vous aide ici en créant une nouvelle sous-classe anonyme.
la source