contexte ambiant vs injection de constructeur

9

J'ai de nombreuses classes de base qui nécessitent ISessionContext de la base de données, ILogManager pour le journal et IService utilisé pour communiquer avec d'autres services. Je veux utiliser l'injection de dépendances pour cette classe utilisée par toutes les classes principales.

J'ai deux implémentations possibles. La classe principale qui accepte IAmbientContext avec les trois classes ou injecte pour toutes les classes les trois classes.

public interface ISessionContext 
{
    ...
}

public class MySessionContext: ISessionContext 
{
    ...
}

public interface ILogManager 
{

}

public class MyLogManager: ILogManager 
{
    ...
}

public interface IService 
{
    ...
}

public class MyService: IService
{
    ...
}

Première solution:

public class AmbientContext
{
    private ISessionContext sessionContext;
    private ILogManager logManager;
    private IService service;

    public AmbientContext(ISessionContext sessionContext, ILogManager logManager, IService service)
    {
        this.sessionContext = sessionContext;
        this.logManager = logManager;
        this.service = service;
    }
}


public class MyCoreClass(AmbientContext ambientContext)
{
    ...
}

deuxième solution (sans contexte ambiant)

public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
    ...
}

Quelle est la meilleure solution dans ce cas?

user3401335
la source
Qu'est-ce qui est " IServiceutilisé pour communiquer avec un autre service?" Si IServicereprésente une vague dépendance vis-à-vis d'autres services, cela ressemble à un localisateur de services et ne devrait pas exister. Votre classe doit dépendre d'interfaces qui décrivent explicitement ce que leur consommateur en fera. Aucune classe n'a jamais besoin d'un service pour donner accès à un service. Une classe a besoin d'une dépendance qui fait quelque chose de spécifique dont la classe a besoin.
Scott Hannen

Réponses:

4

"Best" est beaucoup trop subjectif ici. Comme cela est courant avec de telles décisions, il s'agit d'un compromis entre deux façons également valables de réaliser quelque chose.

Si vous créez un AmbientContextet qui injectent dans de nombreuses classes, vous êtes potentiellement fournir davantage d' informations à chacun d'eux que nécessaire (par exemple , la classe Foone peut utiliser ISessionContext, mais qui est dit au sujet ILogManageret ISessionaussi).

Si vous passez chacun via un paramètre, vous ne dites à chaque classe que les choses dont elle a besoin. Mais, le nombre de paramètres peut rapidement augmenter et vous pouvez trouver que vous avez beaucoup trop de constructeurs et de méthodes avec de nombreux paramètres très répétés, qui pourraient être simplifiés via une classe de contexte.

Il s'agit donc d'équilibrer les deux et de choisir celui qui convient à votre situation. Si vous n'avez qu'une seule classe et trois paramètres, je ne m'embêterais pas personnellement AmbientContext. Pour moi, le point de basculement serait probablement de quatre paramètres. Mais c'est une pure opinion. Votre point de basculement sera probablement différent du mien, alors allez-y avec ce qui vous convient.

David Arno
la source
4

La terminologie de la question ne correspond pas vraiment à l'exemple de code. Le Ambient Contextest un modèle utilisé pour récupérer une dépendance de n'importe quelle classe dans n'importe quel module aussi facilement que possible, sans polluer chaque classe pour accepter l'interface de la dépendance, mais en gardant toujours l'idée d'inversion de contrôle. De telles dépendances sont généralement dédiées à la journalisation, à la sécurité, à la gestion de session, aux transactions, à la mise en cache, à l'audit, ainsi à tout problème transversal dans cette application. Il est en quelque sorte gênant d'ajouter un ILogging, ISecurity, ITimeProvideraux constructeurs et la plupart du temps toutes les classes ont besoin tout en même temps, donc je comprends votre besoin.

Et si la durée de vie de l' ISessioninstance est différente de ILoggercelle? Peut-être que l'instance ISession devrait être créée à chaque demande et l'ILogger une fois. Donc, avoir toutes ces dépendances régies par un objet qui n'est pas le conteneur lui-même ne semble pas le bon choix en raison de tous ces problèmes avec la gestion et la localisation de la durée de vie et d'autres décrits dans ce fil.

