[英]Moving from Service Locator to Dependency Injection
我准备了一些示例应用程序,基于这些应用程序,我想讨论转移到依赖注入而不是服务定位器的问题。 我在DI领域还很新,所以请耐心等待。 示例应用使用Simple Injector作为DI库编写。 Bootstrapper中的注册按预期工作。 每当我需要IMessageBox接口和新实例ComputationCores时,我都会使用它。
我阅读了一些有关DI的文章,因此我知道应该有一些Composition根目录以及它应该如何工作。 但是我发现只是非常基本的示例,没有实词的复杂性。
样例代码:
public class DependencyResolver
{
public static Func<Type, object> ResolveMe;
public static T GetInstance<T>() where T : class
{
return (T)ResolveMe(typeof (T));
}
}
public interface IMessageBox
{
void ShowMessage(string message);
}
public class StandardMessageBox : IMessageBox
{
public StandardMessageBox()
{
Console.WriteLine("StandardMessageBox constructor called...");
}
~StandardMessageBox()
{
Console.WriteLine("StandardMessageBox destructor called...");
}
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
public interface IComputationCoreAlpha
{
int RunComputation(int myParam);
}
public class SyncComputationCoreAlpha : IComputationCoreAlpha
{
public SyncComputationCoreAlpha()
{
Console.WriteLine("SyncComputationCoreAlpha constructor called...");
}
~SyncComputationCoreAlpha()
{
Console.WriteLine("SyncComputationCoreAlpha destructor called...");
}
public int RunComputation(int myParam)
{
return myParam * myParam;
}
}
public class AsyncComputationCoreAlpha : IComputationCoreAlpha
{
public AsyncComputationCoreAlpha()
{
Console.WriteLine("AsyncComputationCoreAlpha constructor called...");
}
~AsyncComputationCoreAlpha()
{
Console.WriteLine("AsyncComputationCoreAlpha destructor called...");
}
public int RunComputation(int myParam)
{
return myParam * myParam;
}
}
public interface IComputationCoreBeta
{
int RunComputation(int myParam);
}
public class SyncComputationCoreBeta : IComputationCoreBeta
{
public SyncComputationCoreBeta()
{
Console.WriteLine("SyncComputationCoreBeta constructor called...");
}
~SyncComputationCoreBeta()
{
Console.WriteLine("SyncComputationCoreBeta destructor called...");
}
public int RunComputation(int myParam)
{
return myParam * myParam;
}
}
public class AsyncComputationCoreBeta : IComputationCoreBeta
{
public AsyncComputationCoreBeta()
{
Console.WriteLine("AsyncComputationCoreBeta constructor called...");
}
~AsyncComputationCoreBeta()
{
Console.WriteLine("AsyncComputationCoreBeta destructor called...");
}
public int RunComputation(int myParam)
{
return myParam * myParam;
}
}
public interface IProjectSubPart
{
int DoCalculations(int myParam);
}
public class ProjectSubPart1 : IProjectSubPart
{
public int DoCalculations(int myParam)
{
var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
messageBoxService.ShowMessage("Hardly working 1...");
var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();
var ccB = DependencyResolver.GetInstance<IComputationCoreAlpha>();
return ccA.RunComputation(myParam) + ccB.RunComputation(myParam + 1);
}
}
public class ProjectSubPart2 : IProjectSubPart
{
public int DoCalculations(int myParam)
{
var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
messageBoxService.ShowMessage("Hardly working 2...");
var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();
return ccA.RunComputation(myParam * 3);
}
}
public class ProjectSubPartN : IProjectSubPart
{
public int DoCalculations(int myParam)
{
var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
messageBoxService.ShowMessage("Hardly working N...");
return -3;
}
}
public class ManhattanProject
{
public void RunProject()
{
var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
messageBoxService.ShowMessage("Project started...");
var subPart1 = new ProjectSubPart1();
var subPart2 = new ProjectSubPart2();
var subPartN = new ProjectSubPartN();
var result = subPart1.DoCalculations(1) + subPart2.DoCalculations(2) + subPartN.DoCalculations(3);
messageBoxService.ShowMessage(string.Format("Project finished with magic result {0}", result));
}
}
public class Sample
{
public void Run()
{
BootStrapper();
var mp = DependencyResolver.GetInstance<ManhattanProject>();
mp.RunProject();
}
private void BootStrapper()
{
var container = new Container();
container.RegisterSingle<IMessageBox, StandardMessageBox>();
container.Register<IComputationCoreAlpha, SyncComputationCoreAlpha>();
container.Register<IComputationCoreBeta, AsyncComputationCoreBeta>();
DependencyResolver.ResolveMe = container.GetInstance;
}
}
在DI中,只允许在“合成”根目录中调用Container.GetInstance(resolve方法),而无处可去。 大多数依赖项应注入构造函数中。
问题1:如果我要改用DI,我认为ManhattanProject的构造函数应如下所示:ManhattanProject(IMessageBox mb,IComputationCoreAlpha cca,IComputationCoreBeta ccb)。 但这将导致每个mb,cca和ccb有一个实例。 并非每次都有新的cca实例,ccb都可以满足我的要求。
Q1a:我想这可以通过某种抽象工厂来解决,例如cca,ccb,它可以为每个请求提供新实例。 但是,那么-BootStrapper的目的是什么?
问题2:ManhattanProject可以由更多使用不同coputationCore(例如42)的ProjectSubParts组成。因此,以这种方式使用构造函数注入(以提供Computation内核)是完全不合适的,应该使用某种外观。 由于Facade在构造函数中的args数量也应该有限,所以我最终会遇到许多嵌套的Facade。 我想这是错误的。
Q3:我正在使用ProjectSubParts,它允许我做一些工作。 所有继承自IProjectSubPart接口。 如果我想为不同的ProjectSubParts注入不同的实现,我该怎么做? 是否应该为每个ProjectSubpart创建新接口,以允许DI容器解析使用哪种实现?
Q4:基于提供的示例(和服务定位器模式),我很容易创建IComputationCoreAlpha实例,该实例可以在每次需要时通过调用DependencyResolver.GetInstance在内部创建新的且干净的内部对象。 而且,我将完全控制它们,在使用完它们后,可以致电Dispose。 如果在DI概念中将在CompositionRoot中创建整个图形,那么这种用法怎么可能?
谢谢
问题1:如果我要改用DI,我认为ManhattanProject的构造函数应如下所示:ManhattanProject(IMessageBox mb,IComputationCoreAlpha cca,IComputationCoreBeta ccb)。
类应仅取决于直接需要的服务。 因此, ManhattanProject
不应依赖任何计算核心,而应仅依赖IProjectSubPart
抽象。
Q1a:我想这可以通过某种抽象工厂来解决,例如cca,ccb,它可以为每个请求提供新实例。 但是,那么-BootStrapper的目的是什么?
引导程序/组合根的目的是建立对象图。 如果创建工厂抽象,则需要在某个地方实现它。 这个“某处”是您合成的根源。 工厂实现应在您的合成根目录内部。
除了使用工厂之外,更好的方法是注入IEnumerable<IProjectSubPart>
。 在这种情况下,您的ManhattanProject
将如下所示:
public class ManhattanProject
{
private readonly IMessageBox messageBoxService;
private readonly IEnumerable<IProjectSubPart> parts;
public ManhattanProject(IMessageBox messageBoxService,
IEnumerable<IProjectSubPart> parts) {
this.messageBoxService = messageBoxService;
this.parts = parts;
}
public void RunProject() {
messageBoxService.ShowMessage("Project started...");
var calculationResults =
from pair in parts.Select((part, index) => new { part, value = index + 1 })
select pair.part.DoCalculations(pair.value);
var result = calculationResults.Sum();
messageBoxService.ShowMessage(
string.Format("Project finished with magic result {0}", result));
}
}
当您依赖IEnumerable<IProjectSubPart>
,可以防止在每次将新的IProjectSubPart
实现添加到系统时更改ManhattanProject
。 您可以在Simple Injector中进行如下注册:
// Simple Injector v3.x
container.RegisterSingleton<IMessageBox, StandardMessageBox>();
container.Register<ManhattanProject>();
container.RegisterCollection<IProjectSubPart>(new[] {
typeof(ProjectSubPart1),
typeof(ProjectSubPart2),
typeof(ProjectSubPartN)
});
// Simple Injector v2.x
container.RegisterSingle<IMessageBox, StandardMessageBox>();
container.Register<ManhattanProject>();
container.RegisterAll<IProjectSubPart>(new[] {
typeof(ProjectSubPart1),
typeof(ProjectSubPart2),
typeof(ProjectSubPartN)
});
通常,您甚至可以屏蔽应用程序的其他部分,而不必知道某个抽象有多种实现,但是在您的情况下,隐藏这种实现似乎是不可能的,因为(当前)由ManhattanProject
负责每个IProjectSubPart
值都不同。 如果有可能然而,正确的解决办法是让ManhattanProject
取决于IProjectSubPart
,而不是直接取决于中IEnumerable<IProjectSubPart>
和你会让组成根注入的复合材料实现,将包裹IEnumerable<IProjectSubPart>
描述在这里 。
可以将相同的模式应用于所有IProjectSubPart
实现。 例如:
public class ProjectSubPart1 : IProjectSubPart
{
private readonly IMessageBox messageBoxService;
private readonly IEnumerable<IComputationCoreAlpha> computers;
public ProjectSubPart1(IMessageBox messageBoxService,
IEnumerable<IComputationCoreAlpha> computers) {
this.messageBoxService = messageBoxService;
this.computers = computers;
}
public int DoCalculations(int myParam) {
messageBoxService.ShowMessage("Hardly working 1...");
var calculationResults =
from pair in computers.Select((computer, index) => new { computer, index })
select pair.computer.RunComputation(myParam + pair.index);
return calculationResults.Sum();
}
}
这些IComputationCoreAlpha
实现可以按如下所示注册为集合:
container.RegisterCollection<IComputationCoreAlpha>(new[] {
typeof(SyncComputationCoreAlpha),
typeof(AsyncComputationCoreAlpha)
});
Q2:由于在构造函数中facade的args数量也应该有限,所以我最终会遇到很多嵌套的facade。
很难说任何有用的东西。 我给定的LINQ查询实现可能不适用于您的情况,但是您的示例过于广泛,无法对此进行非常具体的说明。 最后,您可能需要自定义抽象,但是我不确定。
Q3:我正在使用ProjectSubParts,它允许我做一些工作。 所有继承自IProjectSubPart接口。 如果我想为不同的ProjectSubParts注入不同的实现,我该怎么做? 是否应该为每个ProjectSubpart创建新接口,以允许DI容器解析使用哪种实现?
这在很大程度上取决于您的设计。 为此,我们应该看一下Liskov替代原理 ,该原理基本上说给定抽象的任何子类型都应以与抽象兼容的方式运行。 因此,在您的情况下,如果某个类期望某个IProjectSubPart
实现,而在其他实现中无法正常运行,则意味着您正在违反Liskov替换原理。 这意味着即使这些实现可能具有确切的方法签名,它们的行为也不相同。 在这种情况下,您应该将它们拆分为多个接口。
但是,如果使用者仍然可以正常运行,并且更改实现只是一些方便,那么可以让他们具有相同的抽象是可以的。 一个很好的例子是带有FileLogger
和MailLogger
实现的ILogger
抽象。 在系统的某些部分,您可能认为通过邮件发送消息很重要。 对于依赖ILogger的类而言,无论消息是否写入文件,通过邮件发送还是完全不发送消息,它的功能都相同。
是否要违反LSK取决于您自己确定。
Q4:基于提供的示例(和服务定位器模式),我很容易创建IComputationCoreAlpha实例,该实例可以在每次需要时通过调用DependencyResolver.GetInstance在内部创建新的且干净的内部对象。 而且,我将完全控制它们,在使用完它们后,可以致电Dispose。 如果在DI概念中将在CompositionRoot中创建整个图形,那么这种用法怎么可能?
我要说,DI实际上使这项工作变得容易。 例如,让我们尝试使用服务定位器实现您想要的:
public class LazyComputationCoreAlphaProxy : IComputationCoreAlpha
{
public int RunComputation(int myParam) {
var heavyWeight = DependencyResolver.GetInstance<IComputationCoreAlpha>();
return heavyWeight.RunComputation(myParam);
}
}
这是一个代理类,可以在调用RunComputation
时延迟创建实际实例。 但这实际上给我们带来了一个问题。 如果我们看看消费者将如何使用它,这将变得很清楚:
public int DoCalculations(int myParam) {
var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();
return ccA.RunComputation(myParam);
}
在这里, DoCalculations
方法从服务定位器解析IComputationCoreAlpha
。 这将返回LazyComputationCoreAlphaProxy
实例(因为我们已在定位器中注册了该实例)。 解决后,我们将对其调用RunComputation
。 但是在RunComputation
内部,我们再次解析IComputationCoreAlpha
。 我们想解决IComputationCoreAlpha
,因为否则我们的LazyComputationCoreAlphaProxy
需要直接依赖于不同的实现,但是这会导致违反Dependency Inversion Principle,并且可能导致我们拥有许多不同的LazyComputationCoreAlphaProxy
。 每个实现一个。
但是,如果我们尝试在此处解析IComputationCoreAlpha
,则定位器将再次向我们返回LazyComputationCoreAlphaProxy
,这最终将导致堆栈溢出异常。
现在让我们看一下依赖注入的外观:
public class LazyComputationCoreAlphaProxy : IComputationCoreAlpha
{
private readonly Func<IComputationCoreAlpha> factory;
public LazyComputationCoreAlphaProxy(Func<IComputationCoreAlpha> factory) {
this.factory = factory;
}
public int RunComputation(int myParam) {
var heavyWeight = this.factory.Invoke();
return heavyWeight.RunComputation(myParam);
}
}
在这里,我们将Func工厂注入LazyComputationCoreAlphaProxy
的构造函数中。 这使代理可以忽略其创建的实际类型,同时仍然允许与以前相同的惰性行为。 现在,我们将重新构建对象图的该部分的职责委托给了我们的合成根。 我们可以按以下方式手动进行连接:
LazyComputationCoreAlphaProxy(() => new SyncComputationCoreAlpha())
或者,我们可以使用Simple Injector的装饰器工具为我们完成此操作:
container.RegisterCollection<IComputationCoreAlpha>(new[] {
typeof(SyncComputationCoreAlpha),
typeof(AsyncComputationCoreAlpha)
});
container.RegisterDecorator(
typeof(IComputationCoreAlpha),
typeof(LazyComputationCoreAlphaProxy));
通过RegisterDecorator
注册,Simple Injector将自动将所有IComputationCoreAlpha
实现包装在LazyComputationCoreAlphaProxy
装饰器中。 开箱即用,Simple Injector会了解装饰器内的Func工厂代表,并将确保注入创建了装饰对象的工厂。
但是,由于我们现在正在讨论装饰器。 装饰器的依赖注入为我们提供了更多改进代码的可能性。 例如, IProjectSubPart
中的许多代码看起来相似。 它们都有相同的消息框日志记录代码:
public class ProjectSubPart1 : IProjectSubPart
{
private readonly IMessageBox messageBoxService;
private readonly IEnumerable<IComputationCoreAlpha> computers;
public ProjectSubPart1(IMessageBox messageBoxService,
IEnumerable<IComputationCoreAlpha> computers) {
this.messageBoxService = messageBoxService;
this.computers = computers;
}
public int DoCalculations(int myParam) {
messageBoxService.ShowMessage("Hardly working 1...");
// part specific calculation
}
}
如果您有许多不同的IProjectSubPart
,那么这是很多重复的代码,它们不仅使实际的实现复杂化,而且还需要维护。 什么可以将基础结构问题(或跨领域问题)移出这些类,并仅将其实现一次:在装饰器中:
public class MessageBoxLoggingProjectSubPart : IProjectSubPart
{
private readonly IMessageBox messageBoxService;
private readonly IProjectSubPart decoratee;
public MessageBoxLoggingProjectSubPart(IMessageBox messageBoxService,
IProjectSubPart decoratee) {
this.messageBoxService = messageBoxService;
this.decoratee = decoratee;
}
public int DoCalculations(int myParam) {
messageBoxService.ShowMessage("Hardly working 1...");
return this.decoratee.DoCalculations(myParam);
}
}
使用此装饰器,您可以将零件简化为以下内容:
public class ProjectSubPart1 : IProjectSubPart
{
private readonly IEnumerable<IComputationCoreAlpha> computers;
public ProjectSubPart1(IEnumerable<IComputationCoreAlpha> computers) {
this.computers = computers;
}
public int DoCalculations(int myParam) {
var calculationResults =
from pair in computers.Select((computer, index) => new { computer, index })
select pair.computer.RunComputation(myParam + pair.index);
return calculationResults.Sum();
}
}
请注意, ProjectSubPart1
如何不再需要依赖IMessageBox
。 这将清理实现(不要忘记您拥有的其他42个实现)。 同样,如果我们将手动创建此类零件,则将执行以下操作:
new MessageBoxLoggingProjectSubPart(new ProjectSubPart1(computers));
但是,使用Simple Injector,这变得更加容易:
container.RegisterCollection<IProjectSubPart>(new[] {
typeof(ProjectSubPart1),
typeof(ProjectSubPart2),
typeof(ProjectSubPartN)
});
container.RegisterDecorator(
typeof(IProjectSubPart),
typeof(MessageBoxLoggingProjectSubPart));
现在,只要您想更改记录方式,就只需要更改MessageBoxLoggingProjectSubPart
。 例如,当您想在操作完成后登录时,或者万一抛出异常。 这样可以避免您必须对整个应用程序进行彻底的更改(这就是“ 打开/关闭原则”的全部含义)。
对这么长的帖子我很抱歉。 这是一些注入的土豆:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.