6

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

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

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

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

我有一些具体的实现:

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

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

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

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

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

假设的GetService方法将开始为ClassAEntityService类型提供 ,ClassA而无需任何进一步的代码更改。相反,如果有人出现并删除了所有服务,但BaseEntityServiceGetService方法将返回所有继承自BaseEntity.

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

编辑:

与@Funk 的一些讨论(见下文)和一些额外的谷歌搜索,这些讨论让我想到要查找,这让我添加了更多的流行语。似乎我正在尝试以类型安全的方式收集 DI 容器、策略模式和装饰器模式的所有优点,而不使用服务定位器模式。我开始怀疑答案是否是“使用函数式语言”。

4

3 回答 3

4

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

首先我做了一个界面:

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>()
// ...

以及注册一个看起来像这样的策略提供程序类:

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

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

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.BaseTypefrom的接口currentType以避免重复。)

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

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

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

编辑:虽然我还没有尝试过,但@xander 下面的回答似乎说明了 Simple Injector 可以提供PolicyProvider“开箱即用”的大部分行为。它仍然有少量,Service Locator但要少得多。我强烈建议在使用我的半生不熟的方法之前检查一下。:)

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

于 2019-02-05T00:32:13.207 回答
3

让我感到奇怪的第一件事是你定义

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 pub,第 2 版出来)。


更新

在我的用例中,我不想在运行时更改算法。但我确实希望在不触及它们所操作的类的情况下轻松交换业务逻辑部分。

这就是 DI 的全部意义所在。与其创建服务来管理您的所有业务逻辑(这会导致贫血域模型并且似乎有对您不利的通用差异),不如抽象您的易变依赖项(可能会更改的那些)背后和接口,并将它们注入您的类。

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

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

当然,我们几乎没有触及表面,但它对于构建可维护的“即插即用”解决方案是非常宝贵的。尽管需要转变思路,但从长远来看,明确定义依赖项将改善您的代码库。它允许您在开始分析依赖项时重新组合依赖项,并且通过这样做您甚至可以获得领域知识。


更新 2

在您的睡眠示例中new Bear(hibernate)new Lion(nocturnal)使用 DI 容器将如何完成?

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

代替自动接线

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

我们可以手工完成

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

请注意,歧义来自于有多个ISleep sleepPatterns 在起作用的事实,因此我们需要指定一种或另一种方式。

如何在 Bear.Hunt 和 Cat.Hunt 中提供 IHunt 而不是在 Lion.Hunt 中提供?

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

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

于 2019-03-01T18:43:39.057 回答
1

带简易喷油器

如果您碰巧使用Simple Injector执行 DI 任务,容器可以帮助解决这个问题。(如果您不使用 Simple Injector,请参阅下面的“使用其他 DI 框架”)

该功能在 Simple Injector 文档的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现在遵守里氏替换原则。由于服务实现提供了类型约束,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 : ChildAEntity并且UnusualEntityService<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 的。并非所有容器都能够如此优雅地处理通用注册。例如,微软的 DI 容器类似的注册失败:

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

抽象基类可以处理大部分类型检查/转换样板:

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的实现非常相似;具体来说AuthorizationHandler

于 2019-03-04T03:38:46.833 回答