依赖注入机制以提供通用服务接口的最具体实现
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> { ... }
假设我已经将这些都注册到容器中。所以现在我的问题是,如果我正在遍历 BaseEntity
的 List
如何获得最匹配的注册服务?
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 : 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。并非所有容器都能如此优雅地处理通用注册。例如,类似的注册失败 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
.
的实现非常相似
我觉得我和标题玩了流行语宾果游戏。这是我要问的一个简明示例。假设我有一些实体的继承层次结构。
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> { ... }
假设我已经将这些都注册到容器中。所以现在我的问题是,如果我正在遍历 BaseEntity
的 List
如何获得最匹配的注册服务?
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)
andnew 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 : 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。并非所有容器都能如此优雅地处理通用注册。例如,类似的注册失败 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
.