【问题标题】:Mechanism for Dependency Injection to Provide the Most Specific Implementation of a Generic Service Interface依赖注入机制提供最具体的通用服务接口实现
【发布时间】:2019-02-02 05:28:07
【问题描述】:

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

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> { ... }

假设我已将这些都注册到容器中。所以现在我的问题是,如果我正在遍历 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,该方法将找不到特定类的服务,因此将返回BaseEntityService。稍后,如果有人出现并为此服务添加了注册:

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

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

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

编辑:

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

【问题讨论】:

    标签: c# generics dependency-injection strategy-pattern decorator-pattern


    【解决方案1】:

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

    首先我做了一个界面:

    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&lt;IUnusualEntity&gt; 并选择它们。 (提示:从currentType中减去currentType.BaseType的接口,避免重复。)

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

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

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

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

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

    【讨论】:

      【解决方案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 pub,第 2 版即将出版)。


      更新

      我不想在我的用例中在运行时更改算法。但我确实希望能够轻松交换业务逻辑段,而无需触及它们所操作的类。

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

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


      更新 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 中不提供?

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

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

      【讨论】:

      • 我同意DoSomething 的通用实现是首选,但我不知道该怎么做。我必须定义IEntityService&lt;out T&gt;,以便我可以将所有服务放到一个列表中。 (例如new List&lt;IEntityService&lt;BaseEntity&gt;&gt;().Add(new GrandChildAEntityPolicy()) 否则不会编译。)诚然,协变和逆变规则对我来说有点神秘。也许你可以告诉我一个更好的方法? dotnetfiddle.net/8TZJu8
      • 我也同意你对设计的服务定位器部分的看法。我的想法是在原型设计之后,我可以将其实现为 IoC 库本身的功能,从而允许我执行 class ChildAEntity { public ChildAEntity(IList&lt;IEntityService&lt;ChildAEntity&gt;&gt; policies) { ... } } 之类的操作,从而无需 PolicyProvider 类和服务定位器模式。在我的用例中,我不想在运行时更改算法。但我确实希望在不触及它们所操作的类的情况下轻松交换业务逻辑部分。
      • 我之前已经研究过策略模式,虽然它很接近,但大多数示例都不能很好地与单继承相结合。 C# 中的“多重继承”式示例需要基于每个策略的大量样板代码。
      • 在你的睡眠示例中,new Bear(hibernate)new Lion(nocturnal) 如何在没有服务定位器的情况下使用 DI 完成?这就是促使我考虑允许ISleep&lt;Bear&gt;ISleep&lt;Cat&gt; 的解决方案的原因。这很容易实现,但最后一点是这两种策略可能有一些相同的逻辑。所以如果我能在运行Cat.Sleep时获得ISleep&lt;Cat&gt;ISleep&lt;Lion&gt;ISleep&lt;Animal&gt;等策略集合就好了。
      • 另一个需要考虑的用例是如果Animal 有一个方法Hunt 并且我将IFisher 接口添加到CatBear。如何在Bear.HuntCat.Hunt 中提供IHunt&lt;IFisher&gt; 而不是Lion.Hunt
      【解决方案3】:

      使用简单的注射器

      如果您碰巧使用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 现在遵守 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&lt;T&gt; : IEntityService&lt;T&gt; where T : ChildAEntityUnusualEntityService&lt;T&gt; : IEntityService&lt;T&gt; 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);
      }
      

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

      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

      【讨论】:

      • +1 非常感谢您所做的所有工作。这是非常疯狂的一周,我还没有机会看这个。
      • 好吧,我有机会通读一遍,我认为 Simple Injector 具有我正在寻找的功能。我需要一些时间来尝试一下,但您的示例看起来很有希望!
      • 我的一个想法,你能把它和接口一起使用吗?假设我向ChildBGrandChildA 添加了一个接口IUnusualEntity。我可以创建/注册一个类型 UnusualEntityService : IEntityService&lt;T&gt; where T : IUnusualEntity 并期望它由 Simple Injector 为 ChildBGrandChildA 提供吗?
      • 这需要从IEntityService&lt;T&gt; 中删除泛型类型约束——我看不出它有什么害处。也可以按原样使用 MS DI 实现。我会更新我的例子...
      • 我更新了我的示例代码来演示IUnusualEntity的用法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      • 2020-06-10
      • 2018-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多