DIP原则、IoC以及DI
一、DIP原则
- 高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
该原则理解起来稍微有点抽象,我们可以将该原则通俗的理解为:"依赖于抽象”。
该规则告诉我们,程序中所有的依赖关系都应该终止于抽象类或者接口,从而达到松耦合的目的。因为我们在应用程序中编写的大多数具体类都是不稳定的。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在抽象和接口的后面,可以隔离它们的不稳定性。
举个例子
一个Button对象会触发Click方法,当被按下时,会调用Light对象的TurnOn方法,否则会调用Light对象的TurnOff方法。

这个设计存在两个问题:
- Button类直接依赖于Light类,这种依赖关系意味着当Light改变时,Button类会受到影响;
- 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原则。
打印报表作为一个可复用的流程,不但可以提供可复用的流程环节,还可以提供可复用的流程的定义,当我们进行框架设计的时候,往往会将整个流程控制定制在框架之中,然后提供扩展点供客户端定制。这样的思想体现了流程的所有权从客户端到框架的反转。
比如asp.net mvc或者asp.net api框架,内部定义了http消息从请求,model binder,controller的激活,action的执行,返回response
等可复用的流程。同时还提供了每一个环节的可扩展点。
利用以上思想,我们对ReportService重新设计。
新的设计
采用IoC思想重新设计该报表服务,将原来客户端拥有的流程控制权反转在报表服务框架中。ReportService这样的命名已经不适合我们的想法,新的实现不但提供了报表打印的相关服务,同时还提供了一个可复用的流程,因此重新命名为ReportEngine。我们可以通过模板方法达到此目的:
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个问题:
- 松耦合,由DI容器来创建对象,符合DIP原则;
- 符合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之所以能够工作需要两个步骤:
- 将组件注册到DI容器中;
- DI容器统一管理所有依赖关系,将依赖组件注入到所需要相应的组件中;
3.1 组件的注册方式
组件注册到DI容器中有3种方式:
- 通过XML文件注册
- 通过Attribute(Annotation)注册
- 通过DI容器提供的API注册
.net平台中的大多数DI框架都通过第三种方式进行组件注册,为了介绍这3种不同的注册方式,我们通过Java平台下的Spring框架简单介绍:Java中的Spring最早以XML文件的方式进行组件注册,发展到目前主要通过Annotation来注册。
假如我们有CustomerRepository接口和相应的实现CustomerRepositoryImpl,下面用三种不同的方式将CustomerRepository和CustomerRepositoryImpl的对应关系注册在DI容器中:
public interface CustomerRepository {
List<Customer> findAll();
}
public class CustomerRepositoryImpl implements CustomerRepository {
public List<Customer> findAll() {
List<Customer> customers = new ArrayList<Customer>();
Customer customer = new Customer("Bryan","Hansen");
customers.add(customer);
return customers;
}
}
3.1.1、xml文件注册
<bean name="customerRepository" class="com.thoughtworks.xml.repository.CustomerRepositoryImpl"/>
3.1.2、Annotation注册
@Repository("customerRepository")
public class CustomerRepositoryImpl implements CustomerRepository {
public List<Customer> 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源码中用到了这种注入方式。
四、依赖注入实例
1、Register Resolve Release Pattern
下面描述了一个很简单的Console application, 所有的组件都通过Castle Windsor容器进行构造器注入:
//register
var container = new WindsorContainer();
container.Register(Component.For<IParser>().ImplementedBy<Parser>());
container.Register(Component.For<IWriter>().ImplementedBy<Writer>());
container.Register(Component.For<Application>());
//resolve
var application = container.Resolve<Application>();
application.Execute("hel--lo, wor--ld");
//release
container.Release(application);
这个例子向我们展示了一个最简单的依赖注入使用方式,register所有组件,resolve客户端程序,最后的release步骤向我们展示了如果显示从DI容器得到一个对象,应该显示释放该组件。这一步在大多数情况下并不是必须的,但是在特定场景下会发生内存泄漏。
2、.net平台下依赖注入最佳实践
下面的解决方案描述了一个典型的应用程序分层结构,该分层结构用来描述如何使用Catle windsor进行依赖注入,注意:这并不是一个合理的领域驱动案例,例如我将Domain模型引用到了Application或者ApplicationService程序集中。

处在项目最底层的Repository程序集定义了一组UserRepository及其接口IUserRepository,这样的一个组件如何注册在Windsor Container中呢?Castle提供了一种叫做WindsorInstaller的机制:
public class RepositoryInstaller:IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component.For<IUserRepository>().ImplementedBy<UserRepository>().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<IApplicationService>().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<ApplicationServiceInstaller>());
Container.Install(FromAssembly.Containing<RepositoryInstaller>());
return Container;
}
}
注意Container.Install(...)方法将执行不同应用程序的Installer,此时组件才真真注册进DI容器。该实例展示了如何正确的使用依赖注入框架:
- 不同的程序集之间通过接口依赖,符合DIP原则;
- 通过依赖注入的方式定义好了可运行的流程,但是客户端可以注册不同的组件到DI容器中,符合IoC的思想;
3、如何在asp.net mvc和asp.net webapi使用依赖注入
本文提供的源码中所含的WebApplicationSample项目演示了如何通过自定义WindsorControllerFactory来实现mvc的依赖注入,通过自定义WindsorCompositionRoot实现web api的依赖注入。
五、高级进阶
asp.net core实现了一个还算简单的DI容器DenpendencyInjection,感兴趣的同学可以阅读其源码。
六、源码下载
本文所描述的案例提供下载,点击下载
DIP原则、IoC以及DI的更多相关文章
- 深入理解DIP、IoC、DI以及IoC容器
摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...
- 深入理解DIP、IoC、DI以及IoC容器(转)
深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...
- 【转】深入理解DIP、IoC、DI以及IoC容器
原文链接:http://www.cnblogs.com/liuhaorain/p/3747470.html 前言 对于大部分小菜来说,当听到大牛们高谈DIP.IoC.DI以及IoC容器等名词时,有没有 ...
- DIP、IoC、DI以及IoC容器
深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...
- 【来龙去脉系列】深入理解DIP、IoC、DI以及IoC容器
摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...
- 转:什么是DIP、IoC、DI
DIP依赖倒置原则DIP(Dependency-Inversion Principles) IoC控制反转(Inversion of Control,IoC),简言之就是代码的控制器交由系统控制,而不 ...
- 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解
1.概述 所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模 ...
- 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解(转)
所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合 ...
- 不可不知的DIP、IoC、DI以及IoC容器
面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.当中.OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念. 本文首先用实例阐述四个概 ...
- 深入理解DIP、IoC、DI以及IoC容器(转载)
<转载的这个up的其他的文章也很nice> 这几个词第一眼看,懵逼,第二眼看,更特么懵逼..... 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序. 其中,OOD有一 ...
随机推荐
- 【原】AFNetworking源码阅读(五)
[原]AFNetworking源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中提及到了Multipart Request的构建方法- [AFHTTP ...
- Consul 服务注册与服务发现
上一篇:Mac OS.Ubuntu 安装及使用 Consul 1. 服务注册 对 Consul 进行服务注册之前,需要先部署一个服务站点,我们可以使用 ASP.NET Core 创建 Web 应用程序 ...
- vue.js学习笔记
有了孩子之后,元旦就哪也去不了了(孩子太小),刚好利用一些时间,来公司充充电补补课,学习学习新技术,在这里做一个整理和总结.(选择的东西,既然热爱就把他做好吧!). 下来进入咱们的学习环节: 一.从H ...
- zookeeper源码分析之一服务端启动过程
zookeeper简介 zookeeper是为分布式应用提供分布式协作服务的开源软件.它提供了一组简单的原子操作,分布式应用可以基于这些原子操作来实现更高层次的同步服务,配置维护,组管理和命名.zoo ...
- MJRefresh 源码解读 + 使用
MJRefresh这个刷新控件是一款非常好用的框架,我们在使用一个框架的同时,最好能了解下它的实现原理,不管是根据业务要求在原有的基础上修改代码,还是其他的目的,弄明白作者的思路和代码风格,会受益匪浅 ...
- 设计模式之创建类模式大PK
创建类模式大PK 创建类模式包括工厂方法模式.建造者模式.抽象工厂模式.单例模式和原型模式,他们能够提供对象的创建和管理职责.其 ...
- Ajax使用WCF实现小票pos机打印源码
通过ajax跨域方式调用WCF服务,实现小票pos机的打印,源码提供web方式,客户端方式测试,服务驻留右侧底部任务栏,可控制服务开启暂停,用户可自定义小票打印模板,配合零售录入. qq 22945 ...
- [原创]关于Hibernate中的级联操作以及懒加载
Hibernate: 级联操作 一.简单的介绍 cascade和inverse (Employee – Department) Casade用来说明当对主对象进行某种操作时是否对其关联的从对象也作类似 ...
- BPM配置故事之案例4-子表
公司渐渐对采购管理重视起来了,新招聘了采购主管老李,老李对现有的申请表很不满意,要求将申请物资和申请原因改成物资明细表 物资明细表 小明只好继续致电大毛-- 大毛:把申请物资和申请原因删掉,新增一个数 ...
- 解决 Could not find com.android.tools.build:gradle 问题
今天拉同事最新的代码,编译时老是报如下错误: Error:Could not find com.android.tools.build:gradle:2.2.0.Searched in the fol ...