La IAmbientContextquestion ne résout pas le problème de ne pas polluer tous les constructeurs. Vous devez toujours l'utiliser dans la signature du constructeur, bien sûr, une seule fois cette fois.

Donc, le moyen le plus simple n'est PAS d'utiliser l'injection de constructeur ou tout autre mécanisme d'injection pour gérer les dépendances transversales, mais en utilisant un appel statique . Nous voyons en fait ce modèle assez souvent, mis en œuvre par le cadre lui-même. Vérifiez Thread.CurrentPrincipal qui est une propriété statique qui renvoie une implémentation de l' IPrincipalinterface. Il est également paramétrable pour que vous puissiez modifier l'implémentation si vous le souhaitez, donc vous n'y êtes pas associé.

MyCore ressemble maintenant à quelque chose comme

public class MyCoreClass
{
    public void BusinessFeature(string data)
    {
        LoggerContext.Current.Log(data);

        _repository.SaveProcessedData();

        SessionContext.Current.SetData(data);
        ...etc
    }
}

Ce modèle et les implémentations possibles ont été décrits en détail par Mark Seemann dans cet article . Il peut y avoir des implémentations qui dépendent du conteneur IoC lui-même que vous utilisez.

Vous souhaitez éviter AmbientContext.Current.Logger, AmbientContext.Current.Sessionpour les mêmes raisons que celles décrites ci-dessus.

Mais vous avez d'autres options pour résoudre ce problème: utilisez des décorateurs, l'interception dynamique si votre conteneur a cette capacité ou AOP. Le contexte ambiant devrait être le dernier recours car ses clients cachent leurs dépendances à travers lui. J'utiliserais toujours le contexte ambiant si l'interface imite vraiment mon impulsion à utiliser une dépendance statique comme DateTime.Nowou ConfigurationManager.AppSettingset ce besoin augmente assez souvent. Mais à la fin, l'injection de constructeur pourrait ne pas être une si mauvaise idée pour obtenir ces dépendances omniprésentes.

Adrian Iftode
la source
3

J'éviterais le AmbientContext.

Premièrement, si la classe en dépend, AmbientContextvous ne savez pas vraiment ce qu'elle fait. Vous devez regarder son utilisation de cette dépendance pour déterminer laquelle de ses dépendances imbriquées il utilise. Vous ne pouvez pas non plus regarder le nombre de dépendances et dire si votre classe en fait trop, car l'une de ces dépendances peut en fait représenter plusieurs dépendances imbriquées.

Deuxièmement, si vous l'utilisez pour éviter plusieurs dépendances de constructeur, cette approche encouragera d'autres développeurs (vous y compris) à ajouter de nouveaux membres à cette classe de contexte ambiant. Ensuite, le premier problème est aggravé.

Troisièmement, se moquer de la dépendance AmbientContextest plus difficile parce que vous devez déterminer dans chaque cas s'il faut se moquer de tous ses membres ou uniquement ceux dont vous avez besoin, puis configurer une maquette qui renvoie ces simulations (ou teste des doublons). votre unité teste plus difficile à écrire, à lire et à maintenir.

Quatrièmement, il manque de cohésion et viole le principe de responsabilité unique. C'est pourquoi il a un nom comme "AmbientContext", car il fait beaucoup de choses sans rapport et il n'y a aucun moyen de le nommer en fonction de ce qu'il fait.

Et il viole probablement le principe de ségrégation d'interface en introduisant des membres d'interface dans des classes qui n'en ont pas besoin.

Scott Hannen
la source
2

Le second (sans le wrapper d'interface)

Sauf s'il existe une certaine interaction entre les différents services qui doit être encapsulée dans une classe intermédiaire, cela ne fait que compliquer votre code et limite la flexibilité lorsque vous introduisez l '«interface des interfaces»

Ewan
la source