web-dev-qa-db-fra.com

Sous-classes de constructeur uniquement: est-ce un anti-modèle?

J'avais une discussion avec un collègue, et nous avons fini par avoir des intuitions contradictoires sur le but du sous-classement. Mon intuition est que si une fonction principale d'une sous-classe est d'exprimer une plage limitée de valeurs possibles de son parent, alors elle ne devrait probablement pas être une sous-classe. Il a plaidé pour l'intuition opposée: que la sous-classe représente un objet étant plus "spécifique", et donc une relation de sous-classe est plus appropriée.

Pour mettre mon intuition plus concrètement, je pense que si j'ai une sous-classe qui étend une classe parent mais que le seul code que la sous-classe remplace est un constructeur (oui, je sais que les constructeurs ne "surchargent" généralement pas, supportez-moi), alors ce qu'il fallait vraiment, c'était une méthode d'aide.

Par exemple, considérez cette classe quelque peu réelle:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

Il prétend qu'il serait tout à fait approprié d'avoir une sous-classe comme celle-ci:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Donc, la question:

  1. Parmi les personnes qui parlent de modèles de conception, laquelle de ces intuitions est correcte? Le sous-classement tel que décrit ci-dessus est-il un anti-modèle?
  2. S'il s'agit d'un anti-motif, quel est son nom?

Je m'excuse si cela s'est avéré être une réponse facilement googleable; mes recherches sur google ont principalement renvoyé des informations sur l'anti-modèle du constructeur télescopique et pas vraiment ce que je cherchais.

37
HardlyKnowEm

Si tout ce que vous voulez faire est de créer la classe X avec certains arguments, le sous-classement est une façon étrange d'exprimer cette intention, car vous n'utilisez aucune des fonctionnalités que les classes et l'héritage vous offrent. Ce n'est pas vraiment un anti-pattern, c'est juste étrange et un peu inutile (sauf si vous avez d'autres raisons). Une manière plus naturelle d'exprimer cette intention serait un méthode d'usine, qui dans ce cas est un nom de fantaisie pour votre "méthode d'assistance".

En ce qui concerne l'intuition générale, à la fois "plus spécifique" et "une plage limitée" sont des façons potentiellement néfastes de penser les sous-classes, car elles impliquent toutes deux que faire de Square une sous-classe de Rectangle est une bonne idée. Sans compter sur quelque chose de formel comme LSP, je dirais qu'une meilleure intuition est qu'une sous-classe soit fournit une implémentation de l'interface de base, ou étend l'interface pour ajouter de nouvelles fonctionnalités.

54
Ixrec

Laquelle de ces intuitions est correcte?

Votre collègue est correct (en supposant des systèmes de type standard).

Pensez-y, les classes représentent des valeurs légales possibles. Si la classe A a un champ d'un octet F, vous pourriez être enclin à penser que A a 256 valeurs légales, mais cette intuition est incorrecte. A limite "chaque permutation de valeurs jamais" à "doit avoir le champ F être 0-255".

Si vous étendez A avec B qui a un autre champ d'octet FF, c'est ajout de restrictions. Au lieu de "valeur où F est octet", vous avez "valeur où F est octet && FF est également octet". Puisque vous conservez les anciennes règles, tout ce qui fonctionnait pour le type de base fonctionne toujours pour votre sous-type. Mais puisque vous ajoutez des règles, vous vous spécialisez davantage loin de "pourrait être n'importe quoi".

Ou pour y penser autrement, supposons que A avait un tas de sous-types: B, C et D. Une variable de type A pourrait être n'importe lequel de ces sous-types, mais une variable de type B est plus spécifique. Il (dans la plupart des systèmes de types) ne peut pas être un C ou un D.

Est-ce un anti-modèle?

Ah non? Avoir des objets spécialisés est utile et n'est pas clairement préjudiciable au code. Atteindre ce résultat en utilisant le sous-typage est peut-être un peu discutable, mais dépend des outils dont vous disposez. Même avec de meilleurs outils, cette implémentation est simple, directe et robuste.

Je ne considérerais pas cela comme un anti-modèle car il n'est clairement ni mauvais ni mauvais dans aucune situation.

16
Telastyn

