web-dev-qa-db-fra.com

La création de sous-classes pour des instances spécifiques est-elle une mauvaise pratique?

Considérez la conception suivante

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 ne devraient être que 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.

37

Il n'est pas nécessaire d'avoir des sous-classes spécifiques pour chaque personne.

Vous avez raison, ce devraient plutôt être des instances.

Objectif des sous-classes: étendre les classes parentes

Les sous-classes sont utilisées pour étendre les fonctionnalités fournies par la classe parent. Par exemple, vous pouvez avoir:

  • Une classe parente Battery qui peut Power() quelque chose et peut avoir une propriété Voltage,

  • Et une sous-classe RechargeableBattery, qui peut hériter Power() et Voltage, mais qui peut aussi être Recharge() d.

Notez que vous pouvez passer une instance de la classe RechargeableBattery comme paramètre à toute méthode qui accepte un Battery comme argument. Cela s'appelle principe de substitution Liskov, l'un des cinq principes SOLID. De même, dans la vraie vie, 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 quelque chose. Par exemple, si vous devez manipuler des piles AA, AAA et 9 volts, créeriez-vous trois sous-classes ou utiliser une énumération? "Remplacer la sous-classe par des champs" dans Refactoring par Martin Fowler, page 232, peut vous donner quelques idées et comment passer de l'une à l'autre.

Dans votre exemple, Karl et John n'étendent rien et ne fournissent aucune valeur supplémentaire: vous pouvez avoir exactement la même fonctionnalité en utilisant directement la classe Person. Avoir plus de lignes de code sans valeur supplémentaire n'est jamais bon.

Un exemple d'analyse de rentabilisation

Quelle pourrait être une analyse de rentabilisation où il serait en fait 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, donc Helen, la comptable, ne peut 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 l'entreprise) a des privilèges très spécifiques que personne n'a. Il peut, par exemple, arrêter l'ensemble du système ou licencier une personne. Vous avez eu l'idée.

Le modèle le plus pauvre pour une telle application est d'avoir des classes telles que:

 The class diagram shows Helen, Thomas, Mary and Jimmy classes and their common parent: the abstract Person class.

car la duplication de code se fera très rapidement. Même dans l'exemple très basique de quatre employés, vous dupliquerez du code entre les classes Thomas et Mary. Cela vous poussera à créer une classe parente commune Programmer. Comme vous pouvez également avoir plusieurs comptables, vous créerez probablement également la classe Accountant.

 The class diagram shows an abstract Person with three children: abstract Accountant, an abstract Programmer, and Jimmy. Accountant has a subclass Helen, and Programmer has two subclasses: Thomas and Mary.

Maintenant, vous remarquez qu'avoir la classe Helen n'est pas très utile, ainsi que conserver Thomas et Mary: la plupart de votre code fonctionne de toute façon au niveau supérieur - au niveau niveau de comptables, programmeurs et Jimmy. Le serveur SVN ne se soucie pas si c'est Thomas ou Mary qui a besoin d'accéder au journal - il a seulement besoin de savoir s'il s'agit d'un programmeur ou d'un comptable.

if (person is Programmer)
{
    this.AccessGranted = true;
}

Vous finissez donc par supprimer les classes que vous n'utilisez pas:

 The class diagram contains Accountant, Programmer and Jimmy subclasses with its common parent class: abstract Person.

"Mais je peux garder Jimmy tel quel, car il n'y aura toujours qu'un seul PDG, un seul grand patron - Jimmy", pensez-vous. De plus, Jimmy est beaucoup utilisé dans votre code, qui ressemble en fait à ceci, et non pas comme dans l'exemple précédent:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

Le problème avec cette approche est que Jimmy peut toujours être heurté par un bus, et il y aurait un nouveau PDG. Ou le conseil d'administration peut décider que Mary est si grande qu'elle devrait être un nouveau PDG, et Jimmy serait rétrogradé à un poste de vendeur, alors maintenant, vous devez passer par tout votre code et changez tout.

