J'ai de nombreuses classes de base qui nécessitent ISessionContext de la base de données, IlogManager pour le journal et l'iservice utilisé pour communiquer avec un autre service. Je souhaite utiliser une injection de dépendance pour cette classe utilisée par toutes les classes principales.
J'ai deux possibilités de mise en œuvre. La classe principale qui accepte IAMBientContext avec toutes les trois catégories ou injectez pour toutes les catégories des 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 ambientContext)
public MyCoreClass(ISessionContext sessionContext, ILogManager logManager, IService service)
{
...
}
Quelle est la meilleure solution dans ce cas?
"Best" est beaucoup trop subjectif ici. Comme c'est commun avec de telles décisions, c'est un compromis entre deux manières également valables de réaliser quelque chose.
Si vous créez un AmbientContext
et injectez cela dans de nombreuses classes, vous fournissez potentiellement plus d'informations à chacune d'elles que nécessaire (par exemple, la classe Foo
ne peut utiliser que ISessionContext
, mais On dit à propos de ILogManager
et ISession
aussi).
Si vous passez chacun dans via un paramètre, vous indiquez à chaque classe à quelles seules les choses qu'il a besoin de savoir. Mais, le nombre de paramètres peut rapidement se développer et vous trouverez peut-être beaucoup trop de constructeurs et de méthodes avec de nombreux paramètres hautement répétés, qui pourraient être simplifiés via une classe de contexte.
C'est donc un cas d'équilibrage des deux et de choisir le approprié pour votre situation. Si vous n'avez qu'une seule classe et trois paramètres, je ne me dérangerais personnellement pas avec AmbientContext
. Pour moi, le point de basculement serait probablement quatre paramètres. Mais c'est une opinion pure. Votre point de basculement sera probablement différent de la mienne, alors allez avec ce qui vous convient.
La terminologie de la question ne correspond pas vraiment à l'exemple du code. Les Ambient Context
est un motif utilisé pour accroître une dépendance de n'importe quelle classe dans tout module aussi simple que possible, sans polluer toutes les catégories d'accepter l'interface de la dépendance, mais en gardant toujours l'idée d'inversion du contrôle. De telles dépendances sont généralement dédiées à la journalisation, à la sécurité, à la gestion de la session, aux transactions, à la mise en cache, à l'audit de toute préoccupation transversale dans cette application. C'est en quelque sorte gênant d'ajouter un ILogging
, ISecurity
, ITimeProvider
aux constructeurs et la plupart du temps, toutes les classes n'ont pas besoin en même temps, alors je comprends votre besoin.
Et si la durée de vie de l'instance ISession
est différente du ILogger
un? Peut-être que l'instance d'isession doit être créée sur chaque demande et l'iLogger une fois. Ainsi, avoir toutes ces dépendances régies par un objet qui n'est pas le conteneur lui-même ne cherche pas le bon choix en raison de tous ces problèmes avec la gestion de la vie et la localisation et d'autres décrits dans ce fil.
Le IAmbientContext
dans la question ne résout pas le problème de ne pas polluer chaque constructeur. Vous devez toujours l'utiliser dans la signature du constructeur, sûr, une seule fois cette fois.
Le moyen le plus simple n'est donc pas utilisé d'injection de constructeur ni d'un autre mécanisme d'injection pour faire face aux dépendances transversales, mais à l'aide d'un appel statique . Nous voyons réellement ce modèle assez souvent, mis en œuvre par le cadre lui-même. Vérifier thread.CurrentPrincipal qui est une propriété statique qui renvoie une implémentation de l'interface IPrincipal
. Il est également réglable afin que vous puissiez modifier la mise en œuvre si vous le souhaitez, ainsi que vous ne l'êtes pas couplé.
MyCore
regarde 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 des implémentations possibles ont été décrits en détail par Mark Semelann dans ce article . Il pourrait y avoir des implémentations qui s'appuient sur le conteneur IOC lui-même que vous utilisez.
Vous voulez éviter AmbientContext.Current.Logger
, AmbientContext.Current.Session
Pour les mêmes raisons que décrites ci-dessus.
Mais vous avez d'autres options pour résoudre ce problème: utilisez des décorateurs, une interception dynamique si votre conteneur a cette capacité ou un AOP. Le contexte ambiant devrait être le dernier recours en raison du fait que ses clients cachent leurs dépendances à travers cela. J'utiliserais toujours le contexte ambiant si l'interface imite vraiment mon impulsion d'utiliser une dépendance statique comme DateTime.Now
ou ConfigurationManager.AppSettings
Et ce besoin augmente assez souvent. Mais à la fin, l'injection du constructeur pourrait ne pas être une mauvaise idée d'obtenir ces dépendances omniprésentes.
J'éviterais le AmbientContext
.
Premièrement, si la classe dépend de AmbientContext
alors vous ne savez pas réellement ce que cela fait. Vous devez examiner son utilisation de cette dépendance pour déterminer lequel de ses dépendances imbriquées utilisées. Vous ne pouvez pas non plus regarder le nombre de dépendances et dire si votre classe en fait trop parce que l'une de ces dépendances pourrait réellement représenter plusieurs dépendances imbriquées.
Deuxièmement, si vous l'utilisez pour éviter de multiples dépendances du constructeur, cette approche encouragera d'autres développeurs (y compris vous-même) pour ajouter de nouveaux membres à cette classe de contexte ambiante. Ensuite, le premier problème est composé.
Troisièmement, se moquer de la dépendance sur AmbientContext
est plus difficile parce que vous devez déterminer dans chaque cas s'il faut se moquer de tous ses membres ou simplement ceux dont vous avez besoin, puis créez une simule qui retourne ces moqueurs (ou Test double.) Cela rend votre unité teste plus difficile à écrire, à lire et à entretenir.
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 la nommer en fonction de ce que cela 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.
Le second (sans l'emballage d'interface)
Sauf si il y a une interaction entre les différents services qui nécessitent de l'encapsulation dans une classe intermédiaire, il ne complique que votre code et limite la flexibilité lorsque vous introduisez l'interface des interfaces ".