6
votes

Mécanisme d'injection de dépendances pour fournir l'implémentation la plus spécifique d'une interface de service générique

J'ai l'impression d'avoir joué au bingo à la mode avec le titre. Voici un exemple concis de ce que je demande. Disons que j'ai une hiérarchie d'héritage pour certaines entités.

class ClassAEntityService : IEntityService<ChildAEntity> { ... }

Maintenant, disons que j'ai une interface générique pour un service avec une méthode qui utilise la classe de base:

var entities = List<BaseEntity>();
// ...
foreach(var entity in entities)
{
    // Get the most specific service?
    var service = GetService(entity.GetType()); // Maybe?
    service.DoSomething(entity);
}

J'ai quelques implémentations concrètes:

class BaseEntityService : IEntityService<BaseEntity> { ... }
class GrandChildAEntityService : IEntityService<GrandChildAEntity> { ... }
class ChildBEntityService : IEntityService<ChildBEntity> { ... }

Supposons que je les ai toutes enregistrées avec le conteneur. Alors maintenant, ma question est de savoir si je parcoure une Liste de BaseEntity comment obtenir le service enregistré avec la correspondance la plus proche?

interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }


0 commentaires

3 Réponses :


4
votes

J'ai donc pu lancer quelque chose qui faisait ce dont j'avais besoin.

J'ai d'abord créé une interface:

// policyResults = [
//    { "GrandChildAEntityPolicy", "ChildAEntityPolicy", "BaseEntityPolicy" },
//    { "BaseEntityPolicy" },
//    { "ChildBEntityPolicy", "BaseEntityPolicy" }, 
//    { "ChildAEntityPolicy", "BaseEntityPolicy" }
// ];

Ensuite, j'ai fait quelques implémentations: p >

var entities = new List<BaseEntity> { 
    new GrandChildAEntity(),
    new BaseEntity(),
    new ChildBEntity(),
    new ChildAEntity() };
var policyResults = entities
    .Select(entity => policyProvider
        .GetPolicies<IEntityPolicy<BaseEntity>>(entity.GetType())
        .Select(policy => policy.GetPolicyResult(entity)))
    .ToList();
// policyResults = [
//    { "GrandChildAEntityPolicy", "BaseEntityPolicy" },
//    { "BaseEntityPolicy" },
//    { "ChildBEntityPolicy", "BaseEntityPolicy" }, 
//    { "BaseEntityPolicy" }
// ];

J'ai enregistré chacun d'eux.

var grandChild = new GrandChildAEntity();
var policyResults = policyProvider
    .GetPolicies<IEntityPolicy<BaseEntity>>(grandChild.GetType())
    .Select(x => x.GetPolicyResult(x));
// policyResults == { "GrandChildAEntityPolicy", "BaseEntityPolicy" }

En plus d'enregistrer une classe de fournisseur de règles qui ressemble à ceci: p >

public class PolicyProvider : IPolicyProvider
{
    // constructor and container injection...

    public List<T> GetPolicies<T>(Type entityType)
    {
        var results = new List<T>();
        var currentType = entityType;
        var serviceInterfaceGeneric = typeof(T).GetGenericDefinition();

        while(true)
        {
            var currentServiceInterface = serviceInterfaceGeneric.MakeGenericType(currentType);
            var currentService = container.GetService(currentServiceInterface);
            if(currentService != null)
            {
                results.Add(currentService)
            }
            currentType = currentType.BaseType;
            if(currentType == null)
            {
                break;
            }
        }
        return results;
    }
}

Cela me permet de faire ce qui suit:

// ...
.AddSingleton<IEntityPolicy<BaseEntity>, BaseEntityPolicy>()
.AddSingleton<IEntityPolicy<GrandChildAEntity>, GrandChildAEntityPolicy>()
.AddSingleton<IEntityPolicy<ChildBEntity>, ChildBEntityPolicy>()
// ...

Plus important encore, je peux le faire sans connaître la sous-classe particulière. p >

public class BaseEntityPolicy : IEntityPolicy<BaseEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(BaseEntityPolicy); }
}
public class GrandChildAEntityPolicy : IEntityPolicy<GrandChildAEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(GrandChildAEntityPolicy); }
}
public class ChildBEntityPolicy: IEntityPolicy<ChildBEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(ChildBEntityPolicy); }
}