77
Arseni Mourzenko

Il est stupide d'utiliser ce type de structure de classe juste pour varier les détails qui devraient évidemment être des champs réglables sur une instance. Mais cela est particulier à votre exemple.

Créer des classes différentes de Persons est presque certainement une mauvaise idée - vous devrez changer votre programme et recompiler chaque fois qu'une nouvelle personne entre dans votre domaine. Cependant, cela ne signifie pas que l'héritage d'une classe existante afin qu'une chaîne différente soit renvoyée quelque part n'est pas parfois utile.

La différence réside dans la façon dont vous vous attendez à ce que les entités représentées par cette classe varient. Les utilisateurs sont presque toujours supposés aller et venir, et presque toutes les applications sérieuses traitant d'utilisateurs doivent pouvoir ajouter et supprimer des utilisateurs au moment de l'exécution. Mais si vous modélisez des choses comme différents algorithmes de chiffrement, la prise en charge d'un nouveau est probablement un changement majeur de toute façon, et il est logique d'inventer une nouvelle classe dont la méthode myName() renvoie une chaîne différente (et dont la fonction perform() la méthode fait probablement quelque chose de différent).

15
Kilian Foth

Pourquoi devrais-je créer de nouvelles classes lorsque les instances sont suffisantes?

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.

Il viole également le principe Open Closed , car 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 ainsi devenue moins compréhensible.

Cependant, parfois, si vous ne disposez que d'une ou deux configurations spéciales qui sont souvent utilisées dans le code, il est parfois plus pratique et moins long de simplement sous-classer un parent qui a un constructeur complexe. Dans de tels cas particuliers, je ne vois rien de mal à une telle approche. Appelons cela constructeur currying j/k

12
Falcon

Si telle est l'étendue de la pratique, je conviens que c'est une mauvaise pratique.

Si John et Karl ont des comportements différents, alors les choses changent un peu. Il se pourrait que person ait une méthode pour cleanRoom(Room room) où il s'avère que John est un grand colocataire et nettoie très efficacement, mais Karl ne nettoie pas et ne nettoie pas beaucoup la pièce.

Dans ce cas, il serait logique de les avoir leur propre sous-classe avec le comportement défini. Il existe de meilleures façons de réaliser la même chose (par exemple, une classe CleaningBehavior), mais au moins cette façon n'est pas une violation terrible des principes OO.

8
corsiKa

Dans certains cas, vous savez qu'il n'y aura qu'un nombre défini d'instances et il serait alors acceptable (bien que laid à mon avis) d'utiliser cette approche. Il est préférable d'utiliser une énumération si la langue le permet.

Exemple Java:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}
6
Adam

Beaucoup de gens ont déjà répondu. J'ai pensé donner mon point de vue personnel.


Il était une fois, je travaillais sur une application (et je le fais toujours) qui crée de la musique.

L'application avait une classe abstraite Scale avec plusieurs sous-classes: CMajor, DMinor, etc. Scale ressemblait à ceci:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Les générateurs de musique ont travaillé avec une instance Scale spécifique pour générer de la musique. L'utilisateur sélectionnerait une gamme 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 gammes? L'utilisateur sélectionnerait des notes dans une liste, appuyez 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 en classes. Puis ça m'a frappé:

Il est souvent intuitif de penser en termes de "superclasses et sous-classes". Presque tout peut être exprimé via ce système: superclasse Person et sous-classes John et Mary; superclasse Car et sous-classes Volvo et Mazda; superclasse Missile et sous-classes SpeedRocked, LandMine et TrippleExplodingThingy.

C'est très naturel de penser de cette façon, surtout pour la personne relativement nouvelle à OO.

Mais nous devons toujours nous rappeler que les classes sont des modèles et des objets le contenu est-il versé dans ces modèles . Vous pouvez verser le contenu que vous voulez dans le modèle, créant 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 est de ajouter des fonctionnalités réelles, ou développer le modèle.

