web-dev-qa-db-fra.com

Existe-t-il une alternative à l'injection bâtarde? (Injection de l'homme pauvre AKA via le constructeur par défaut)

Je suis le plus souvent tenté d'utiliser "l'injection bâtarde" dans quelques cas. Quand j'ai un constructeur "correct" d'injection de dépendance:

public class ThingMaker {
    ...
    public ThingMaker(IThingSource source){
        _source = source;
    }

Mais alors, pour les classes que j'ai l'intention de API publiques (classes que d'autres équipes de développement consommeront), je ne peux jamais trouver une meilleure option que d'écrire un constructeur "bâtard" par défaut avec le plus probable nécessaire dépendance:

    public ThingMaker() : this(new DefaultThingSource()) {} 
    ...
}

L'inconvénient évident ici est que cela crée une dépendance statique sur DefaultThingSource; dans l'idéal, il n'y aurait pas une telle dépendance, et le consommateur injecterait toujours ce IThingSource qu'il voulait. Cependant, c'est trop difficile à utiliser; les consommateurs veulent renouveler un ThingMaker et se mettre au travail pour créer des choses, puis des mois plus tard, injecter autre chose lorsque le besoin s'en fait sentir. Cela ne laisse que quelques options à mon avis:

  1. Omettez le constructeur bâtard; forcer le consommateur de ThingMaker à comprendre IThingSource, à comprendre comment ThingMaker interagit avec IThingSource, à trouver ou à écrire une classe concrète, puis à injecter une instance dans leur appel de constructeur.
  2. Omettez le constructeur bâtard et fournissez une usine, un conteneur ou une autre classe/méthode d'amorçage distincte; faire comprendre au consommateur qu'il n'a pas besoin d'écrire son propre IThingSource; forcer le consommateur de ThingMaker à trouver et à comprendre l'usine ou le bootstrapper et à l'utiliser.
  3. Gardez le constructeur bâtard, permettant au consommateur de "renouveler" un objet et de l'exécuter, et de faire face à la dépendance statique facultative de DefaultThingSource.

Boy, # 3 semble sûr attrayant. Y a-t-il une autre meilleure option? # 1 ou # 2 ne semblent pas en valoir la peine.

115
Patrick Szalapski

Pour autant que je comprends, cette question concerne la façon d'exposer une API faiblement couplée avec des valeurs par défaut appropriées. Dans ce cas, vous pouvez avoir une bonne valeur par défaut locale , auquel cas la dépendance peut être considérée comme facultative. Une façon de gérer les dépendances facultatives consiste à utiliser Injection de propriété au lieu de Injection de constructeur - en fait, c'est en quelque sorte le scénario de l'affiche pour l'injection de propriété.

Cependant, le véritable danger de l'injection bâtarde est lorsque la valeur par défaut est une valeur par défaut étrangère , car cela signifierait que le constructeur par défaut traîne le long d'un couplage indésirable avec le Assemblage implémentant la valeur par défaut. Si je comprends bien cette question, cependant, le défaut prévu proviendrait de la même Assemblée, auquel cas je ne vois aucun danger particulier.

Dans tous les cas, vous pourriez également envisager une façade comme décrit dans l'une de mes réponses précédentes: bibliothèque "conviviale" de Dependency Inject (DI)

BTW, la terminologie utilisée ici est basée sur le langage de modèle de mon livre .

60
Mark Seemann

Mon compromis est un tour sur @BrokenGlass:

1) Le constructeur unique est un constructeur paramétré

2) Utilisez la méthode d'usine pour créer un ThingMaker et passez cette source par défaut.

public class ThingMaker {
  public ThingMaker(IThingSource source){
    _source = source;
  }

  public static ThingMaker CreateDefault() {
    return new ThingMaker(new DefaultThingSource());
  }
}

