Je discutais avec un collègue et nous avons fini par avoir des intuitions contradictoires quant à l'objectif du sous-classement. Mon intuition est que si l'une des fonctions principales d'une sous-classe est d'exprimer une plage limitée de valeurs possibles de son parent, il ne devrait probablement pas s'agir d'une sous-classe. Il a plaidé pour l'intuition opposée: cette sous-classe représente un objet 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 parente mais que le seul code que cette sous-classe écrase est un constructeur ce qui était vraiment nécessaire était une méthode d'assistance.
Par exemple, considérons cette classe un 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 affirme qu'il serait tout à fait approprié d'avoir une sous-classe comme celle-ci:
public class SystemDataHelperBuilder
{
public SystemDataHelperBuilder()
: base(Configuration.GetSystemDatabaseEngine(),
Configuration.GetSystemConnectionString())
{
}
}
Alors, la question:
- Parmi les personnes qui parlent de modèles de conception, laquelle de ces intuitions est correcte? Le sous-classement décrit ci-dessus est-il un anti-motif?
- Si c'est un anti-motif, quel est son nom?
Je m'excuse si cela s'avère être une réponse facile à comprendre; mes recherches sur google ont pour la plupart retourné des informations sur l'anti-motif du constructeur télescopique et pas vraiment ce que je cherchais.
la source
Réponses:
Si tout ce que vous voulez, c'est créer une classe X avec certains arguments, le sous-classement est un moyen étrange d'exprimer cette intention, car vous n'utilisez aucune des fonctionnalités offertes par les classes et l'héritage. Ce n'est pas vraiment un anti-modèle, c'est juste étrange et un peu inutile (sauf si vous avez d'autres raisons pour cela). Une méthode plus naturelle pour exprimer cette intention serait une méthode d'usine , qui dans ce cas est un nom sophistiqué pour votre "méthode d'assistance".
En ce qui concerne l'intuition générale, "plus spécifique" et "une plage limitée" sont des façons potentiellement néfastes de penser aux sous-classes, car elles impliquent toutes deux que faire de Square une sous-classe de Rectangle soit une bonne idée. Sans me fier à quelque chose de formel tel que LSP, je dirais qu'une meilleure classe est qu'une sous-classe fournit une implémentation de l'interface de base ou étend l'interface pour ajouter de nouvelles fonctionnalités.
la source
SystemDataHelperBuilder
spécifiquement.Votre collègue est correct (en supposant les 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 octetF
, vous pourriez être enclin à penser qu'ilA
a 256 valeurs légales, mais cette intuition est incorrecte.A
restreint "toute permutation de valeurs" à "le champ doitF
être compris entre 0 et 255".Si vous étendez
A
avecB
qui a un autre champ d'octetFF
, c'est ajouter des restrictions . Au lieu de "valeur oùF
est un octet", vous avez "valeur oùF
est un octet &&FF
est également un octet". Puisque vous conservez les anciennes règles, tout ce qui a fonctionné 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 de penser à une autre façon, on suppose que
A
avait un tas de sous - types:B
,C
etD
. Une variable de typeA
peut être n'importe lequel de ces sous-types, mais une variable de typeB
est plus spécifique. Il (dans la plupart des systèmes de types) ne peut pas être unC
ou unD
.Enh? Avoir des objets spécialisés est utile et ne nuit pas clairement au code. Atteindre ce résultat en utilisant le sous-typage est peut-être un peu discutable, mais dépend des outils à votre disposition. Même avec de meilleurs outils, cette mise en œuvre est simple, directe et robuste.
Je ne considérerais pas cela comme un anti-modèle car ce n’est pas clairement faux et mauvais dans aucune situation.
la source
byte and byte
définitivement plus de valeurs que le typebyte
; 256 fois plus. Cela dit, je ne pense pas que vous puissiez associer des règles à un nombre d'éléments (ou à une cardinalité si vous voulez être précis). Il tombe en panne lorsque vous considérez des ensembles infinis. Il y a autant de nombres pairs que de nombres entiers, mais savoir qu'un nombre est pair est toujours une restriction / me donne plus d'informations que le simple fait de savoir que c'est un nombre entier! On pourrait en dire autant des nombres naturels.n -> 2n
). Ce n'est pas toujours possible. vous ne pouvez pas mapper les entiers sur les réels, de sorte que l'ensemble des réels est "plus grand" que les entiers. Mais vous pouvez mapper l'intervalle (réel) [0, 1] sur l'ensemble de tous les réels, ce qui leur donne la même "taille". Les sous-ensembles sont définis comme "chaque élément deA
est également un élément deB
", précisément parce qu'une fois que vos ensembles deviennent infinis, il se peut qu'ils aient la même cardinalité mais qu'ils aient des éléments différents.Non, ce n'est pas un anti-modèle. Je peux penser à un certain nombre de cas d'utilisation pratiques pour cela:
MySQLDao
etSqliteDao
dans votre système, mais pour une raison quelconque, vous voulez vous assurer qu'une collection ne contient que des données provenant d'une source, si vous utilisez des sous-classes telles que vous les décrivez, vous pouvez demander au compilateur de vérifier cette exactitude particulière. D'autre part, si vous stockez la source de données sous forme de champ, cela devient une vérification à l'exécution.AlgorithmConfiguration
+ProductClass
etAlgorithmInstance
. En d'autres termes, si vous configurezFooAlgorithm
pourProductClass
dans le fichier de propriétés, vous ne pouvez en obtenir qu'unFooAlgorithm
pour cette classe de produits. La seule façon d'obtenir deuxFooAlgorithm
s pour une donnéeProductClass
est de créer une sous-classeFooBarAlgorithm
. Cela convient, car le fichier de propriétés est facile à utiliser (il n’est pas nécessaire de recourir à la 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.Bottom Line: Il n'y a pas vraiment de mal à faire cela. Un anti-motif est défini comme étant:
Il n'y a aucun risque d'être hautement contre-productif ici, donc ce n'est pas un anti-modèle.
la source
Si quelque chose est un motif ou un antipattern dépend de manière significative de la langue et de l'environnement dans lesquels vous écrivez. Par exemple, les
for
boucles sont un motif en assembleur, C et des langages similaires et un anti-motif en lisp.Laissez-moi vous raconter une histoire d'un code que j'ai écrit il y a longtemps ...
C'était dans un langage appelé LPC et implémenté un cadre pour lancer des sorts dans un jeu. Vous disposiez d'une super-classe de sorts, qui comportait une sous-classe de sorts de combat permettant d'effectuer certaines vérifications, puis une sous-classe pour les sorts à dégâts directs à une cible, qui étaient ensuite classés dans les sorts individuels - missile magique, éclair, etc.
Le fonctionnement de LPC était dû au fait que vous aviez un objet principal qui était un Singleton. Avant de partir, vous avez eu Singleton, ce n’était pas du tout. On n'a pas fait de copies des sorts, mais plutôt invoqué les méthodes (effectivement) statiques dans les sorts. Le code pour missile magique ressemblait à:
Et vous voyez ça? C'est une classe de constructeur seulement. Il a défini certaines valeurs qui se trouvaient dans sa classe abstraite parente (non, il ne devrait pas utiliser de séparateur - c’est une immuable) et c’est tout. Cela a bien fonctionné . C'é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 sont dans la classe abstraite dont elle hérite. Jetez un coup d'oeil sur StringBuilder en Java pour un exemple - pas seulement un constructeur, mais je suis pressé de trouver beaucoup de logique dans les méthodes elles-mêmes (tout se trouve dans AbstractStringBuilder).
Ce n'est pas une mauvaise chose, et ce n'est certainement pas un anti-modèle (et dans certaines langues, ce peut être un modèle en soi).
la source
L'utilisation la plus courante de telles sous-classes que je puisse imaginer est la hiérarchie des exceptions, bien qu'il s'agisse d'un cas dégénéré dans lequel nous ne définirions généralement rien dans la classe, tant que le langage nous permet d'hériter des constructeurs. Nous utilisons l'héritage pour exprimer qu'il
ReadError
s'agit d'un cas particulier deIOError
, etc., mais nousReadError
n'avons pas besoin de remplacer les méthodes deIOError
.Nous faisons cela parce que les exceptions sont détectées en vérifiant leur type. Ainsi, nous devons encoder la spécialisation dans le type afin que quelqu'un ne puisse attraper que
ReadError
sans tout capturerIOError
, s'il le souhaite.Normalement, vérifier les types de choses est terriblement mauvais et nous essayons de l'éviter. Si nous parvenons à l'éviter, il n'y a alors aucun besoin essentiel d'exprimer des spécialisations dans le système de types.
Cela peut néanmoins être utile, par exemple si le nom du type apparaît dans la forme de chaîne journalisée de l'objet: il s'agit d'un langage et éventuellement d'un framework. Vous pouvez en principe remplacer le nom du type dans un tel système avec un appel à une méthode de la classe, dans ce cas , nous aurions remplacer plus que le constructeur dans
SystemDataHelperBuilder
, nous avions également remplacerloggingname()
. Ainsi, s'il existe une telle journalisation dans votre système, vous pouvez imaginer que, bien que votre classe ne remplace littéralement que le constructeur, "moralement", elle redéfinit également le nom de la classe. Ainsi, à des fins pratiques, il ne s'agit pas d'une sous-classe réservée aux constructeurs. Il a un comportement différent souhaitable, ça 'Donc, je dirais que son code peut être une bonne idée s'il y a du code ailleurs qui vérifie d'une manière ou d'une autre les instances de
SystemDataHelperBuilder
, soit explicitement avec une branche (comme les branches capturant des exceptions basées sur le type) ou peut-être parce qu'il y a un code générique qui finira par en utilisant le nomSystemDataHelperBuilder
de manière utile (même s'il ne s'agit que de la journalisation).Cependant, utiliser des types pour des cas particuliers crée une certaine confusion, car si
ImmutableRectangle(1,1)
etImmutableSquare(1)
se comporter différemment de quelque manière que ce soit, on finira par se demander pourquoi et peut-être souhaiter que ce ne soit pas le cas. Contrairement aux hiérarchies d'exceptions, vous vérifiez si un rectangle est un carré en vérifiant siheight == width
, pas avecinstanceof
. Dans votre exemple, il existe une différence entre aSystemDataHelperBuilder
etDataHelperBuilder
créé avec exactement les mêmes arguments, ce qui peut poser problème. Par conséquent, une fonction permettant de retourner un objet créé avec les arguments "corrects" me semble normalement être la bonne chose à faire: la sous-classe entraîne deux représentations différentes de la "même" chose, et cela n'aide que si vous faites quelque chose que vous faites. normalement essayer de ne pas faire.Notez que je ne parle pas 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 de mauvaises idées qu'un programmeur ait jamais envisagées. Ainsi, toutes les idées ne sont pas un motif ou un anti-motif, il devient seulement que lorsque quelqu'un identifie qu'il se produit de manière répétée dans différents contextes et le nomme ;-) Je ne connais pas de nom particulier pour ce type de sous-classement.
la source
ImmutableSquareMatrix:ImmutableMatrix
, à condition qu’il existe un moyen efficace de demanderImmutableMatrix
à rendre son contenu aussi réel queImmutableSquareMatrix
possible. Même siImmutableSquareMatrix
c’était fondamentalement unImmutableMatrix
constructeur dont la hauteur et la largeur devaient correspondre, et dont laAsSquareMatrix
méthode se retournerait tout simplement (plutôt que de construire, par exemple, unImmutableSquareMatrix
même tableau de sauvegarde), une telle conception autoriserait la validation des paramètres au moment de la compilation pour les méthodes nécessitant un traitement carré. matricesSystemDataHelperBuilder
, et pas n'importe lesquellesDataHelperBuilder
, alors il est logique de définir un type pour cela (et une interface, en supposant que vous traitiez DI de cette façon) . Dans votre exemple, il existe certaines opérations qu'une matrice carrée pourrait avoir comme un déterminant, mais votre argument est valable, même si le système n'a pas besoin de celles que vous souhaitez vérifier par type. .height == width
, pas avecinstanceof
". Vous pourriez préférer quelque chose que vous pouvez affirmer statiquement.foo=new ImmutableMatrix(someReadableMatrix)
est plus clair que celui desomeReadableMatrix.AsImmutable()
ou mêmeImmutableMatrix.Create(someReadableMatrix)
, mais ces dernières formes offrent des possibilités sémantiques utiles; si le constructeur peut dire que la matrice est carrée, il est possible de refléter son type, ce qui pourrait être plus propre que d'exiger du code en aval qui nécessite une matrice carrée pour créer une nouvelle instance d'objet.new
opérateur. Une classe est juste un objet appelable qui, lorsqu'il est appelé, renvoie (par défaut) une instance d'elle-même. Donc, si vous voulez préserver la cohérence de l'interface, il est parfaitement possible d'écrire une fonction fabrique dont le nom commence par une majuscule. Elle ressemble donc à un appel du constructeur. Non pas que je le fasse régulièrement avec les fonctions d'usine, mais c'est là quand vous en avez besoin.La définition la plus stricte, orientée objet, d'une relation de sous-classe est appelée «est-a». En utilisant l'exemple traditionnel d'un carré et d'un rectangle, un carré «est un rectangle».
Par là, vous devriez utiliser le sous-classement pour toute situation dans laquelle vous pensez qu'un «est-un» est une relation significative pour votre code.
Dans votre cas spécifique de sous-classe avec uniquement un constructeur, vous devriez l’analyser dans une perspective «is-a». Il est clair qu'un SystemDataHelperBuilder «est un» DataHelperBuilder, donc le premier passage suggère que son utilisation est 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 devriez conserver la sous-classe.
Cependant, si aucun autre code ne le fait, alors vous avez une relationhp qui pourrait être une relation «est-une» ou qui pourrait être implémentée avec une fabrique. Dans ce cas, vous pouvez aller dans les deux sens, mais je recommanderais une relation Usine plutôt qu’une relation «est-ce». Lors de l'analyse de code orienté objet écrit par une autre personne, la hiérarchie des classes est une source d'informations importante. Il fournit les relations «is-a» dont ils auront besoin pour comprendre le code. En conséquence, le fait de placer ce comportement dans une sous-classe favorise le comportement de "quelque chose que quelqu'un trouvera s'il approfondit les méthodes de la superclasse" à "quelque chose que tout le monde examinera avant même de se plonger dans le code". Citant Guido van Rossum, "le code est lu plus souvent qu'il n'est écrit", la baisse de la lisibilité de votre code pour les développeurs à venir est donc un lourd prix à payer. Je choisirais de ne pas payer si je pouvais utiliser une usine.
la source
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. Ceci vérifie tant que la sous-classe n'essaye jamais d'affaiblir les garanties offertes par le supertype. Cela peut donner des garanties plus fortes (plus spécifiques), de sorte que l'intuition de votre collègue est correcte.
Cela dit, la sous-classe que vous avez créée n’est probablement pas fausse (étant donné que 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
interface
demandez-vous si cela vous serait bénéfique, mais c'est le cas de toutes les utilisations de l'héritage.la source
Je pense que la solution pour répondre à cette question consiste à examiner 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 modèle anti car la classe enfreint le principe de responsabilité unique: elle se configure avec une source spécifique de cette configuration et non pas "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 les chaînes de connexion à la base de données, cela a souvent été le cas au niveau de l'application au cours d'un événement quelconque lors du démarrage de l'application.
la source
J'utilise assez régulièrement des sous-classes constructeurs pour exprimer différents concepts.
Par exemple, considérons la classe suivante:
Nous pouvons ensuite créer une sous-classe:
Vous pouvez ensuite utiliser un CapitalizeAndPrintOutputter n’importe où dans votre code et vous savez exactement quel type de sortie il s’agit. De plus, ce modèle facilite 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.
L'utilisation de ce modèle est vraiment très flexible et utile. Oui, vous pouvez utiliser les méthodes d'usine pour faire la même chose, mais vous perdez (du moins en C #) une partie de la puissance du système de typographie et l'utilisation des conteneurs DI devient de plus en plus lourde.
la source
Permettez-moi de noter d’abord que lors de l’écriture du code, la sous-classe n’est pas plus spécifique que le parent, car les deux champs initialisés sont tous deux réglables par le code client.
Une instance de la sous-classe ne peut être distinguée qu'à l'aide d'un downcast.
Maintenant, si vous avez une conception dans laquelle les propriétés
DatabaseEngine
etConnectionString
ont été réimplémentées par la sous-classe à laquelleConfiguration
vous avez accédé , vous avez une contrainte qui rend plus logique la présence de la sous-classe.Cela dit, "anti-pattern" est trop fort à mon goût; il n'y a rien de vraiment faux ici.
la source
Dans l'exemple que vous avez donné, votre partenaire a raison. Les deux constructeurs d'assistants de données sont des stratégies. L'un est plus spécifique que l'autre, mais ils sont tous deux interchangeables une fois initialisés.
Fondamentalement, si un client du datahelperbuilder devait recevoir le systemdatahelperbuilder, rien ne se casserait. Une autre option aurait été de faire en sorte que systemdatahelperbuilder soit une instance statique de la classe datahelperbuilder.
la source