Et c'est pourquoi j'aurais dû créer une classe concrète Scale, avec un champ Note[], Et laisser des objets remplir ce modèle ; éventuellement par le constructeur ou quelque chose. Et finalement, je l'ai fait.

Chaque fois que vous concevez un modèle dans une classe (par exemple, un membre Note[] Vide qui doit être rempli, ou un String name Auquel il faut attribuer une valeur), rappelez-vous que c'est le travail des objets de cette classe de remplir le modèle (ou éventuellement ceux qui créent ces objets). Les sous-classes sont destinées à ajouter des fonctionnalités, pas à remplir des modèles.


Vous pourriez être tenté de créer un système de type "superclasse Person, sous-classes John et Mary", 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 de Person p = new Person("Mary", 57, Sex.FEMALE). Cela rend les choses plus organisées et plus structurées. Mais comme nous l'avons dit, la création d'une nouvelle classe pour chaque combinaison de données n'est pas une bonne approche, car elle gonfle le code pour rien et vous limite en termes de capacités d'exécution.

Voici donc une solution: utilisez une usine de base, peut-être même statique. Ainsi:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

De cette façon, vous pouvez facilement utiliser les "préréglages" du "venir avec le programme", comme ceci: 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 voulez pour permettre à l'utilisateur de le faire. Par exemple.:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Ou encore mieux: faites quelque chose comme ça:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

Je me suis emporté. Je pense que vous avez l'idée.


Les sous-classes ne sont pas destinées à remplir les modèles définis par leurs superclasses. Les sous-classes sont destinées à ajouter des fonctionnalités . Les objets sont destinés à remplir les modèles, c'est à cela qu'ils servent.

Vous ne devez pas créer une nouvelle classe pour chaque combinaison possible de données. (Tout comme je n'aurais pas dû créer une nouvelle sous-classe Scale pour chaque combinaison possible de Notes).

Voici 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 superclasse. Si la réponse à cette question est "non", alors vous essayez peut-être de "remplir le modèle" de la superclasse, auquel cas créez simplement un objet. (Et peut-être une usine avec des "préréglages" ', pour vous faciliter la vie).

J'espère que cela pourra aider.

6
Aviv Cohn

Ceci est une exception aux "devrait être des instances" ici - bien que légèrement extrême.

Si vous vouliez que le compilateur applique les autorisations d'accès aux méthodes selon qu'il s'agit de Karl ou John (dans cet exemple) plutôt que de demander à l'implémentation de la méthode de vérifier quelle instance a été transmise, alors vous pourriez utiliser différents Des 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 qu'au moment de l'exécution et cela pourrait être utile - en particulier dans un contexte de sécurité où le compilateur peut être plus fiable que les contrôles de code au moment de l'exécution (par exemple) des bibliothèques externes.

2
David Scholefield

C'est considéré comme mauvaise pratique d'avoir du code dupliqué .

C'est au moins partiellement parce que les lignes de codes et donc la taille de la base de code sont en corrélation avec les coûts de maintenance et les 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 laquelle cela se fait de cette façon? Dans votre exemple - il ne pouvait pas y en avoir - mais dans certains exemples, il pourrait y avoir une sorte de raison de le faire. Celui auquel je peux penser du haut de ma tête se trouve dans certains cadres comme les premiers Grails où l'héritage ne jouait pas nécessairement bien avec certains des plugins pour 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 être plus étirés où les classes séparées sont en fait plus faciles à maintenir (certains usages de la commande ou des modèles de stratégie viennent à l'esprit).

2
dcgregorya

Il existe un cas d'utilisation: former une référence à une fonction ou une méthode en tant qu'objet normal.

Vous pouvez le voir dans Java Code GUI beaucoup:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

Le compilateur vous aide ici en créant une nouvelle sous-classe anonyme.

0
Simon Richter