Évidemment, cela n'élimine pas votre dépendance, mais cela me rend plus clair que cet objet a des dépendances dans lesquelles un appelant peut plonger profondément s'il le souhaite. Vous pouvez rendre cette méthode d'usine encore plus explicite si vous le souhaitez (CreateThingMakerWithDefaultThingSource) si cela aide à la compréhension. Je préfère cela à la priorité sur la méthode d'usine IThingSource car elle continue de favoriser la composition. Vous pouvez également ajouter une nouvelle méthode d'usine lorsque le DefaultThingSource est obsolète et avoir un moyen clair de trouver tout le code à l'aide du DefaultThingSource et de le marquer pour être mis à niveau.

Vous avez couvert les possibilités dans votre question. Classe d'usine ailleurs pour plus de commodité ou de commodité au sein de la classe elle-même. La seule autre option peu attrayante serait basée sur la réflexion, cachant encore plus la dépendance.

32
Steve Jackson

Une alternative consiste à avoir une méthode d'usineCreateThingSource() dans votre classe ThingMaker qui crée la dépendance pour vous.

Pour les tests ou si vous avez besoin d'un autre type de IThingSource, vous devrez alors créer une sous-classe de ThingMaker et remplacer CreateThingSource() pour renvoyer le type concret que vous souhaitez. Évidemment, cette approche n'en vaut la peine que si vous devez principalement être en mesure d'injecter la dépendance pour les tests, mais pour la plupart/toutes les autres utilisations, vous n'avez pas besoin d'un autre IThingSource

8
BrokenGlass

Si vous devez avoir une dépendance "par défaut", également connue sous le nom d'Injection de dépendance du pauvre, alors vous devez initialiser et "câbler" la dépendance quelque part.

Je garderai les deux constructeurs mais aurai une usine juste pour l'initialisation.

public class ThingMaker
{
    private IThingSource _source;

    public ThingMaker(IThingSource source)
    {
        _source = source;
    }

    public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
    {
    }
}

Maintenant, en usine, créez l'instance par défaut et autorisez le remplacement de la méthode:

public class ThingFactory
{
    public virtual IThingSource CreateThingSource()
    {
        return new DefaultThingSource();
    }
}

Mise à jour:

Pourquoi utiliser deux constructeurs: Deux constructeurs montrent clairement comment la classe est destinée à être utilisée. Le constructeur sans paramètre déclare: créez simplement une instance et la classe exécutera toutes ses responsabilités. Maintenant, le deuxième constructeur indique que la classe dépend de IThingSource et fournit un moyen d'utiliser une implémentation différente de celle par défaut.

Pourquoi utiliser une usine: 1- Discipline: La création de nouvelles instances ne devrait pas faire partie des responsabilités de cette classe, une classe d'usine est plus appropriée. 2- DRY: Imaginez que dans la même API, d'autres classes dépendent également de IThingSource et font de même. Remplacez une fois que la méthode d'usine renvoyant IThingSource et toutes les classes de votre API commencent automatiquement à utiliser la nouvelle instance.

Je ne vois aucun problème à coupler ThingMaker à une implémentation par défaut d'IThingSource tant que cette implémentation a du sens pour l'API dans son ensemble et vous fournissez également des moyens de remplacer cette dépendance à des fins de test et d'extension.

6
JCallico

Je vote pour # 3. Vous vous faciliterez la vie - et celle d'autres développeurs -.

6
Fernando

Les exemples généralement liés à ce style d'injection sont souvent extrêmement simplistes: "dans le constructeur par défaut de la classe B, appelez un constructeur surchargé avec new A() et soyez sur votre chemin!"

La réalité est que les dépendances sont souvent extrêmement complexes à construire. Par exemple, que faire si B a besoin d'une dépendance non-classe comme une connexion à une base de données ou un paramètre d'application? Vous liez ensuite la classe B à la System.Configuration espace de noms, augmentant sa complexité et son couplage tout en diminuant sa cohérence, le tout pour encoder des détails qui pourraient simplement être externalisés en omettant le constructeur par défaut.

Ce style d'injection indique au lecteur que vous avez reconnu les avantages de la conception découplée mais que vous ne souhaitez pas vous y engager. Nous savons tous que lorsque quelqu'un verra ce constructeur par défaut juteux, facile et à faible friction, il l'appellera, quelle que soit la rigidité de son programme à partir de ce moment. Ils ne peuvent pas comprendre la structure de leur programme sans lire le code source de ce constructeur par défaut, ce qui n'est pas une option lorsque vous distribuez simplement les assemblys. Vous pouvez documenter les conventions de nom de chaîne de connexion et de clé de paramètres d'application, mais à ce stade, le code ne se suffit pas à lui-même et il incombe au développeur de rechercher la bonne incantation.