J'ai développé un peu ce point pour permettre aux politiques de fournir une valeur ordinale si nécessaire et j'ai ajouté une certaine mise en cache à l'intérieur de GetPolicies afin qu'il ne soit pas nécessaire de construire la collection à chaque fois. J'ai également ajouté une logique qui me permet de définir des politiques d'interface IUnusualEntityPolicy: IEntityPolicy et de les récupérer également. (Astuce: soustrayez les interfaces de currentType.BaseType de currentType pour éviter la duplication.)

(Il convient de mentionner que l'ordre de List n'est pas garanti, j'ai donc utilisé autre chose dans ma propre solution. Pensez à faire de même avant d'utiliser ceci.)

Je ne sais toujours pas si c'est quelque chose qui existe déjà ou s'il existe un terme pour mais cela fait que la gestion des politiques d'entité se sent découplée d'une manière gérable. Par exemple, si j'enregistrais un ChildAEntityPolicy: IEntityPolicy mes résultats deviendraient automatiquement:

public interface IEntityPolicy<T>
{
    string GetPolicyResult(BaseEntity entity);
}

EDIT: Bien que Je ne l'ai pas encore essayé, la réponse de @ xander ci-dessous semble illustrer que Simple Injector peut fournir une grande partie du comportement du PolicyProvider "prêt à l'emploi". Il y a encore une petite quantité de Service Locator mais beaucoup moins. Je recommande vivement de vérifier cela avant d'utiliser mon approche à moitié cuite. :)

EDIT 2: Ma compréhension des dangers entourant un localisateur de service est qu'il rend vos dépendances un mystère. Cependant, ces stratégies ne sont pas des dépendances, ce sont des modules complémentaires facultatifs et le code doit s'exécuter, qu'elles aient été enregistrées ou non. En ce qui concerne les tests, cette conception sépare la logique d'interprétation de la somme des résultats des politiques et la logique des politiques elles-mêmes.


0 commentaires

3
votes

La première chose qui me semble étrange est que vous définissiez

container.Register( 
    typeof(Bear), 
    () => new Bear(hibernate));

au lieu de

container.Register( 
    typeof(IZoo), 
    typeof(Zoo));

tout en fournissant des implémentations différentes pour chacun T.

Dans une hiérarchie bien conçue, DoSomething (entité BaseEntity) ne devrait pas avoir à changer sa fonctionnalité en fonction du type réel (dérivé).

Si tel est le cas, vous pouvez extraire la fonctionnalité en suivant le principe de ségrégation de l'interface .

Si la fonctionnalité est vraiment que em > dépendant du sous-type, peut-être que l'interface DoSomething () appartient aux types eux-mêmes.

Si vous souhaitez modifier les algorithmes à l'exécution, il existe également le modèle de stratégie , mais même dans ce cas, les implémentations concrètes ne sont pas censées être modifiées si souvent (c'est-à-dire lors de l'itération d'une liste) .

Sans plus d'informations sur votre conception et ce que vous essayez d'accomplir, il est difficile de fournir des conseils supplémentaires. Veuillez ref:

Notez que Service Locator est considéré comme un anti-pattern. Le seul but d'un conteneur DI devrait être de composer le graphe d'objets au démarrage (dans la racine de la composition).

Quant à une bonne lecture, si vous aimez cuisiner, il y a Injection de dépendances dans .NET em> (Manning pub, 2e éd à paraître).


MISE À JOUR

Je ne souhaite pas modifier les algorithmes lors de l'exécution dans mon cas d'utilisation. Mais je veux qu'il soit facile d'échanger des segments de logique métier sans toucher aux classes sur lesquelles ils opèrent.

C'est la raison d'être de DI. Au lieu de créer des services pour gérer toute votre logique métier - ce qui se traduit par un modèle de domaine anémique et semble avoir une variance générique qui travaille contre vous - il est avantageux d'abstraire vos dépendances volatiles - celles susceptibles de changer - derrière et d'interfacer, et de les injecter dans vos classes.

L'exemple ci-dessous utilise l'injection de constructeur. p>

public interface ISleep { void Sleep(); }

class Nocturnal : ISleep { public void Sleep() => Console.WriteLine("NightOwl"); }
class Hibernate : ISleep { public void Sleep() => Console.WriteLine("GrizzlyBear"); }

public abstract class Animal
{
    private readonly ISleep _sleepPattern;

    public Animal(ISleep sleepPattern)
    {
        _sleepPattern = sleepPattern ?? throw new NullReferenceException("Can't sleep");
    }

    public void Sleep() => _sleepPattern.Sleep();
}

