【发布时间】:2020-05-24 08:50:48
【问题描述】:
我有以下与 Unity 相关的问题。下面的代码存根设置了基本场景,问题在底部。
注意,[Dependency] 属性不适用于下面的示例并导致 StackoverflowException,但构造函数注入确实有效。
NOTE(2) 下面的一些 cmets 开始分配“标签”,例如代码异味、糟糕的设计等……因此,为避免混淆,这里是没有任何设计的业务设置。
这个问题似乎在一些最著名的 C# 大师中引起了激烈的争论。事实上,这个问题远远超出了 C#,它更多地属于纯计算机科学。该问题基于服务定位器模式和纯依赖注入模式之间众所周知的“战斗”:https://martinfowler.com/articles/injection.html 与 http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ 以及后续更新以纠正依赖注入变得过于复杂时的情况:http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/
这是一种情况,它与最后两个描述的不太吻合,但似乎完全适合第一个。
我有大量(50 多个)我称之为微服务的集合。如果您有更好的名字,请在阅读时“应用”它。它们中的每一个都对单个对象进行操作,我们称之为引用。但是,元组(上下文+引号)似乎更合适。报价是一个业务对象,它被处理并序列化到数据库中,上下文是一些支持信息,在处理报价时是必需的,但不会保存到数据库中。其中一些支持信息实际上可能来自数据库或某些第三方服务。这是无关紧要的。装配线是一个现实世界的例子:装配工人(微服务)接收一些输入(指令(上下文)+零件(报价)),处理它(根据指令对零件做某事和/或修改指令)如果成功或在出现问题时丢弃它(引发异常),则进一步传递它。微服务最终被捆绑成一小部分(大约 5 个)高级服务。这种方法将一些非常复杂的业务对象的处理线性化,并允许将每个微服务与所有其他微服务分开测试:只需给它一个输入状态并测试它是否产生预期的输出。
这就是有趣的地方。由于涉及的步骤数量众多,高级服务开始依赖于许多微服务:10+ 甚至更多。这种依赖是自然的,它只是反映了底层业务对象的复杂性。除此之外,几乎可以不断地添加/删除微服务:基本上,它们是一些业务规则,几乎像水一样流动。
这与上述 Mark 的建议严重冲突:如果我有 10 多个有效独立的规则应用于某个高级服务中的报价,那么,根据第三篇博客,我应该将它们聚合成一些逻辑组,比方说不超过 3-4,而不是通过构造函数注入所有 10+。但是没有逻辑组!虽然有些规则是松散依赖的,但它们中的大多数不是,因此人为地将它们捆绑在一起弊大于利。
规则变化频繁,这就变成了维护的噩梦:每次规则变化时,所有真实/模拟的调用都必须更新。
我什至没有提到这些规则取决于美国各州,因此理论上大约有 50 个规则集合,每个州和每个工作流都有一个集合。虽然有些规则在所有州之间共享(例如“将报价保存到数据库”),但还有很多州特定的规则。
这是一个非常简单的例子。
报价 - 保存到数据库中的业务对象。
public class Quote
{
public string SomeQuoteData { get; set; }
// ...
}
微服务。他们每个人都会执行一些小的更新来引用。也可以从一些较低级别的微服务构建更高级别的服务。
public interface IService_1
{
Quote DoSomething_1(Quote quote);
}
// ...
public interface IService_N
{
Quote DoSomething_N(Quote quote);
}
所有微服务都继承自这个接口。
public interface IQuoteProcessor
{
List<Func<Quote, Quote>> QuotePipeline { get; }
Quote ProcessQuote(Quote quote = null);
}
// Low level quote processor. It does all workflow related work.
public abstract class QuoteProcessor : IQuoteProcessor
{
public abstract List<Func<Quote, Quote>> QuotePipeline { get; }
public Quote ProcessQuote(Quote quote = null)
{
// Perform Aggregate over QuotePipeline.
// That applies each step from workflow to a quote.
return quote;
}
}
高级“工作流”服务之一。
public interface IQuoteCreateService
{
Quote CreateQuote(Quote quote = null);
}
以及我们使用许多低级微服务的实际实现。
public class QuoteCreateService : QuoteProcessor, IQuoteCreateService
{
protected IService_1 Service_1;
// ...
protected IService_N Service_N;
public override List<Func<Quote, Quote>> QuotePipeline =>
new List<Func<Quote, Quote>>
{
Service_1.DoSomething_1,
// ...
Service_N.DoSomething_N
};
public Quote CreateQuote(Quote quote = null) =>
ProcessQuote(quote);
}
实现DI主要有两种方式:
标准做法是通过构造函数注入所有依赖:
public QuoteCreateService(
IService_1 service_1,
// ...
IService_N service_N
)
{
Service_1 = service_1;
// ...
Service_N = service_N;
}
然后用 Unity 注册所有类型:
public static class UnityHelper
{
public static void RegisterTypes(this IUnityContainer container)
{
container.RegisterType<IService_1, Service_1>(
new ContainerControlledLifetimeManager());
// ...
container.RegisterType<IService_N, Service_N>(
new ContainerControlledLifetimeManager());
container.RegisterType<IQuoteCreateService, QuoteCreateService>(
new ContainerControlledLifetimeManager());
}
}
然后 Unity 将发挥其“魔力”并在运行时解析所有服务。问题是,目前我们有大约 30 个这样的微服务,而且预计数量还会增加。随后,一些构造函数已经注入了 10 多个服务。这不方便维护、模拟等......
当然,可以使用这里的想法:http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/ 但是,微服务之间并没有真正的关联,因此将它们捆绑在一起是一个没有任何理由的人为过程。此外,它还会破坏使整个工作流程线性和独立的目的(微服务获取当前“状态”,然后使用引用执行一些操作,然后继续前进)。他们都不关心之前或之后的任何其他微服务。
另一种想法似乎是创建一个单一的“服务存储库”:
public interface IServiceRepository
{
IService_1 Service_1 { get; set; }
// ...
IService_N Service_N { get; set; }
IQuoteCreateService QuoteCreateService { get; set; }
}
public class ServiceRepository : IServiceRepository
{
protected IUnityContainer Container { get; }
public ServiceRepository(IUnityContainer container)
{
Container = container;
}
private IService_1 _service_1;
public IService_1 Service_1
{
get => _service_1 ?? (_service_1 = Container.Resolve<IService_1>());
set => _service_1 = value;
}
// ...
}
然后用Unity注册,把所有相关服务的构造函数改成这样:
public QuoteCreateService(IServiceRepository repo)
{
Service_1 = repo.Service_1;
// ...
Service_N = repo.Service_N;
}
这种方法的好处(与前一种相比)如下:
所有微服务和更高级别的服务都可以以统一的形式创建:可以轻松添加/删除新的微服务,而无需修复服务的构造函数调用和所有单元测试。随后,维护和复杂性降低。
由于接口IServiceRepository,很容易创建一个自动化单元测试,它将迭代所有属性并验证所有服务都可以实例化,这意味着不会出现令人讨厌的运行时意外。
这种方法的问题在于它开始看起来很像服务定位器,有些人认为这是一种反模式:http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ 然后人们开始争辩说所有依赖项都必须明确而不是隐藏如ServiceRepository。
我该怎么办?
【问题讨论】:
-
我的看法是,这到处都是代码味道。它也看起来像XY problem。应审查当前的设计决策。
-
由于您指的是 Mark Seemann 的博客,我建议您阅读 Dependency Injection in .NET, second edition 的第 6.1 章,其中广泛讨论了修复构造函数注入代码气味。
-
请不要“滥用”微服务这个词。微服务是独立运行的自治应用程序。您的微服务都在同一个进程中运行并变异同一个实体,这两个特征都与微服务非常不同。
-
你得到一个堆栈溢出异常的事实表明你有一个循环依赖。大多数 DI 容器对此进行检查并给出有意义的异常,而不是抛出堆栈跟踪。
标签: c# dependency-injection unity-container