# LearningDI **Repository Path**: richieyangs/LearningDI ## Basic Information - **Project Name**: LearningDI - **Description**: No description available - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 1 - **Created**: 2016-11-06 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 一、DIP原则 > * 高层模块不应该依赖于底层模块,二者都应该依赖于抽象。 > * 抽象不应该依赖于细节,细节应该依赖于抽象。 该原则理解起来稍微有点抽象,我们可以将该原则通俗的理解为:**"依赖于抽象”**。 该规则告诉我们,程序中所有的依赖关系都应该终止于`抽象类或者接口`,从而达到松耦合的目的。因为我们在应用程序中编写的大多数具体类都是`不稳定的`。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在`抽象和接口`的后面,可以隔离它们的不稳定性。 ### 举个例子 一个Button对象会触发Click方法,当被按下时,会调用Light对象的TurnOn方法,否则会调用Light对象的TurnOff方法。 这个设计存在两个问题: 1. Button类直接依赖于Light类,这种依赖关系意味着当Light改变时,Button类会受到影响; 2. Button对象只能控制Light对象,想要控制电视或者冰箱就不行了; ### 新的设计: 这个方案对那些需要被Button控制的对象提出了一个约束。需要被Button控制的对象必须要实现ISwitchableDevice接口。 所为`原则`,只是描述了什么是对的,但是并没有说清楚如何去做。在软件工程中,我们经常使用`DI(依赖注入)`来达到这个目的。但是提到依赖注入,人们又会经常提起`IoC`这个术语。所以先让我们来了解下什么是IoC。 ## 二、IoC IoC的全名是Inverse of Control,即**控制反转**。这一术语并不是用来描述面向对象的某种原则或者模式,IoC体现为一种**流程控制的反转**,一般用来对框架进行设计。 ### 举个例子 ReportService是一个用来显示报表的流程,该流程包括`Trim()`,`Clean()`,`Show()`三个环节。 public class ReportService { private string _data; public ReportService(string data) { _data = data; } public void Trim(string data) { _data = data.Trim(); } public void Clean() { _data = _data.Replace("@", ""); _data = _data.Replace("-", ""); //...other rules } public void Show() { Console.WriteLine(_data); } } 客户端通过下面的方式使用该服务: var reportService = new ReportService(input); reportService.Trim(input); reportService.Clean(); reportService.Show(); 这样的一个设计体现了**过程式**的思考方式,客户端依次调用每个环节从而组成了整个报表显示流程,这样的代码体现了:**客户端拥有流程控制权**。 我们来分析下这段代码,ReportService提供了3个可重用的Api,正如ReportService的命名一样,它告诉我们它是一个服务,我们只能重用他提供的三个服务,它无法提供一个打印报表的流程,整个流程是客户端来控制的。 另外,该设计也违反了[tell, Don't ask原则](http://martinfowler.com/bliki/TellDontAsk.html)。 打印报表作为一个可复用的流程,不但可以提供可复用的**流程环节**,还可以提供可复用的**流程的定义**,当我们进行框架设计的时候,往往会将整个流程控制定制在框架之中,然后提供扩展点供客户端定制。这样的思想体现了流程的所有权从客户端到框架的反转。 比如asp.net mvc或者asp.net api框架,内部定义了http消息从请求,model binder,controller的激活,action的执行,返回response 等可复用的流程。同时还提供了每一个环节的可扩展点。 利用以上思想,我们对ReportService重新设计。 ### 新的设计 采用IoC思想重新设计该报表服务,将原来**客户端拥有的流程控制权反转在报表服务框架中**。`ReportService`这样的命名已经不适合我们的想法,新的实现不但提供了报表打印的相关服务,同时还提供了一个可复用的流程,因此重新命名为`ReportEngine`。我们可以通过[模板方法](https://en.wikipedia.org/wiki/Template_method_pattern)达到此目的: public class ReportEngine { private string _data; public ReportEngine(string data) { _data = data; } public void Show() { Trim(); Clean(); Display(); } public virtual void Trim() { _data = _data.Trim(); } public virtual void Clean() { _data = _data.Replace("@", ""); _data = _data.Replace("-", ""); } public virtual void Display() { Console.WriteLine(_data); } } 此时的报表服务在`Show()`方法中定义好了一组可复用的流程,客户端只需要根据自己的需求重写每个环节即可。客户端可以通过下面的方式使用ReportEngine var reportEngine=new StringReportEngine(input); reportEngine.Show(); ## 三、DI(Dependency Injection) DI即**依赖注入**,主要解决了2个问题: 1. 松耦合,由**DI容器**来创建对象,符合DIP原则; 2. 符合IoC的思想,整个应用程序事先定义好了一套可工作的流程,通过在客户端替换DI容器中的具体实现达到重写某个组件的目的; 除此之外,使用依赖注入还可以带来以下好处: * 促使你写出更加符合面向对象原则的代码,符合**优先使用对象组合,而不是继承**的原则; * 使系统更加具有可测试性; * 使系统更加具备可扩展性和可维护性; * 由于所有组件都由DI容器管理,所以可以很方便的实现AOP拦截 我记得之前在stackoverflow上看到过类似这样的一个问题: > 如何给5岁小孩解释什么叫DI? > 得分最高的答案是:小孩在饿的时候只需喊一声**我要吃饭**即可,而无需关注**吃什么**,**饭是怎么来的**等问题。 public class Kid { private readonly IFoodSupplier _foodSupplier; public Kid(IFoodSupplier foodSupplier) { _foodSupplier = foodSupplier; } public void HaveAMeal() { var food = _foodSupplier.GetFood(); //eat } } DI的背后是一个**DI Container(DI容器)**在发挥作用。DI之所以能够工作需要两个步骤: 1. 将组件注册到DI容器中; 2. DI容器统一管理所有依赖关系,将依赖组件注入到所需要相应的组件中; ### 3.1 组件的注册方式 组件注册到DI容器中有3种方式: 1. 通过XML文件注册 2. 通过Attribute(Annotation)注册 3. 通过DI容器提供的API注册 .net平台中的大多数DI框架都通过第三种方式进行组件注册,为了介绍这3种不同的注册方式,我们通过Java平台下的Spring框架简单介绍:Java中的Spring最早以XML文件的方式进行组件注册,发展到目前主要通过Annotation来注册。 假如我们有`CustomerRepository`接口和相应的实现`CustomerRepositoryImpl`,下面用三种不同的方式将`CustomerRepository`和`CustomerRepositoryImpl`的对应关系注册在DI容器中: public interface CustomerRepository { List findAll(); } public class CustomerRepositoryImpl implements CustomerRepository { public List findAll() { List customers = new ArrayList(); Customer customer = new Customer("Bryan","Hansen"); customers.add(customer); return customers; } } #### 3.1.1、xml文件注册 #### 3.1.2、Annotation注册 @Repository("customerRepository") public class CustomerRepositoryImpl implements CustomerRepository { public List findAll() { //... } } #### 3.1.3、通过Java代码来实现注册 @Configuration public class AppConfig { @Bean(name = "customerRepository") public CustomerRepository getCustomerRepository() { return new CustomerRepositoryImpl(); } } #### 3.1.4通过下面的方式从Container来获取一个实例 appContext.getBean("customerService", CustomerService.class); 一旦我们将所有组件都注册在容器中,就可以靠DI容器进行依赖注入了。 ### 3.2 依赖注入的三种方式 #### 3.2.1. 构造器注入 正如上面`Kid`的实现一样,我们通过构造器来注入`IFoodSupplier`组件,这种方式也是依赖注入最佳方式。 #### 3.2.2. 属性注入 public class Kid2 { public IFoodSupplier FoodSupplier { get; set; } public void HaveAMeal() { var food = FoodSupplier.GetFood(); //eat } } 即通过一个可读写的属性完成注入,该方案的缺点在于为了达到依赖注入的目的而破坏了对象的封装性,所以不推荐。 #### 3.2.3 方法注入 通过添加方法的参数来完成注入,一般来说这种方式都可以通过构造器注入的方式来替换,所以也不太常用。值得一提的是[asp.net core](https://github.com/aspnet)源码中用到了这种注入方式。 ## 四、依赖注入实例 ### 1、Register Resolve Release Pattern 下面描述了一个很简单的Console application, 所有的组件都通过Castle Windsor容器进行构造器注入: //register var container = new WindsorContainer(); container.Register(Component.For().ImplementedBy()); container.Register(Component.For().ImplementedBy()); container.Register(Component.For()); //resolve var application = container.Resolve(); application.Execute("hel--lo, wor--ld"); //release container.Release(application); 这个例子向我们展示了一个最简单的依赖注入使用方式,register所有组件,resolve客户端程序,最后的release步骤向我们展示了如果显示从DI容器得到一个对象,应该显示释放该组件。这一步在大多数情况下并不是必须的,但是在特定场景下会发生[内存泄漏](http://www.cnblogs.com/richieyang/p/3627131.html)。 ### 2、.net平台下依赖注入最佳实践 下面的解决方案描述了一个典型的应用程序分层结构,该分层结构用来描述如何使用`Catle windsor`进行依赖注入,注意:这并不是一个合理的领域驱动案例,例如我将`Domain`模型引用到了`Application`或者`ApplicationService`程序集中。 ![bestPractice](http://images.cnblogs.com/cnblogs_com/richieyang/669302/o_DI-3.png) 处在项目最底层的`Repository`程序集定义了一组`UserRepository`及其接口`IUserRepository`,这样的一个组件如何注册在Windsor Container中呢?Castle提供了一种叫做`WindsorInstaller`的机制: public class RepositoryInstaller:IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Component.For().ImplementedBy().LifestyleScoped()); } } 该Installer利用Fluent Api定义了`IUserRepository`和`UserRepository`的对应关系,相对于Java中的Spring框架提供的代码注册方式,该方案的优越之处是显而易见的。 **另外的重点在于该Installer此时并没有执行,只有当客户端调用此Installer时,该组件才真真注册进容器。**这一点很关键,我们后面还会提到。 接下来的`ApplicationService`层使用了`Repository`的抽象,一个典型的使用片断如下: public class UserApplicationService : IUserApplicationService { private readonly IUserRepository _userRepository; public UserApplicationService(IUserRepository userRepository) { _userRepository = userRepository; } public void Register(User user) { _userRepository.Save(user); } //..... } 我们通过构造器注入的方式注入了`IUserRepository`,同时,作为Service层,它也拥有自己的Installer: public class ApplicationServiceInstaller:IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register( Classes.FromThisAssembly().BasedOn().WithServiceDefaultInterfaces().LifestyleScoped()); } } 上面的例子示范了如何通过Castle提供的高级api来实现将该程序集中所有继承于`IApplicationService`的组件和其默认接口一次性全部注册到DI容器中。 比如`UserApplicationService`和`IUserApplicationService`,以及未来将要实现的`OrderApplicationService`以及`IOrderApplicationService`。 接下来到客户端程序集Application层,Application作为使用`ApplicationService`程序集的客户端,他才拥有将组件注册进DI容器的能力,我们定义一个`ApplicationBootstrap`来初始化DI容器并注册组件: public class ApplicationBootstrap { public static IWindsorContainer Container { get; private set; } public static IWindsorContainer RegisterComponents() { Container=new WindsorContainer(); Container.Install(FromAssembly.This()); Container.Install(FromAssembly.Containing()); Container.Install(FromAssembly.Containing()); return Container; } } 注意`Container.Install(...)`方法将执行不同应用程序的Installer,此时组件才真真注册进DI容器。该实例展示了如何正确的使用依赖注入框架: 1. 不同的程序集之间通过接口依赖,符合DIP原则; 2. 通过依赖注入的方式定义好了可运行的流程,但是客户端可以注册不同的组件到DI容器中,符合IoC的思想; ### 3、如何在asp.net mvc和asp.net webapi使用依赖注入 本文提供的源码中所含的`WebApplicationSample`项目演示了如何通过自定义`WindsorControllerFactory`来实现mvc的依赖注入,通过自定义`WindsorCompositionRoot`实现web api的依赖注入。 ## 五、高级进阶 asp.net core实现了一个还算简单的DI容器[DenpendencyInjection](https://github.com/aspnet/DependencyInjection),感兴趣的同学可以阅读器源码。 ## 六、源码下载 本文所描述的案例提供下载,[点击下载](https://git.oschina.net/richieyangs/LearningDI)