我觉得我用标题玩了流行语宾果游戏。这是我要问的简明例子。假设我有一些实体的继承层次结构。
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> { ... }
假设我已经用容器注册了这些。所以现在我的问题是,如果我正在迭代List
的BaseEntity
,我如何获得最接近匹配的注册服务?
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
,而无需进行任何进一步的代码更改。相反,如果有人出现并删除了除BaseEntityService
之外的所有服务,那么GetService
方法将返回继承自BaseEntity
的所有类。
即使我使用的DI容器不直接支持它,我也很确定我可以滚动一些东西。我在这里陷入困境吗?这是反模式吗?
编辑:
与@Funk的一些讨论(见下文)和一些额外的谷歌搜索使我想到的那些讨论让我更加流行语。看起来我正在尝试以类型安全的方式收集DI容器,策略模式和装饰器模式的所有优点,而不使用服务定位器模式。我开始怀疑答案是“使用功能语言”。
所以我能够做出我所需要的东西。
首先我做了一个界面:
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.BaseType
中减去currentType
的接口以避免重复。)
(值得一提的是List
的顺序不能保证,所以我在自己的解决方案中使用了其他东西。在使用之前考虑做同样的事情。)
仍然不确定这是否已经存在或者是否有一个术语,但它使管理实体政策感觉以可管理的方式分离。例如,如果我注册了ChildAEntityPolicy : IEntityPolicy<ChildAEntity>
,我的结果将自动变为:
// policyResults = [
// { "GrandChildAEntityPolicy", "ChildAEntityPolicy", "BaseEntityPolicy" },
// { "BaseEntityPolicy" },
// { "ChildBEntityPolicy", "BaseEntityPolicy" },
// { "ChildAEntityPolicy", "BaseEntityPolicy" }
// ];
编辑:虽然我还没有尝试过,@ xander下面的答案似乎说明Simple Injector可以提供“开箱即用”的PolicyProvider
的大部分行为。还有少量的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中的Dependency Injection(曼宁酒吧,第二版出版)。
UPDATE
在我的用例中,我不想在运行时更改算法。但我确实希望在不触及他们操作的类的情况下更换业务逻辑部分变得容易。
这就是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
在您的睡眠示例中,如何使用DI容器完成
new Bear(hibernate)
和new Lion(nocturnal)
?
抽象使代码变得灵活。他们在对象图中引入了接缝,因此您可以在以后轻松实现其他功能。在启动时,将填充DI容器并要求构建对象图。那时,代码被编译,因此如果支持抽象太模糊,那么指定具体类没有坏处。在我们的例子中,我们想要指定ctor参数。请记住,接缝在那里,此时我们只是构建图形。
而不是自动接线
container.Register(
typeof(IZoo),
typeof(Zoo));
我们可以手工完成
container.Register(
typeof(Bear),
() => new Bear(hibernate));
请注意,模糊性来自于有多个ISleep sleepPattern
s正在发挥作用,因此我们需要指定这种或那种方式。
如何在Bear.Hunt和Cat.Hunt中提供IHunt,而不是Lion.Hunt?
继承永远不会是最灵活的选择。这就是为什么作品往往受到青睐,并不是说你应该放弃每一个等级,而是要注意沿途的摩擦。在我提到的书中,有一整章关于拦截,它解释了如何使用装饰器模式动态地用新功能装饰抽象。
最后,我希望容器在层次结构方法中选择最接近的匹配对我来说听起来不对。虽然看起来很方便,但我更喜欢将容器设置正确。
如果您碰巧使用Simple Injector进行DI职责,容器可以帮助解决这个问题。 (如果您没有使用Simple Injector,请参阅下面的“使用其他DI框架”)
该功能在Advanced Scenarios: Mixing collections of open-generic and non-generic components下的Simple Injector文档中描述。
您需要对服务接口和实现稍作调整。
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容器一起使用。
这次,我们从界面中删除泛型类型定义。相反,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
。