依赖注入机制以提供通用服务接口的最具体实现

Mechanism for Dependency Injection to Provide the Most Specific Implementation of a Generic Service Interface

我觉得我和标题玩了流行语宾果游戏。这是我要问的一个简明示例。假设我有一些实体的继承层次结构。

class BaseEntity { ... }
class ChildAEntity : BaseEntity { ... }
class GrandChildAEntity : ChildAEntity { ... }
class ChildBEntity : BaseEntity { ... }

现在假设我有一个服务的通用接口,其方法使用基础 class:

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

我有一些具体的实现:

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

假设我已经将这些都注册到容器中。所以现在我的问题是,如果我正在遍历 BaseEntityList 如何获得最匹配的注册服务?

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

我想做的是建立一种机制,如果实体的类型为 ClassA,该方法将找不到特定 class 的服务,因此 return BaseEntityService。稍后如果有人出现并为此服务添加了注册:

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

假设的 GetService 方法将开始为 ClassA 类型提供 ClassAEntityService,而不需要任何进一步的代码更改。相反,如果有人出现并删除了除 BaseEntityService 之外的所有服务,那么 GetService 方法将 return 对于所有继承自 BaseEntity 的 classes。

我很确定我可以滚动一些东西,即使我使用的 DI 容器不直接支持它。我在这里落入陷阱了吗?这是反模式吗?

编辑:

与@Funk 的一些讨论(见下文)和一些额外的 Google 搜索这些讨论让我想到要查找,这让我为此添加了更多流行语。看起来我正在尝试以类型安全的方式收集 DI 容器、策略模式和装饰器模式的所有优点,而不使用服务定位器模式。我开始怀疑答案是否是 "Use a Functional Language."

所以我能够推出满足我需要的东西。

首先我做了一个界面:

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

然后我做了几个实现:

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); }
}

我都注册了。

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

以及注册一个看起来像这样的策略提供者class:

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;
    }
}

这允许我执行以下操作:

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

更重要的是,我可以在不知道特定子class的情况下做到这一点。

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" }
// ];

我对此进行了一些扩展,以允许策略在必要时提供序号值,并在 GetPolicies 中添加了一些缓存,因此不必每次都构建集合。我还添加了一些逻辑,允许我定义接口策略 IUnusualEntityPolicy : IEntityPolicy<IUnusualEntity> 并选择它们。 (提示:从 currentType 中减去 currentType.BaseType 的接口以避免重复。)

(值得一提的是 List 的顺序是不能保证的,所以我在自己的解决方案中使用了其他东西。在使用这个之前考虑做同样的事情。)

仍然不确定这是否已经存在或者是否有一个术语,但它使管理实体策略以一种可管理的方式感觉分离。例如,如果我注册了 ChildAEntityPolicy : IEntityPolicy<ChildAEntity>,我的结果将自动变为:

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

编辑: 虽然我还没有尝试过,@xander 下面的回答似乎说明了简单注入器可以提供 PolicyProvider "out of the box"。仍然有少量 Service Locator 到它,但要少得多。我强烈建议在使用我不成熟的方法之前检查一下。 :)

编辑 2: 我对服务定位器的危险的理解是它使您的依赖关系成为一个谜。然而,这些策略不是依赖项,它们是可选的附加组件,代码应该 运行 无论它们是否已注册。关于测试,这种设计将解释策略总和结果的逻辑与策略本身的逻辑分开。

首先让我感到奇怪的是你定义

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

而不是

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

而您仍然为每个 T.

提供不同的实现

在设计良好的层次结构中,DoSomething(BaseEntity entity) 不必根据实际(派生)类型更改其功能。

如果是这种情况,您可以按照接口隔离原则.

提取功能

如果功能确实 子类型相关,那么 DoSomething() 接口可能属于类型本身。

如果您想在 运行 时更改算法,也可以使用 策略模式 ,但即便如此,具体实现也不会经常更改(即在迭代列表时)。

如果没有关于您的设计和您想要完成的目标的更多信息,就很难提供进一步的指导。请参考:

请注意 服务定位器 被认为是一种反模式。 DI 容器的唯一目的应该是在启动时(在组合根中)组合对象图。

至于好书,如果你喜欢烹饪,有 .NET 中的依赖注入(Manning 酒吧,第二版即将出版)。


更新

I don't want to change algorithms at runtime in my use case. But I do want it to be easy to swap out segments of business logic without touching the classes they operate on.

这就是 DI 的意义所在。与其创建服务来管理你所有的业务逻辑——这会导致 Anemic Domain Model 并且似乎有通用的差异对你不利——它值得抽象你的不稳定的依赖关系——那些可能会改变的——在后面和接口,并注入那些进入你的 类.

下面的示例使用构造函数注入。

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());
    }
}

当然,我们只是触及表面,但它对于构建可维护的 "plug and play" 解决方案来说是无价的。虽然这需要转变思维,但显式定义您的依赖项将在很长一段时间内改进您的代码库 运行。它允许您在开始分析依赖项时重构它们,通过这样做您甚至可以获得领域知识。


更新 2

In your sleep example how would new Bear(hibernate) and new Lion(nocturnal) be accomplished using a DI Container?

抽象使代码可以灵活更改。他们在对象图中引入了接缝,因此您以后可以轻松实现其他功能。在启动时,DI 容器被填充并被要求构建对象图。那时,代码已编译,因此如果支持抽象太模糊,指定具体的 类 也没什么坏处。在我们的例子中,我们想要指定 ctor 参数。请记住,接缝就在那里,此时我们只是在构建图形。

而不是自动接线

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

我们可以手工完成

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

请注意,歧义来自于多个 ISleep sleepPattern 正在发挥作用,因此我们需要指定一种或另一种方式。