Non, ce n'est pas un anti-modèle. Je peux penser à un certain nombre de cas d'utilisation pratiques pour cela:

  1. Si vous souhaitez utiliser la vérification de la compilation pour vous assurer que les collections d'objets ne sont conformes qu'à une sous-classe particulière. Par exemple, si vous avez MySQLDao et SqliteDao dans votre système, mais pour une raison quelconque, vous voulez vous assurer qu'une collection ne contient que des données d'une seule source, si vous utilisez des sous-classes comme vous le décrivez, vous pouvez demander au compilateur de vérifier cette exactitude particulière. D'un autre côté, si vous stockez la source de données en tant que champ, cela deviendrait une vérification à l'exécution.
  2. Mon application actuelle a une relation un à un entre AlgorithmConfiguration + ProductClass et AlgorithmInstance. En d'autres termes, si vous configurez FooAlgorithm pour ProductClass dans le fichier de propriétés, vous ne pouvez obtenir qu'un FooAlgorithm pour cette classe de produits. La seule façon d'obtenir deux FooAlgorithm pour un ProductClass donné est de créer une sous-classe FooBarAlgorithm. C'est correct, car cela rend le fichier de propriétés facile à utiliser (il n'y a pas besoin de syntaxe pour de nombreuses configurations différentes dans une section du fichier de propriétés), et il est très rare qu'il y ait plus d'une instance pour une classe de produit donnée.
  3. @ réponse de Telastyn donne un autre bon exemple.

Bottom line: Il n'y a pas vraiment de mal à faire cela. Un Anti-pattern est défini comme:

Un anti-modèle (ou anti-modèle) est une réponse courante à un problème récurrent qui est généralement inefficace et risque d'être très contre-productif.

Il n'y a aucun risque d'être hautement contre-productif ici, donc ce n'est pas un anti-modèle.

12
durron597

Si quelque chose est un modèle ou un contre-modèle, cela dépend de manière significative de la langue et de l'environnement dans lesquels vous écrivez. Par exemple, for boucles sont un modèle dans Assembly, C et les langages similaires et un anti-modèle dans LISP.

Permettez-moi de vous raconter une histoire de code que j'ai écrit il y a longtemps ...

C'était dans un langage appelé LPC et implémenté un framework pour lancer des sorts dans un jeu. Vous aviez une superclasse de sorts, qui avait une sous-classe de sorts de combat qui géraient certains contrôles, puis une sous-classe pour les sorts de dégâts directs à cible unique, qui était ensuite sous-classée pour les sorts individuels - missile magique, éclair et autres.

La nature du fonctionnement du LPC était que vous aviez un objet maître qui était un Singleton. avant d'aller "eww Singleton" - c'était et ce n'était pas du tout "eww". On n'a pas fait de copie des sorts, mais plutôt invoqué les méthodes statiques (effectivement) dans les sorts. Le code du missile magique ressemblait à ceci:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

Et vous voyez ça? C'est une classe réservée aux constructeurs. Il a défini certaines valeurs qui étaient dans sa classe abstraite parent (non, il ne devrait pas utiliser de setters - c'est un immuable) et c'est tout. Cela a fonctionné à droite . Il était facile à coder, à étendre et à comprendre.

Ce type de modèle se traduit dans d'autres situations où vous avez une sous-classe qui étend une classe abstraite et définit quelques valeurs qui lui sont propres, mais toutes les fonctionnalités se trouvent dans la classe abstraite dont elle hérite. Allez voir StringBuilder in Java pour un exemple de cela - pas tout à fait constructeur seulement, mais je suis pressé de trouver beaucoup de logique dans les méthodes elles-mêmes (tout est dans le AbstractStringBuilder).

Ce n'est pas une mauvaise chose, et ce n'est certainement pas un anti-modèle (et dans certaines langues, il peut s'agir d'un modèle lui-même).

9
user40980

La définition orientée objet la plus stricte d'une relation de sous-classe est appelée "is-a". En utilisant l'exemple traditionnel d'un carré et d'un rectangle, un rectangle "est-un".

Par cela, vous devez utiliser le sous-classement pour toute situation où vous pensez qu'un "est-un" est une relation significative pour votre code.

Dans votre cas spécifique d'une sous-classe avec rien d'autre qu'un constructeur, vous devez l'analyser du point de vue "is-a". Il est clair qu'un SystemDataHelperBuilder "est-un" DataHelperBuilder, donc la première passe suggère que c'est une utilisation valide.