public class Lion : Animal
{
    public Lion(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Cat : Lion
{
    public Cat(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Bear : Animal
{
    public Bear(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Program
{
    public static void Main()
    {
        var nocturnal = new Nocturnal();
        var hibernate = new Hibernate();

        var animals = new List<Animal>
        {
            new Lion(nocturnal),
            new Cat(nocturnal),
            new Bear(hibernate)
        };

        var Garfield = new Cat(hibernate);
        animals.Add(Garfield);

        animals.ForEach(a => a.Sleep());
    }
}

Bien sûr, nous avons à peine effleuré la surface, mais c'est inestimable pour créer des solutions «plug and play» maintenables. Bien que cela nécessite un changement d'esprit, la définition explicite de vos dépendances améliorera votre base de code à long terme. Il vous permet de recomposer vos dépendances lorsque vous commencez à les analyser, et ce faisant, vous pouvez même acquérir des connaissances sur le domaine.


MISE À JOUR 2

Dans votre exemple de sommeil, comment le nouvel ours (hibernation) et le nouveau lion (nocturne) seraient-ils réalisés en utilisant un conteneur DI?

Les abstractions rendent le code flexible pour le changement. Ils introduisent des coutures dans le graphique d'objets, de sorte que vous pouvez facilement implémenter d'autres fonctionnalités plus tard. Au démarrage, le conteneur DI est rempli et invité à créer le graphe d'objets. À ce moment-là, le code est compilé, il n'y a donc aucun mal à spécifier des classes concrètes si l'abstraction de support est trop vague. Dans notre cas, nous voulons spécifier l'argument ctor. Rappelez-vous, les coutures sont là, pour le moment, nous construisons simplement le graphique.

Au lieu du câblage automatique

interface IEntityService<T> where T : BaseEntity { void DoSomething(T entity)... }

Nous pouvons le faire à la main

interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }

Notez que l'ambiguïté vient du fait qu'il y a plusieurs ISleep sleepPattern en jeu, nous devons donc spécifier d'une manière ou d'une autre.

Comment puis-je fournir IHunt dans Bear.Hunt et Cat.Hunt mais pas Lion.Hunt?

L'héritage ne sera jamais l'option la plus flexible. C'est pourquoi la composition est souvent privilégiée, pour ne pas dire que vous devez abandonner chaque hiérarchie, mais soyez conscient des frictions en cours de route. Dans le livre que j'ai mentionné, il y a un chapitre entier sur l'interception, il explique comment utiliser le motif du décorateur pour décorer dynamiquement une abstraction avec de nouvelles capacités.

En fin de compte, le je veux que le conteneur choisisse l'approche la plus proche de la hiérarchie ne me semble pas juste. Bien que cela puisse sembler pratique, je préfère configurer le conteneur correctement.


6 commentaires

Je suis d'accord que l'implémentation générique de DoSomething serait préférable mais je ne savais pas comment le faire. J'ai dû définir IEntityService afin de pouvoir rassembler tous les services dans une seule liste. (Par exemple, new List > (). Add (new GrandChildAEntityPolicy ()) ne se compilera pas autrement.) Certes, les règles de covariance et de contravariance sont un peu un mystère pour moi. Peut-être pouvez-vous me montrer un meilleur moyen? dotnetfiddle.net/8TZJu8


Je suis également d'accord avec vous sur la partie localisateur de service de la conception. Ma pensée était qu'après le prototypage, je pourrais implémenter cela en tant que fonctionnalité de la bibliothèque IoC elle-même me permettant de faire des choses comme class ChildAEntity {public ChildAEntity (IList > politiques) {...}} supprimant ainsi le besoin de la classe PolicyProvider et du modèle Service Locator. Je ne souhaite pas modifier les algorithmes lors de l'exécution dans mon cas d'utilisation. Mais je veux qu'il soit facile d'échanger des segments de logique métier sans toucher aux classes sur lesquelles ils opèrent.


J'ai déjà examiné le modèle de stratégie et même s'il s'en rapproche, la plupart des exemples ne correspondent pas bien à l'héritage unique. Les exemples «d'héritage multiple» en C # nécessitent beaucoup de code standard pour chaque stratégie.


Dans votre exemple de sommeil, comment new Bear (hibernate) et new Lion (nocturne) seraient-ils réalisés en utilisant DI sans localisateur de service? C'est ce qui m'a amené à envisager une solution qui permettrait ISleep et ISleep . C'est assez facile à réaliser, mais le dernier élément est que les deux stratégies peuvent avoir une logique identique. Ce serait donc bien si je pouvais obtenir une collection de stratégies telles que ISleep , ISleep et ISleep lors de l'exécution de Cat.Sleep


Un autre cas d'utilisation à considérer est si Animal a une méthode Hunt et j'ajoute l'interface IFisher à Cat et < code> Ours . Comment fournir IHunt dans Bear.Hunt et Cat.Hunt mais pas dans Lion.Hunt ?


Correction, j'aurais dû demander: "comment serait [...] accompli en utilisant un conteneur DI?"



1
votes

Avec Simple Injector

Si vous utilisez Simple Injector pour les fonctions DI, le conteneur peut vous aider. (Si vous n'utilisez pas Simple Injector, reportez-vous à la section «Avec d'autres cadres DI» ci-dessous)

La fonctionnalité est décrite dans la documentation de Simple Injector, sous Scénarios avancés: mélange de collections d'open -composants génériques et non génériques .

Vous devrez apporter un léger ajustement à votre interface de service et à vos implémentations.

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService), typeof(ChildAEntityService), typeof(GrandChildAEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService), typeof(ChildBEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService), typeof(BaseEntityService) })]
public void Test4(Type entityType, Type[] expectedServiceTypes)
{
    // Services appear to be resolved in reverse order of registration, but
    // I'm not sure if this behavior is guaranteed.
    var serviceProvider = new ServiceCollection()
        .AddTransient<IEntityService, UnusualEntityService>()
        .AddTransient<IEntityService, ChildAEntityService>()
        .AddTransient<IEntityService, ChildBEntityService>()
        .AddTransient<IEntityService, GrandChildAEntityService>()
        .AddTransient<IEntityService, BaseEntityService>()
        .AddTransient<EntityServiceFactory>() // this should have an interface, but I omitted it to keep the example concise
        .BuildServiceProvider();

    // Don't get hung up on this line--it's part of the test, not the solution.
    BaseEntity entity = (dynamic)Activator.CreateInstance(entityType);

    var entityServices = serviceProvider
        .GetService<EntityServiceFactory>()
        .GetServices(entity);

    Assert.Equal(
        expectedServiceTypes,
        entityServices.Select(s => s.GetType())
    );
}

Les services sont désormais génériques, avec une contrainte de type décrivant le type d'entité le moins spécifique qu'ils sont capables de gérer. En prime, DoSomething adhère désormais au principe de substitution de Liskov. Puisque les implémentations de service fournissent des contraintes de type, l'interface IEntityService n'en a plus besoin.

Enregistrez tous les services comme une seule collection de génériques ouverts. Simple Injector comprend les contraintes de type générique. Lors de la résolution, le conteneur filtrera essentiellement la collection vers les seuls services pour lesquels la contrainte de type est satisfaite.

Voici un exemple de travail, présenté comme une xUnit test.

class EntityServiceFactory
{
    readonly IServiceProvider serviceProvider;