L'optimisation du code pour que ceux qui l'écrivent puissent s'en tirer sans comprendre ce qu'ils disent est une chanson de sirène, un anti-modèle qui conduit finalement à perdre plus de temps à démêler la magie que de gagner du temps dans l'effort initial. Soit découpler ou pas; garder un pied dans chaque motif diminue la concentration des deux.

2
Bryan Watts

Vous n'êtes pas satisfait de l'impureté OO de cette dépendance, mais vous ne dites pas vraiment quel problème cela cause finalement.

  • ThingMaker utilise-t-il DefaultThingSource d'une manière non conforme à IThingSource? Non.
  • Pourrait-il arriver un moment où vous seriez obligé de retirer le constructeur sans paramètre? Étant donné que vous êtes en mesure de fournir une implémentation par défaut à ce moment, peu probable.

Je pense que le plus gros problème ici est le choix du nom, pas l'opportunité d'utiliser la technique.

2
Amy B

Pour ce que ça vaut, tout le code standard que j'ai vu dans Java le fait comme ceci:

public class ThingMaker  {
    private IThingSource  iThingSource;

    public ThingMaker()  {
        iThingSource = createIThingSource();
    }
    public virtual IThingSource createIThingSource()  {
        return new DefaultThingSource();
    }
}

Quiconque ne veut pas d'un objet DefaultThingSource peut remplacer createIThingSource. (Si possible, l'appel à createIThingSource serait ailleurs que par le constructeur.) C # n'encourage pas à remplacer comme Java le fait, et il pourrait ne pas être aussi évident qu'il le ferait être dans Java que les utilisateurs peuvent et peut-être devraient fournir leur propre implémentation IThingSource. (Ni aussi évident comment pour le fournir.) Je suppose que # 3 est le chemin à parcourir, mais je pensais que je voudrais mentionner cela.

1
RalphChapin

Juste une idée - peut-être un peu plus élégante mais malheureusement ne se débarrasse pas de la dépendance:

  • supprimer le "constructeur bâtard"
  • dans le constructeur standard, vous définissez le paramètre source par défaut sur null
  • alors vous vérifiez que la source est nulle et si c'est le cas, vous lui affectez "new DefaultThingSource ()" otherweise quoi que le consommateur injecte
0
Yahia

Avoir une fabrique interne (interne à votre bibliothèque) qui mappe DefaultThingSource à IThingSource, qui est appelée à partir du constructeur par défaut.

Cela vous permet de "renouveler" la classe ThingMaker sans paramètres ni aucune connaissance d'IThingSource et sans dépendance directe sur DefaultThingSource.

0

Je supporte l'option # 1, avec une extension: faire de DefaultThingSource une classe publique. Votre formulation ci-dessus implique que DefaultThingSource sera caché aux consommateurs publics de l'API, mais si je comprends bien votre situation, il n'y a aucune raison de ne pas exposer la valeur par défaut. De plus, vous pouvez facilement documenter le fait qu'en dehors de circonstances spéciales, une new DefaultThingSource() peut toujours être passée à ThingMaker.

0
JSBձոգչ

Pour les API vraiment publiques, je gère généralement cela en utilisant une approche en deux parties:

  1. Créez un assistant dans l'API pour permettre à un consommateur d'API d'enregistrer les implémentations d'interface "par défaut" de l'API avec le conteneur IoC de son choix.
  2. S'il est souhaitable de permettre au consommateur d'API d'utiliser l'API sans son propre conteneur IoC, hébergez un conteneur facultatif dans l'API qui est rempli avec les mêmes implémentations "par défaut".

La partie vraiment délicate ici est de décider quand activer le conteneur # 2, et la meilleure approche de choix dépendra fortement de vos consommateurs d'API.

0
Nicole Calinoiu