La question à laquelle vous devez répondre est de savoir si l'un des autres codes exploite cette relation "est-un". Un autre code tente-t-il de distinguer SystemDataHelperBuilders de la population générale de DataHelperBuilders? Si tel est le cas, la relation est tout à fait raisonnable et vous devez conserver la sous-classe.

Cependant, si aucun autre code ne fait cela, alors ce que vous avez est un relationshp qui pourrait être une relation "is-a", ou il pourrait être implémenté avec une usine. Dans ce cas, vous pouvez aller dans les deux sens, mais je recommanderais une usine plutôt qu'une relation "est-un". Lors de l'analyse de code orienté objet écrit par un autre individu, la hiérarchie des classes est une source majeure d'informations. Il fournit les relations "est-un" dont ils auront besoin pour donner un sens au code. Par conséquent, mettre ce comportement dans une sous-classe favorise le comportement de "quelque chose que quelqu'un trouvera s'il fouille dans les méthodes de la superclasse" à "quelque chose que tout le monde examinera avant même de commencer à plonger dans le code". Pour citer Guido van Rossum, "le code est lu plus souvent qu'il n'est écrit", donc la diminution de la lisibilité de votre code pour les développeurs à venir est un prix très lourd à payer. Je choisirais de ne pas le payer si je pouvais utiliser une usine à la place.

2
Cort Ammon

L'utilisation la plus courante de ces sous-classes à laquelle je peux penser est les hiérarchies d'exceptions, bien que ce soit un cas dégénéré où nous ne définirions généralement rien du tout dans la classe, tant que le langage nous permet d'hériter de constructeurs. Nous utilisons l'héritage pour exprimer que ReadError est un cas spécial de IOError, et ainsi de suite, mais ReadError n'a pas besoin de remplacer les méthodes de IOError.

Nous le faisons parce que les exceptions sont détectées en vérifiant leur type. Ainsi, nous devons coder la spécialisation dans le type afin que quelqu'un ne puisse attraper que ReadError sans attraper tout IOError, s'il le souhaite.

Normalement, la vérification des types de choses est très mauvaise et nous essayons de l'éviter. Si nous parvenons à l'éviter, il n'est pas nécessaire d'exprimer des spécialisations dans le système de types.

Cela peut quand même être utile, par exemple si le nom du type apparaît dans la forme de chaîne journalisée de l'objet: c'est le langage et éventuellement le framework. Vous pouvez en principe remplacer le nom du type dans un tel système par un appel à une méthode de la classe, auquel cas nous voudrions remplacer plus que le constructeur dans SystemDataHelperBuilder, nous remplacerions également loggingname(). Donc, s'il y a une telle journalisation dans votre système, vous pouvez imaginer que, bien que votre classe ne remplace littéralement que le constructeur, "moralement", elle remplace également le nom de la classe, et donc à des fins pratiques, ce n'est pas une sous-classe réservée au constructeur. Il a un comportement différent souhaitable, c'est juste que vous obtenez ce comportement gratuitement sans écrire de substitutions de méthode grâce à une utilisation astucieuse de la réflexion ailleurs.

Donc, je dirais que son code peut être une bonne idée s'il y a du code ailleurs qui vérifie en quelque sorte les instances de SystemDataHelperBuilder, soit explicitement avec une branche (comme les branches de capture d'exception basées sur le type) ou peut-être parce qu'il y en a code générique qui finira par utiliser le nom SystemDataHelperBuilder de manière utile (même s'il s'agit simplement de journalisation).

Cependant, l'utilisation de types pour des cas spéciaux entraîne une certaine confusion, car si ImmutableRectangle(1,1) et ImmutableSquare(1) se comportent différemment de toute façon alors finalement quelqu'un va se demander pourquoi et je souhaite peut-être que non. Contrairement aux hiérarchies d'exceptions, vous vérifiez si un rectangle est un carré en vérifiant si height == width, pas avec instanceof. Dans votre exemple, il y a une différence entre un SystemDataHelperBuilder et un DataHelperBuilder créé avec exactement les mêmes arguments, et cela peut entraîner des problèmes. Par conséquent, une fonction pour renvoyer un objet créé avec les "bons" arguments me semble normalement la bonne chose à faire: la sous-classe donne deux représentations différentes de "la même" chose, et cela n'aide que si vous faites quelque chose que vous normalement essayerait de ne pas faire.

