控制反转
- 整个ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使用依赖注入提供所需的服务对象。
要了解这个依赖注入容器以及它的服务提供机制,需要先了解什么是依赖注入。而提到依赖注入,还需先引入控制反转(Inverse of Control,IoC)。
- 流程控制的反转
IoC是设计框架所采用的一种基本思想。所谓的控制反转就是将应用对流程的控制转移到框架之中。
以下示例为传统的面向类库编程的实现:
public static class MvcLib
{
public static Task ListenAsync(Uri address);
public static Task<Request> ReceiveAsync();
public static Task<Controller> CreateContrllerAsync();
public static Task<View> ExcuteControllerAsync(Controller controller);
public static Task RenderViewAsync(View view);
}
class Program { static async Task Main() { //ListenAsync方法启动一个监听器并将其绑定到指定的地址进行HTTP请求的监听 var address = new Uri("http://0.0.0.0:8080/mvcapp"); await MvcLib.ListenAsync(adress); while(true) { //抵达的请求通过ReciveAsync方法进行接收,通过一个Request对象来表示 var request = await MvcLib.ReciveAsync(); //CreateControllerAsync方法根据接收的请求解析并激活目标Controller对象 var controller = await MvcLib.CreateControllerAsync(request); //ExecuteControllerAsync方法执行激活的Controller对象并返回一个表示视图的View对象 var view = await MvcLib.ExecuteControllerAsync(controller); //RenderViewAsync方法最终将View对象转换成HTML请求, 并作为当前请求响应的内容返回请求的客户端 await MvcLib.RenderViewAsync(view); } } }
对于上面演示的MvcLib来说,作为消费者的应用程序需要自行控制整个HTTP请求的处理流程,但实际上这是一个很“泛化”的工作流程。几乎所有的MVC应用都采用这样的流程来监听、接收请求并最终予以响应。
- 如果将MvcLib从类库改造成一个框架,可以将其称为MvcFrame。MvcFrame的核心是一个呗称为MvcEngine的执行引擎,它驱动一个编排好的工作流对HTTP请求进行一致性处理。
在传统面向类库编程的时代,针对HTTP请求处理的流程被牢牢地控制在应用程序之中。引用框架之后,请求处理的控制权转移到了框架之中。
- 好莱坞法则
在好莱坞,演员吧简历递交给电影公司后就只能回家等消息。由于电影公司对整个娱乐项目具有完全控制权,演员只能被动地接收电影公司的邀约。“不要给我们打电话,我们会给你打电话”——这就是著名的好莱坞法则,IoC完美地体现了这一原则。
在IoC的应用语境中,框架就如同掌握整个电影制片流程的电影公司。由于它是整个工作流程的实际控制者,所以只有它知道哪个环节需要哪些组件。应用程序就像演员,它只需要按照框架制定的规则注册这些组件即可,因为框架会在适当的时机自动加载并执行 注册的组件。
在ASP.NET Core MVC应用开发来说,我们只需要按照约定的规则(如约定的目录结构和文件与类型命名方式等)定义响应的Controller类型和View文件即可。
采用IoC可以实现流程控制从应用程序向框架的转移,但是被转移的仅仅是一个泛化的流程。任何一个具体的应用可能都需要对该流程的某些环节进行定制。
- 一般来说,框架会以相应的形式提供一系列的扩展点,应用程序通过注册扩展的方式实现对流程某个环节的定制。在引擎被启动之前,应用程序将所需的扩展注册到框架之中,一旦引擎被正常启动,这些注册的扩展会自动参与整个流程的执行。
- 综上,IoC一方面通过流程控制从应用程序向框架的反转,实现了针对流程自身的重用,另一方面通过内置的扩展机制使这个被重用的流程能够自由地被定制,这两个因素决定了框架自身的价值。
IoC模式
- 很多人将IoC理解为一种面向对象的设计模式,实际上,IoC不仅与面向对象没有必然联系,它自身甚至不算是一种设计模式。
- 一般来讲,设计模式提供了一种解决某种具体问题的方案,但IoC既没有一个针对性的问题领域,其自身也没有提供一种可操作的解决方案,所以我们更加倾向于将IoC视为一种设计原则。很多设计模式都采用了IoC原则。
- 模板方法
- 模板方法(Template Method)是与IoC联系最紧密的一种设计模式。其与IoC的意图一致,主张将一个可复用的工作流程或者有多个步骤组成的算法定义成模板方法,组成这个流程或者算法的单一步骤则在相应的虚方法之中实现。
模板方法会根据预先编排的流程调用这些虚方法。这些方法均定义在一个类中,可以通过派生该类并重写相应的虚方法的方式达到对流程定制的目的。
对于前面的MVC示例,可以将整个请求处理流程在一个MvcEngine类中实现:
public class MvcEngine { public async Task StartAsync(Uri address) { await ListenAsync(address); while(true) { //抵达的请求通过ReciveAsync方法进行接收,通过一个Request对象来表示 var request = await ReciveAsync(); //CreateControllerAsync方法根据接收的请求解析并激活目标Controller对象 var controller = await CreateControllerAsync(request); //ExecuteControllerAsync方法执行激活的Controller对象并返回一个表示视图的View对象 var view = await ExecuteControllerAsync(controller); //RenderViewAsync方法最终将View对象转换成HTML请求, 并作为当前请求响应的内容返回请求的客户端 await RenderViewAsync(view); } } public virtual Task ListenAsync(Uri address); public virtual Task<Request> ReceiveAsync(); public virtual Task<Controller> CreateContrllerAsync(); public virtual Task<View> ExcuteControllerAsync(Controller controller); public virtual Task RenderViewAsync(View view); }
对于具体的应用程序来说,如果定义在MvcEngine中针对请求的处理方式完全符合要求,那么只需要创建一个MvcEngine对象,然后指定一个监听地址来调用模板方法StartAsync,以开启MVC引擎即可。
- 如果该MVC引擎对请求某个环节的处理无法满足要求,可以创建MvcEngine的派生类,然后重写实现该环节相应的虚方法即可。
- 工厂方法
- 对于一个复杂的流程来说,我们倾向于将组成该流程的各个环节实现在相应的组件之中,所以针对流程的定制可以通过提供相应组件的形式实现。
- 23种设计模式种有一种重要的类型,即创建型模式,如常用的工厂方法和抽象方法。IoC体现的针对流程的复用与定制同样可以通过这些设计模式来完成。
- 所谓工厂方法。就是在某个类(对应下面的MvcEngine)中定义用来提供所需服务对象的方法,这个方法可以是一个单纯的抽象方法,也可以是具有默认实现的虚方法。至于方法声明的返回类型,可以是一个接口或者抽象类,也可以是未封闭的具体类型。派生类可以采用重写工厂方法的方式提供所需的服务对象。
以MVC为例,让独立的组件完成整个请求处理流程的几个核心环节。具体来说,可以为这些核心组件定义如下几个对应的接口:
//负责监听、接收和响应请求(针对请求的响应有ReceiveAsync方法返回的HttpContext上下文来完成) public interface IWebListener { Task ListenAsync(Uri address); Task<HttpContext> ReceiveAsync(); } //负责根据当前HttpContext上下文激活目标Controller对象, 并在COntroller对象执行后做一些释放回收工作 public interface IControllerActivator { Task<Controller> CreateControllerAsync(HttpContext httpcontext); Task ReleaseAsync(Controller controller); } //负责完成对Controller的执行 public interface IControllerExecutor { Task<View> ExecuteAsync(Controller controller, HttpContext httpContext); } //负责针对视图的呈现 public interface IViewRenderer { Task RendAsync(View view, HttpContext httpContext); }
在MvcEnging中,定义如下:
public class MvcEngin { public async Task StartAsync(Uri address) { //获取对应接口对象 var listener = GetWebListener(); var activator = GetControllerActivitor(); var executor = GetControllerExcutor(); var render = GetViewRenderer(); //使用接口对象中的方法 await listener.ListenAsync(address); while(true) { var httpContext = await listener.ReceiveAsync(); var controller = await activator.CreateControllerAsync(httpContext); try { var view = await executor.ExecuteAsync(controller, httpContext); await render.RendAsync(view, httpContext); } finally { await activator.ReleaseAsync(controller); } } } //如下方法用于获取对应接口的对象 protected virtual IWebLister GetWebListener(); protected virtual IControllerActivator GetControllerActivator(); protected virtual IControllerExecutor GetControllerExecutor(); protected virtual IViewRenderer GetViewRenderer(); }
如果要对请求处理的某个环节进行定制,可以将定制的操作实现在对应接口的实现类中,然后在派生出一个MvcEngine的派生类,重写对应的工厂方法来提供被定制的对象。
例如,以单例模式提供目标Controller对象的实现就定义在SingletonControllerActivator类中。在派生于MvcEngine的FoobarMvcEngine类中重写工厂方法GetControllerActivator,使其返回一个SingletonControllerActivator对象:
public class SingletonControllerActivator : IControllerActivator { //重写该组件的相关方法 public Task<Controller> CreateControllerAsync(HttpCOntext httpController) { //... } public Task ReleaseAsync(COntroller controller) => Task.CompletedTask; }
public class FoobarMvcEngine : MvcEngine { //重写工厂方法,使其返回定制后的接口对象 protected override ControllerActivator GetControllerActivator() => new SingletonControllerActivator(); }
思维导图:
- 抽象工厂
工厂方法和抽象工厂均提供了一个“生产”对象实例的工厂,但是两者在设计上有本质上的区别。
- 工厂方法利用定义在某个类型的抽象方法或虚方法完成了针对“单一对象”的提供,而抽象工厂则利用一个独立的接口或抽象类提供“一组相关的对象”。
- 具体来说,我们需要定义一个独立的工厂接口或者抽象工厂类,并在其中定义多个工厂方法来提供“同一系列”的多个相关对象。如果希望抽象工厂具有一组默认的“产出”,也可以将一个未被封闭的类型作为抽象工厂,以虚方法形式定义的工厂方法将默认的对象作为返回值。
- 如下,可以定义一个名为IMvcEngineFactory的接口作为抽象工厂,并在其中定义4个方法用来提供请求监听和处理过程使用到的4种核心对象: ```csharp public interface IMvcEngineFactory { IWebLister GetWebListener(); IControllerActivator GetControllerActivator(); IControllerExecutor GetControllerExecutor(); IViewRenderer GetViewRenderer(); }
```csharp
public class MvcEngineFactory : IMvcEngineFactory
{
protected virtual IWebLister GetWebListener();
protected virtual IControllerActivator GetControllerActivator();
protected virtual IControllerExecutor GetControllerExecutor();
protected virtual IViewRenderer GetViewRenderer();
}
然后在创建MvcEngine对象的时候提供一个具体的IMvcEngineFactory对象。如果没有显式指定,MvcEngine通常默认使用EngineFactory对象:
public class MvcEngine { public IMvcEngineFactory EngineFactory { get;} public MvcEngine(IMvcEngineFactory engineFactory = null) { EngineFactory = engineFactory ?? new MvcEngineFactory(); } public async Task StartAsync(Uri address) { //利用IMvcEngineFactory对象来获取相应的对象完成对请求的处理流程 var listener = EngineFactory.GetWebListener(); var activator = EngineFactory.GetControllerActivitor(); var executor = EngineFactory.GetControllerExcutor(); var render = EngineFactory.GetViewRenderer(); await listener.ListenAsync(address); while(true) { var httpContext = await listener.ReceiveAsync(); var controller = await activator.CreateControllerAsync(httpContext); try { var view = await executor.ExecuteAsync(controller, httpContext); await render.RendAsync(view, httpContext); } finally { await activator.ReleaseAsync(controller); } } } }
如果具体的应用程序需要采用SingletonControllerActivator以单例的模式来激活目标Controller对象,可按如下方式定义一个具体的工厂类FoobarEngineFactory。
public class FoobarEngineFactory : MvcEngineFactory { public override ControllerActivator GetControllerActivator() { return new SingletonControllerActivator(); } }
最终的应用程序将利用FoobarEngineFactory对象来创建作为引擎的MvcEngine对象:
public class App { static async Task Main() { var address = new Uri("http://0.0.0.0:8080/mvcapp"); var engine = new MvcEngine(new FoobarEngineFactory()); await engine.StartAsync(address); //... } }
思维导图:
依赖注入
- IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架之中,以实现对流程的复用,并按照好莱坞法则实现应用程序的代码与框架之间的交互。
- 可以采用若干设计模式以不同的方式实现IoC,如模板方法、工厂方法和抽象工厂。
- 除此之外,依赖注入(Dependency Injection,DI)是一种更具有价值的IoC模式。
- 由容器提供对象
- 与前面介绍的工厂方法和抽象工厂模式一样,依赖注入是一种“对象提供型”的设计模式。可以将提供的对象统称为“服务”“服务对象”“服务实例”。
- 在一个采用依赖注入的应用中,我们定义某个类型时,只需要直接将它依赖的服务采用相应的方式注入进来即可。
- 在应用启动时,我们会对所需的服务进行全局注册。一般来说,服务大都是针对实现的接口或者继承的抽象类进行注册的,服务注册信息会在后续消费过程中帮助我们提供对应的服务实例。
按照好莱坞法则,应用只需定义并注册好所需的服务,服务示例的提供则完全交给框架来完成。框架利用一个独立的容器(Container)来提高所需的每个服务实例。
依赖注入容器
我们将这个被框架用来提供服务的容器称为依赖注入容器。
例如,创建一个Cat依赖注入容器类,即可调用其扩展方法GetService
从某个Cat对象中获取指定类型的服务对象: public static class CatExtensions { public static T GetService<T>(this Cat cat); }
而后在MvcEngine类中,有如下定义:
public class MvcEngine { public Cat Cat { get;} public MvcEngine(Cat cat) => Cat = cat; public async Task StartAsync(Uri address) { //利用IMvcEngineFactory对象来获取相应的对象完成对请求的处理流程 var listener = Cat.GetService<IWebListener>(); var activator = Cat.GetService<IControllerActivitor>(); var executor = Cat.GetService<IControllerExcutor>(); var render = Cat.GetService<IViewRenderer>(); await listener.ListenAsync(address); while(true) { var httpContext = await listener.ReceiveAsync(); var controller = await activator.CreateControllerAsync(httpContext); try { var view = await executor.ExecuteAsync(controller, httpContext); await render.RendAsync(view, httpContext); } finally { await activator.ReleaseAsync(controller); } } } }
由于服务注册最终决定了依赖注入容器根据指定的服务类型会提供一个什么样的服务实例,所以我们可以通过修改服务注册的方式来实现对框架的定制。
- 如果应用程序需要采用SingletonControllerActivator以单例的模式来激活目标Controller,则可在启动MvcEngine之前按照如下形式将SingletonControllerActivator注册到依赖注入容器中:
public class App { static void Main(string[] args) { var cat = new Cat(); cat.Register<IControllerActivator, SingletonControllerActivator>(); var engine = new MvcEngine(cat); var adress = new Uri("http://0.0.0.0:8080/mvcapp"); engine.StartAsync(adress); } }
- 3种依赖注入方式
- 一项任务往往需要多个对象相互协作才能完成,或者说某个对象在完成某项任务的时候需要直接或间接地依赖其他的对象来完成某些必要的步骤,所以运行时对象之间的依赖关系是由目标任务决定的,是“恒定不变”的,自然也无所谓“解耦”的说法。但是运行时对象通过对应的类来定义,类与类之间的耦合可以通过对依赖进行抽象的方式来降低或解除。
- 从服务消费的角度来讲,如果借助一个接口对消费的服务进行抽象,那么服务消费程序针对具体服务类型的依赖可以转到服务接口的依赖上面,但运行时提供给消费者的总是一个针对某个具体服务类型的对象。在完成定义在服务接口的操作时,这个对象可能需要其他相关对象的参与,即可能具有对其他服务对象的依赖。
从面向对象编程的角度来讲,类型中的字段或者属性是依赖的一种主要体现方式。如果类型A中具有一个类型B的字段或属性,那么类型A就对类型B产生了依赖,所以可以将依赖注入简单地理解为一种针对依赖字段或者属性的自动化初始化方式。可以通过3种方法达到这个目的。
(1)构造器注入
构造器注入就是在构造函数中借助参数将依赖的对象注入由由它创建的对象之中。
如下,Foo针对Bar的依赖体现在只读属性Bar上,针对该属性的初始化是在构造函数种实现的,具体的属性值有构造函数传入的参数提供:
public class Foo { public IBar Bar { get;} public Foo(IBar bar) => Bar = bar; }
除此以外,构造器注入还体现在对构造函数的选择上。
如下,Foo类定义了两个构造函数,依赖注入容器在创建Foo对象之前需要先选择一个合适的构造函数。至于目标构造函数如何选择,不同的依赖注入容器可能有不同的策略,如可以选择参数最多或者最少的构造函数,也可以按照如下方式在目标构造函数上标注一个InjectionAttribute特性:
public class Foo { public IBar Bar { get;} public IBaz Baz { get;} [Injection] public Foo(Ibar bar) => Bar = bar; public Foo(IBar bar, Ibaz baz) : this(bar) => Baz = baz; }
(2)属性注入
如果依赖直接体现为类的某个属性,并且该属性不是只读的,则可以让依赖注入容器在对象创建之后自动对其赋值,进而达到依赖注入的目的。一般来说,在定义这种类型的时候,需要显式地将这样的属性标识为需要自动注入的依赖属性,从而与其它普通属性进行区分。
如下,Foo类种定义了两个可读写的公共属性(Bar和Baz),通过标注InjectionAttribute特性的方式可以将属性Baz设置为自动注入的依赖属性。由依赖注入容器提供的Foo对象的Baz属性将会被自动初始化:
public class Foo { public IBar Bar { get; set;} [Injection] public IBaz baz { get; set;} }
(3)方法注入
体现依赖关系的字段或属性可以通过方法的形式初始化。如下所示,Foo对Bar的依赖体现在只读属性上,针对该属性的初始化实现在Initialize方法中,具体的属性值由该方法传入的参数提供。同样,通过标注特性的方式可以将该方法标识为注入方法:依赖注入容器在调用构造函数创建一个Foo对象之后,会自动调用Initialize方法对只读属性Bar进行赋值。
public class Foo { public IBar Bar { get;} [Injection] public Initialize(Ibar bar) => Bar = bar; }
(4)广泛应用与ASP.NET Core的方式
一种更加自由的方式,这种方式在ASP.NET Core应用中具有广泛应用。ASP.NET Core在启动的时候会调用注册的Startup对象来完成中间件的注册,而定义Startup类型的时候不需要让它实现某个接口,所以用于注册中间件的Configure方法没有一个固定的声明,但可以按照如下方式将任意依赖的服务实例直接注入这个方法中:
public class Startup { public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz); }
类似的注入方式同样可以应用到中间件类型的定义上。与用来注册中间件的Startup类型一样,ASP.NET Core框架下的中间件类型同样不需要实现某个预定义的接口,用于处理请求的InvokeAsync方法或者Invoke方法同样可以按照如下方式注入任意的依赖服务:
public class FoobarMiddleware { private readonly RequestDelegate _next; public FoobarMiddleware(RequestDelegate next) => _next = next; public Task InvokeAsync(HttpContext httpContext, IFoo foo, IBar bar, IBaz baz); }
上述这种方式的方法注入促成了一种“面向约定”的编程方式,它不再需要实现某个预定义的接口或者继承某个预定义的基类。
- 对于前面的几种注入方式,构造器注入是最理想的形式,不建议使用属性注入和方法注入(基于约定的方法除外)。
- Service Locator模式
即在构造函数种“注入”了代表依赖注入容器的Cat对象,在任何使用到依赖服务的地方,只需要利用它来提供对应的服务实例即可:
public class Foo : IFoo { public Cat Cat { get;} public Foo(Cat cat) => Cat = cat; public async Task InvokeAsync() { await Cat.GetService<IBar>().InvokeAsync(); await Cat.GetService<IBaz>().InvokeAsync(); } }
但这种方式采用的设计模式不是依赖注入,而是一种被称为Service Locator的设计模式。Service Locator模式同样具有一个通过服务注册创建的全局的容器来提供所需的服务实例,该容器被称为Service Locator。
- 依赖注入与Service Locator
(1)差异性
依赖注入和Service Locator实际上是同一事物在不同设计模式中的不同称谓,其差异性主要体现在:
结论:
- 推荐使用依赖注入模式。
- 原因:
- 本着“松耦合、高内聚”的设计原则,既然已经将一组相关的操作定义在一个能够复用的服务中,就应该尽量要求服务自身不但具有独立和自治的特性,而且要求服务之间应该具有明确的界定,服务之间的依赖关系应该是明确的而不是模糊的。
- 不论采用属性注入、方法注入还是Service Locator提供当前依赖的服务,都相当于为当前的服务增添一个新的依赖(前两者对Injection标记依赖),即针对依赖注入容器或者Service Locator的依赖。
- 当前服务针对另一个服务的依赖与针对依赖注入容器或者Service Locator的依赖具有本质上的不同。
- 前者是一种基于类型的依赖,不论是基于服务的接口还是实现类型,这是一种“契约”的依赖。这种依赖不仅是明确的,也是有保障的。
- 但依赖注入容器或者Service Locatoe本质上是一个黑盒,它能够提供所需服务的前提是相应的服务注册已经预先添加到容器之中,但是这种依赖不仅是模糊的也是不可靠的。