web-dev-qa-db-fra.com

Injection de dépendances pour une bibliothèque avec des dépendances internes

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

  1. Il existe deux conteneurs Unity, un dans l'application Web et un dans le 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.
  2. RepositoryLocator est codé en dur pour l'exécution de la production. Je ne pense pas que le codage soit un péché cardinal comme le font de nombreux ingénieurs, mais je le considère avec suspicion.
  3. 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.

  4. 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?

9
John Wu

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:

  • Dans votre API, fournissez une méthode de point d'entrée initiale, qui doit être exécutée une fois par le client afin que votre bibliothèque fonctionne correctement;
  • Dans cette méthode (qui fait partie de votre bibliothèque), vous configurez le conteneur avec les classes de référentiels des fournisseurs (internes à votre bibliothèque);
  • Gardez les fournisseurs privés et vos services publics
  • C'est tout: votre client (le site Web) ne doit se soucier que d'injecter vos services, rien de plus.
  • Facultatif: vous pouvez utiliser le fichier de configuration pour lier les interfaces et les implémentations

À propos de votre question/inquiétudes:

  1. Vous (généralement) n'avez aucun contrôle sur ce que fera votre client; l'application cliente pourrait même ne pas injecter vos classes, elle pourrait simplement les utiliser immédiatement; alors ne vous en faites pas, comme dit dans les commentaires;
  2. Le fichier de configuration peut être une meilleure approche pour cela, facilement modifiable pour les environnements de production ou de test;
  3. En ce qui concerne les tests unitaires, je ne vois aucun problème avec cet attribut;
  4. Ma suggestion résout ce problème
2
Emerson Cardoso

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.

1
Scott Hannen