    public EntityServiceFactory(IServiceProvider serviceProvider)
        => this.serviceProvider = serviceProvider;

    public IEnumerable<IEntityService> GetServices(BaseEntity entity)
        => serviceProvider
            .GetServices<IEntityService>()
            .Where(s => s.CanHandle(entity));
}

Similaire à votre exemple, vous pouvez ajouter ChildAEntityService : IEntityService où T: ChildAEntity et UnusualEntityService : IEntityService où T: IUnusualEntity et tout fonctionne bien ...

class BaseEntityService : GenericEntityService<BaseEntity>
{
    protected override void DoSomethingImpl(BaseEntity entity) => throw new NotImplementedException();
}

class ChildBEntityService : GenericEntityService<ChildBEntity>
{
    protected override void DoSomethingImpl(ChildBEntity entity) => throw new NotImplementedException();
}


5 commentaires

+1 Merci beaucoup pour tout ce travail. C'était une semaine assez folle et je n'ai pas encore eu l'occasion de regarder ça.


D'accord, j'ai eu l'occasion de lire et je pense que Simple Injector a la fonctionnalité que je recherche. Cela me prendra du temps pour l'essayer mais votre exemple semble très prometteur!


Une pensée que j'ai, seriez-vous capable de l'utiliser également avec des interfaces? Disons que j'ai ajouté une interface IUnusualEntity à ChildB et GrandChildA . Puis-je créer / enregistrer un type UnusualEntityService: IEntityService où T: IUnusualEntity et m'attendre à ce qu'il soit fourni par Simple Injector pour ChildB et GrandChildA ?


Cela nécessiterait de supprimer la contrainte de type générique de IEntityService - je n'y vois aucun mal. C'est également possible avec l'implémentation MS DI en l'état. Je vais mettre à jour mes exemples ...


J'ai mis à jour mon exemple de code pour démontrer l'utilisation de IUnusualEntity .