Notez que je pas parle en termes de modèles de conception et d'anti-modèles, car je ne pense pas qu'il soit possible de fournir une taxonomie de toutes les bonnes et mauvaises idées auxquelles un programmeur ait jamais pensé . Ainsi, chaque idée n'est pas un modèle ou un anti-modèle, cela ne devient que lorsque quelqu'un identifie qu'elle se produit à plusieurs reprises dans différents contextes et la nomme ;-) Je ne connais aucun nom particulier pour ce type de sous-classement.

2
Steve Jessop

Un sous-type n'est jamais "mauvais" tant que vous pouvez toujours remplacer une instance de son supertype par une instance du sous-type et que tout fonctionne toujours correctement. Cela vérifie tant que la sous-classe n'essaye jamais d'affaiblir les garanties du supertype. Cela peut apporter des garanties plus solides (plus spécifiques), donc en ce sens, l'intuition de votre collègue est correcte.

Compte tenu de cela, la sous-classe que vous avez créée n'est probablement pas fausse (puisque je ne sais pas exactement ce qu'ils font, je ne peux pas le dire avec certitude.) Vous devez vous méfier du problème de la classe de base fragile, et vous devez également demandez-vous si un interface vous serait bénéfique, mais c'est vrai pour toutes les utilisations de l'héritage.

1
Doval

Je pense que la clé pour répondre à cette question est de regarder celui-ci, un scénario d'utilisation particulier.

L'héritage est utilisé pour appliquer les options de configuration de la base de données, comme la chaîne de connexion.

Il s'agit d'un anti-modèle parce que la classe viole le principe de responsabilité unique --- Elle se configure avec une source spécifique de cette configuration et non "Faire une chose et bien le faire". La configuration d'une classe ne doit pas être intégrée à la classe elle-même. Pour définir des chaînes de connexion à la base de données, la plupart du temps, j'ai vu cela se produire au niveau de l'application lors d'une sorte d'événement se produisant au démarrage de l'application.

1
Greg Burghardt

J'utilise des sous-classes de constructeur uniquement assez régulièrement pour exprimer différents concepts.

Par exemple, considérez la classe suivante:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

On peut alors créer une sous-classe:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Vous pouvez ensuite utiliser un CapitalizeAndPrintOutputter n'importe où dans votre code et vous savez exactement de quel type de sortie il s'agit. De plus, ce modèle facilite en fait l'utilisation d'un conteneur DI pour créer cette classe. Si le conteneur DI connaît PrintOutputMethod et Capitalizer, il peut même créer automatiquement le CapitalizeAndPrintOutputter pour vous.

L'utilisation de ce modèle est vraiment assez flexible et utile. Oui, vous pouvez utilisez des méthodes d'usine pour faire la même chose, mais alors (au moins en C #) vous perdez une partie de la puissance du système de type et certainement l'utilisation de conteneurs DI devient plus lourde.

1
Stephen

Permettez-moi de noter d'abord que lorsque le code est écrit, la sous-classe n'est pas réellement plus spécifique que le parent, car les deux champs initialisés sont tous les deux réglables par le code client.

Une instance de la sous-classe ne peut être distinguée qu'en utilisant un downcast.

Maintenant, si vous avez une conception dans laquelle les propriétés DatabaseEngine et ConnectionString ont été réimplémentées par la sous-classe, qui a accédé à Configuration pour ce faire, alors vous avez une contrainte qui rend plus sens d'avoir la sous-classe.

Cela dit, "anti-pattern" est trop fort à mon goût; il n'y a vraiment rien de mal ici.

1
Keith

Dans l'exemple que vous avez donné, votre partenaire a raison. Les deux générateurs d'aide aux données sont des stratégies. L'un est plus spécifique que l'autre, mais ils sont tous les deux interchangeables une fois initialisés.

Fondamentalement, si un client du datahelperbuilder devait recevoir le systemdatahelperbuilder, rien ne se briserait. Une autre option aurait été d'avoir le systemdatahelperbuilder être une instance statique de la classe datahelperbuilder.

0
Michael Brown