How do I provide IHunt in Bear.Hunt and Cat.Hunt but not Lion.Hunt?

继承永远不会是最灵活的选择。这就是组合经常受到青睐的原因,并不是说您应该放弃每个层次结构,而是要注意沿途的摩擦。在我提到的书中有一整章是关于拦截的,它解释了如何使用装饰器模式来动态地装饰具有新功能的抽象。

最后,我希望容器选择层次结构中最接近的匹配项 方法对我来说并不合适。虽然这看起来很方便,但我更愿意正确设置容器。

带简单注射器

如果您碰巧使用 Simple Injector 执行 DI 任务,容器可以提供帮助。 (如果您没有使用 Simple Injector,请参阅下面的 "With Other DI Frameworks,")

简单注入器文档中描述了该功能,在 Advanced Scenarios: Mixing collections of open-generic and non-generic components 下。

您需要对服务接口和实现稍作调整。

interface IEntityService<T>
{
    void DoSomething(T entity);
}

class BaseEntityService<T> : IEntityService<T> where T : BaseEntity
{
    public void DoSomething(T entity) => throw new NotImplementedException();
}

class ChildBEntityService<T> : IEntityService<T> where T : ChildBEntity
{
    public void DoSomething(T entity) => throw new NotImplementedException();
}

服务现在是通用的,具有描述它们能够处理的最不具体实体类型的类型约束。作为奖励,DoSomething 现在遵守 Liskov 替换原则。由于服务实现提供类型约束,IEntityService 接口不再需要一个。

将所有服务注册为一个开放泛型集合。 Simple Injector 理解泛型类型约束。解析时,容器实质上会将集合过滤为仅满足类型约束的那些服务。

这是一个工作示例,作为 xUnit 测试呈现。

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(BaseEntityService<ChildAEntity>) })]
public void Test1(Type entityType, Type[] expectedServiceTypes)
{
    var container = new Container();

    // Services will be resolved in the order they were registered
    container.Collection.Register(typeof(IEntityService<>), new[] {
        typeof(ChildBEntityService<>),
        typeof(GrandChildAEntityService<>),
        typeof(BaseEntityService<>),
    });

    container.Verify();

    var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);

    Assert.Equal(
        expectedServiceTypes,
        container.GetAllInstances(serviceType).Select(s => s.GetType())
    );
}

与您的示例类似,您可以添加 ChildAEntityService<T> : IEntityService<T> where T : ChildAEntityUnusualEntityService<T> : IEntityService<T> where T : IUnusualEntity 并且一切正常...

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService<GrandChildAEntity>), typeof(ChildAEntityService<GrandChildAEntity>), typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService<ChildBEntity>), typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService<ChildAEntity>), typeof(BaseEntityService<ChildAEntity>) })]
public void Test2(Type entityType, Type[] expectedServiceTypes)
{
    var container = new Container();

    // Services will be resolved in the order they were registered
    container.Collection.Register(typeof(IEntityService<>), new[] {
        typeof(UnusualEntityService<>),
        typeof(ChildAEntityService<>),
        typeof(ChildBEntityService<>),
        typeof(GrandChildAEntityService<>),
        typeof(BaseEntityService<>),
    });

    container.Verify();

    var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);

    Assert.Equal(
        expectedServiceTypes,
        container.GetAllInstances(serviceType).Select(s => s.GetType())
    );
}

正如我之前提到的,此示例特定于 Simple Injector。并非所有容器都能如此优雅地处理通用注册。例如,类似的注册失败 Microsoft's DI container:

[Fact]
public void Test3()
{
    var services = new ServiceCollection()
        .AddTransient(typeof(IEntityService<>), typeof(BaseEntityService<>))
        .AddTransient(typeof(IEntityService<>), typeof(GrandChildAEntityService<>))
        .AddTransient(typeof(IEntityService<>), typeof(ChildBEntityService<>))
        .BuildServiceProvider();

    // Exception message: System.ArgumentException : GenericArguments[0], 'GrandChildBEntity', on 'GrandChildAEntityService`1[T]' violates the constraint of type 'T'.
    Assert.Throws<ArgumentException>(
        () => services.GetServices(typeof(IEntityService<ChildBEntity>))
    );
}

与其他 DI 框架

我设计了一个替代解决方案,应该适用于任何 DI 容器。

这一次,我们从接口中删除泛型类型定义。相反,CanHandle() 方法会让调用者知道实例是否可以处理给定的实体。

interface IEntityService
{
    // Indicates whether or not the instance is able to handle the entity.
    bool CanHandle(object entity);
    void DoSomething(object entity);
}

抽象基础 class 可以处理大部分 type-checking/casting 样板文件:

abstract class GenericEntityService<T> : IEntityService
{
    // Indicates that the service can handle an entity of typeof(T),
    // or of a type that inherits from typeof(T).
    public bool CanHandle(object entity)
        => entity != null && typeof(T).IsAssignableFrom(entity.GetType());

    public void DoSomething(object entity)
    {
        // This could also throw an ArgumentException, although that
        // would violate the Liskov Substitution Principle
        if (!CanHandle(entity)) return;

        DoSomethingImpl((T)entity);
    }

    // This is the method that will do the actual processing
    protected abstract void DoSomethingImpl(T entity);
}

这意味着实际的服务实现可以非常简单,例如:

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();
}

要将它们从 DI 容器中取出,您需要一个友好的工厂:

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));
}

最后,为了证明一切正常:

[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())
    );
}

由于涉及转换,我认为这不如 Simple Injector 实现优雅。不过,它仍然很不错,而且这种模式有一些先例。它与 MVC Core 的 Policy-Based Authorization; specifically AuthorizationHandler.

的实现非常相似