Contexte
Je travaille sur une bibliothèque de classe à l'appui d'un site Web. La bibliothèque combine les API associées de plusieurs fournisseurs différents, qui ont tous leurs propres nuances et objets de domaine particuliers. Par exemple, imaginez qu'il existe un fournisseur de cartes de crédit qui expose un objet BankAccount
, un fournisseur de prêts qui expose un Account
et un système de compte courant expose un AccountDto
. Le principal objectif de conception de la bibliothèque est de masquer ces différentes implémentations et de permettre au site Web de fonctionner avec un seul concept de Account
, avec un ensemble de services qui décident quel fournisseur appeler et comment coordonner les données à partir de les différentes sources.
Pour masquer la confusion et garder l'API propre, seuls mes propres objets et services de domaine sont public
. À l'intérieur de la bibliothèque, chaque fournisseur possède ses propres référentiel et objets de domaine. Les classes du fournisseur sont toutes marquées internal
pour empêcher les développeurs Web de contourner la couche de services (intentionnellement ou non).
Donc, le flux va comme ceci:
Site Web >> Mon API >> Couche de service (publique) >> Couche de référentiel (interne)
Cette solution utilise l'injection de dépendances et le conteneur Unity IoC. Les services de mes API sont enregistrés dans la racine de composition et injectés directement dans les contrôleurs des sites Web en tant qu'arguments constructeurs. Tout cela fonctionne bien.
Le problème
Voici le problème avec lequel je me bats. Mes services ont des arguments constructeurs qui permettent d'y injecter les référentiels. Mais les référentiels sont internal
donc le site web ne peut pas les enregistrer dans la racine de la composition (sans faire quelque chose de vraiment bizarre avec Reflection). De plus, je ne veux pas que les développeurs de sites Web se soucient de les injecter.
La question est donc la suivante: comment injecter les référentiels internes/privés dans la couche de service public, tout en respectant les conventions acceptées dans une approche DI?
Ma solution proposée
La façon dont je fais cela en ce moment est la suivante:
J'ai ajouté une interface et une classe abstraites RepositoryLocator
à la bibliothèque. Il a son propre conteneur Unity. Il n'est pas scellé afin qu'un projet de test unitaire puisse dériver sa propre version et remplacer les référentiels de stub.
public interface IRepositoryLocator
{
T GetRepository<T>() where T : IRepository;
}
public abstract class BaseRepositoryLocator : IRepositoryLocator
{
protected readonly UnityContainer _container = new UnityContainer();
public virtual T GetRepository<T>() where T : IRepository
{
return _container.Resolve<T>();
}
}
Ensuite, j'ai ajouté un DefaultRepositoryLocator
à ma bibliothèque qui est codé en dur pour fournir les référentiels d'exécution de production.
public sealed class DefaultRepositoryLocator : BaseRepositoryLocator
{
public DefaultRepositoryLocator()
{
RegisterDefaultTypes();
}
private void RegisterDefaultTypes()
{
_container.RegisterType<IVendorARepository, VendorARepository>();
_container.RegisterType<IVendorBRepository, VendorBRepository>();
_container.RegisterType<IVendorCRepository, VendorCRepository>();
}
}
Ensuite, dans mes classes de services, au lieu d'injecter des référentiels, j'injecte le référentiel locator, et je récupère les référentiels nécessaires dans le constructeur.
public class AccountService : IAccountService
{
internal readonly IVendorARepository _vendorA;
internal readonly IVendorBRepository _vendorB;
public AccountService(IRepositoryLocator repositoryLocator)
{
_vendorA = repositoryLocator.GetRepository<IVendorARepository>();
_vendorB = repositoryLocator.GetRepository<IVendorBRepository>();
}
}
La seule tâche de l'équipe de développement Web
Dans la racine de composition du site Web, il leur suffit d'enregistrer le localisateur de référentiel par défaut dans leur conteneur Unity.
container.RegisterType<IRepositoryLocator, DefaultRepositoryLocator>();
Une fois cela fait, le conteneur Unity injecte le localisateur de référentiel dans tous les services qui sont injectés dans les contrôleurs, et les services peuvent récupérer les référentiels dont ils ont besoin.
Test unitaire
Dans un projet de test unitaire, l'assembly de test unitaire aura accès aux membres internes à l'aide de l'attribut InternalsVisibleTo , et le projet de test substituerait son propre StubRepositoryLocator
conforme à l'interne mais -interfaces désormais publiques.
Choses gênantes à propos de cette conception
Alors, vous tous experts IoC, quel genre de problèmes ai-je créé? Voici ce qui m'inquiète
RepositoryLocator
de la bibliothèque. J'entends qu'il ne devrait vraiment y en avoir qu'un, mais j'ai également lu que c'est un anti-modèle pour le faire circuler.Le projet de test unitaire aura besoin de InternalsVisibleToAttribute ajouté à la bibliothèque. C'est définitivement une odeur de code et je me demande comment nous pourrions y faire face si le projet de test unitaire change de nom ou s'il doit y en avoir plusieurs.
Les constructeurs de mes services prennent une injection pour le RepositoryLocator
lui-même, au lieu d'injections séparées pour les référentiels individuels. Cela masque les dépendances individuelles au moment de la compilation.
Existe-t-il une approche qui résout ces problèmes et est cohérente avec les meilleures pratiques IoC, tout en cachant la couche de référentiel derrière l'étendue internal
?
Dans la racine de composition du site Web, il leur suffit d'enregistrer le localisateur de référentiel par défaut
Le site Web de l'OMI n'a même pas besoin de connaître vos référentiels. Comme vous l'avez dit, les classes repos et vendors sont internes, donc votre site ne devrait pas se soucier d'y accéder ou de les injecter quelque part. Encore une fois, comme vous l'avez dit, c'est le rôle de votre bibliothèque d'abstraire plusieurs fournisseurs et d'exposer une API plus simple à l'utilisateur (qui est le site Web dans votre exemple).
Cela dit, je pense que vous devriez:
À propos de votre question/inquiétudes:
Vous pouvez utiliser une extension de conteneur . Cela permet à l'assembly contenant des classes internes de configurer ses propres dépendances plutôt que d'exiger que le consommateur les connaisse et les configure.
La documentation le montre comme ceci:
IUnityContainer container = new UnityContainer();
container.AddNewExtension<MyCustomExtension>();
Le consommateur ajoute simplement l'extension, et cette classe d'extension gère les détails.