依赖关系注入以提供通用服务接口的最特定实现的机制

Spe*_*ort 6 c# generics dependency-injection strategy-pattern decorator-pattern

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

class BaseEntity { ... }
class ChildAEntity : BaseEntity { ... }
class GrandChildAEntity : ChildAEntity { ... }
class ChildBEntity : BaseEntity { ... }
Run Code Online (Sandbox Code Playgroud)

现在让我们说说我为服务提供了一个通用接口,该接口具有使用基类的方法:

interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }
Run Code Online (Sandbox Code Playgroud)

我有一些具体的实现:

class BaseEntityService : IEntityService<BaseEntity> { ... }
class GrandChildAEntityService : IEntityService<GrandChildAEntity> { ... }
class ChildBEntityService : IEntityService<ChildBEntity> { ... }
Run Code Online (Sandbox Code Playgroud)

假设我已经将所有这些都注册到了容器中。所以,现在我的问题是,如果我迭代通过ListBaseEntity?我如何获得注册的服务与最接近的匹配?

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

我想做的是建立一种机制,这样,如果实体具有ClassA该方法的类型,则该类将找不到任何服务,因此将返回BaseEntityService。稍后,如果有人出现并为该服务添加了注册:

class ClassAEntityService : IEntityService<ChildAEntity> { ... }
Run Code Online (Sandbox Code Playgroud)

假设GetService方法将开始提供ClassAEntityService用于ClassA类型,而不需要任何进一步的代码改变。相反,如果有人来了并只是删除了所有服务,BaseEntityService则该GetService方法将为继承自的所有类返回该服务BaseEntity

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

编辑:

与@Funk进行的一些讨论(请参阅下文)以及这些讨论引起的其他Google搜索使我认为查找起来使我为此添加了更多流行语。似乎我正在尝试以一种类型安全的方式而不使用服务定位器模式来收集DI容器,策略模式和装饰器模式的所有优点。我开始怀疑答案是否是“使用功能语言”。

Spe*_*ort 4

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

首先我做了一个界面:

public interface IEntityPolicy<T>
{
    string GetPolicyResult(BaseEntity entity);
}
Run Code Online (Sandbox Code Playgroud)

然后我做了一些实现:

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); }
}
Run Code Online (Sandbox Code Playgroud)

我对他们每个人都进行了注册。

// ...
.AddSingleton<IEntityPolicy<BaseEntity>, BaseEntityPolicy>()
.AddSingleton<IEntityPolicy<GrandChildAEntity>, GrandChildAEntityPolicy>()
.AddSingleton<IEntityPolicy<ChildBEntity>, ChildBEntityPolicy>()
// ...
Run Code Online (Sandbox Code Playgroud)

以及注册一个策略提供者类,如下所示:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这允许我执行以下操作:

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

更重要的是,我可以在不知道特定子类的情况下执行此操作。

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" }
// ];
Run Code Online (Sandbox Code Playgroud)

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

(值得一提的是,不能保证 的顺序List,因此我在自己的解决方案中使用了其他内容。在使用它之前请考虑执行相同的操作。)

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

// policyResults = [
//    { "GrandChildAEntityPolicy", "ChildAEntityPolicy", "BaseEntityPolicy" },
//    { "BaseEntityPolicy" },
//    { "ChildBEntityPolicy", "BaseEntityPolicy" }, 
//    { "ChildAEntityPolicy", "BaseEntityPolicy" }
// ];
Run Code Online (Sandbox Code Playgroud)

编辑:虽然我还没有尝试过,但@xander 下面的答案似乎说明简单注入器可以提供PolicyProvider“开箱即用”的大部分行为。仍然有一点点Service Locator,但已经少了很多。我强烈建议您在使用我的半生不熟的方法之前先检查一下。:)

编辑 2:我对服务定位器周围危险的理解是,它使您的依赖项变得神秘。然而,这些策略不是依赖项,它们是可选的附加组件,无论它们是否已注册,代码都应该运行。在测试方面,这种设计将解释策略总结果的逻辑与策略本身的逻辑分开。