一直以来就是越难的东西越值钱!

嘿嘿,这篇博文章转载自:http://www.cnblogs.com/liuhaorain/p/3747470.html

摘要

  面向对象设计(OOD)有助于我们开发出高性能、易扩展和易复用的程序。其中,OOD有一个重要的思想就是依赖倒置原则(DIP),并由此引申出IoC、DI以及Ioc容器等概念。通过这篇文章我们将一起学习这些概念,并理清他们之间微妙的关系。

前言

我们先来大致的了解一下这些概念:

  1. 依赖倒置原则(DIP):这是一种软件架构设计的原则(抽象概念)。
  2. 控制反转(IoC):这是一种反转流、依赖和接口的方式(DIP的具体实现方式)。
  3. 依赖注入(DI):这是一种IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。
  4. IoC容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)。

依赖倒置原则(DIP)

在讲概念之前,我们先看生活中的一个例子。

  在我们的现实生活中,只要有一张银行卡,随便到哪一家银行的ATM都能取钱。在这个场景中,ATM相当于高层模块,而银行卡相当于低层模块。ATM定义了一个插口(接口),供所有的银行卡插入使用。也就是说,ATM不依赖于具体的哪种银行卡。它只需定义好银行卡的规格参数(接口),所有实现了这些规格参数的银行卡都能在ATM上使用。现实生活如此,软件开发更是如此。依赖倒置原则,它转换了二者之间的依赖关系,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口。通俗的讲,就是高层模块负责定义接口,低层模块负责实现这些接口。

现实生活中的实例不足以说明依赖倒置原则的重要性,那下面我们将通过软件开发的场景来理解为什么要使用依赖倒置原则。

场景一  依赖无倒置(低层模块定义接口,高层模块负责实现)

  从上图中,我们发现高层模块的类依赖于低层模块的接口。因此,低层模块需要考虑到所有的接口。如果有新的低层模块类出现时,高层模块需要修改代码,来实现新的低层模块的接口。这样,就破坏了开放封闭原则。

场景二 依赖倒置(高层模块定义接口,低层模块负责实现)

  在这个图中,我们发现高层模块定义了接口,将不再直接依赖于低层模块,低层模块负责实现高层模块定义的接口。这样,当有新的低层模块实现时,不需要修改高层模块的代码。

  由此,我们可以总结出使用DIP的优点:

  • 系统更柔韧:可以修改一部分代码而不影响其他模块。
  • 系统更健壮:不会因为修改一部分代码而导致系统崩溃
  • 系统更高效:组件之间松耦合,且可复用,提高开发效率。

控制反转(IoC)

  DIP是一种 软件设计原则,它仅仅告诉我们两个模块之间应该如何依赖,但是它并没有告诉我们如何去做。IoC则是一种 软件设计模式,它告诉我们应该如何做,来解除相互依赖模块的耦合。控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制即依赖对象不在被依赖模块的类中直接通过new来获取。在图1的例子我们可以看到,ATM它自身并没有插入具体的银行卡(工行卡、农行卡等等),而是将插卡工作交给人来控制,即我们来决定将插入什么样的银行卡来取钱。同样我们也通过软件开发过程中场景来加深理解。

  我个人的理解就是:假如我们现在要从A地去到B地,设计原则的主要作用就是给我们指出来从A到B我们应该往那个方向走,那个方向是正确的;设计模式就相当于已经有很多人从A走到B了,但是可能大家 走到路线不是太一样,但是相对来说总要有一条比较好的路能即快又安全的到达目的地,这条路就能够为后来再去B点的人提供一个指导。

  做过电商网站的人都会面临这样一个问题:订单入库。假设系统设计初期,用的是SQL Server数据库。通常我们会定义一个SqlServerDal类,用于数据库的读写。

 public class SqlServerDal
{
public void Add()
{
Console.WriteLine("在数据库中添加一条订单!");
}
}

  然后我们定义一个Order类,负责订单的逻辑处理。由于订单要入库,需要依赖于数据库的操作。因此在Order类中,我们需要定义SqlServerDal类的变量并初始化。

 public class Order
{
private readonly SqlServerDal dal = new SqlServerDal();//添加一个私有变量保存数据库操作的对象 public void Add()
{
dal.Add();
}
}

  最后,我们写一个控制台程序来检验成果。

 class Program
{
static void Main(string[] args)
{
Order order = new Order();
order.Add(); Console.Read();
}
}

  然后运行就能输出结果。

  OK,结果看起来挺不错的!正当你沾沾自喜的时候,这时BOSS过来了。“小刘啊,刚客户那边打电话过来说数据库要改成Access”,“对你来说,应当小CASE啦!”BOSS又补充道。带着自豪而又纠结的情绪,我们思考着修改代码的思路。

   由于换成了Access数据库,SqlServerDal类肯定用不了了。因此,我们需要新定义一个AccessDal类,负责Access数据库的操作。

 public class AccessDal
{
public void Add()
{
Console.WriteLine("在ACCESS数据库中添加一条记录!");
}
}

  然后,再看Order类中的代码。由于,Order类中直接引用了SqlServerDal类的对象。所以还需要修改引用,换成AccessDal对象。

 public class Order
{
private readonly AccessDal dal = new AccessDal();//添加一个私有变量保存数据库操作的对象 public void Add()
{
dal.Add();
}
}

  控台程序不变,如下:

 class Program
{
static void Main(string[] args)
{
Order order = new Order();
order.Add(); Console.Read();
}
}

  输出结果:

  费了九牛二虎之力,程序终于跑起来了!试想一下,如果下次客户要换成MySql数据库,那我们是不是还得重新修改代码?

  显然,这不是一个良好的设计,组件之间高度耦合,可扩展性较差,它违背了DIP原则。高层模块Order类不应该依赖于低层模块SqlServerDal,AccessDal,两者应该依赖于抽象。那么我们是否可以通过IoC来优化代码呢?答案是肯定的。IoC有2种常见的实现方式:依赖注入和服务定位。其中,依赖注入使用最为广泛。下面我们将深入理解依赖注入(DI),并学会使用。

依赖注入(DI)

  控制反转(IoC)一种重要的方式,就是将被依赖对象的创建和绑定转移到依赖对象类的外部来实现。在上述的实例中,Order类所依赖的对象SqlServerDal的创建和绑定是在Order类内部进行的。事实证明,这种方法并不可取。既然,不能在Order类内部直接绑定依赖关系,那么如何将SqlServerDal对象的引用传递给Order类使用呢?

依赖注入(DI):提供一种机制,将被依赖(低层模块)对象的引用传递给依赖(高层模块)对象。通过DI,我们可以在Order类的外部将SqlServerDal对象的引用传递给Order类对象。那么具体是如何实现呢?

方法一 构造函数注入

  构造函数函数注入,毫无疑问就是通过构造函数传递依赖。因此,构造函数的参数必然用来接收一个依赖对象。那么参数的类型是什么呢?是具体依赖对象的类型还是一个抽象类型?根据DIP原则,我们知道高层模块不应该依赖于低层模块,两者应该依赖于抽象。那么构造函数的参数应该是一个抽象类型。我们再回到上面那个问题,如何将SqlServerDal对象的引用传递给Order类使用呢

首选,我们需要定义SqlServerDal的抽象类型IDataAccess,并在IDataAccess接口中声明一个Add方法。

 public interface IDataAccess
{
void Add();
}

然后在SqlServerDal类中,实现IDataAccess接口。

 public class SqlServerDal:IDataAccess
{
public void Add()
{
Console.WriteLine("在数据库中添加一条订单!");
}
}

接下来,我们还需要修改Order类。

 public class Order
{
private IDataAccess _ida;//定义一个私有变量保存抽象 //构造函数注入
public Order(IDataAccess ida)
{
_ida = ida;//传递依赖
} public void Add()
{
_ida.Add();
}
}

OK,我们再来编写一个控制台程序。

 class Program
{
static void Main(string[] args)
{
SqlServerDal dal = new SqlServerDal();//在外部创建依赖对象
Order order = new Order(dal);//通过构造函数注入依赖 order.Add(); Console.Read();
}
}

输出结果:

  从上面代码中我们可以看出,我们将依赖对象SqlServerDal对象的创建和绑定转移到Order类外部来实现,这样就解除了SqlServerDal和Order类的耦合关系。当我们数据库换成Access数据库时,只需定义一个AccessDal类,然后外部重新绑定依赖,不需要修改Order类内部代码,则可实现Access数据库的操作。

定义AccessDal类:

 public class AccessDal:IDataAccess
{
public void Add()
{
Console.WriteLine("在ACCESS数据库中添加一条记录!");
}
}

然后在控制台程序中重新绑定依赖关系:

 class Program
{
static void Main(string[] args)
{
AccessDal dal = new AccessDal();//在外部创建依赖对象
Order order = new Order(dal);//通过构造函数注入依赖 order.Add(); Console.Read();
}
}

输出结果:

显然,我们不需要修改Order类的代码,就完成了Access数据库的移植,这无疑体现了IoC的精妙。

方法二 属性注入

顾名思义,属性注入是通过属性来传递依赖。因此,我们首先需要在依赖类Order中定义一个属性:

 public class Order
{
private IDataAccess _ida;//定义一个私有变量保存抽象 //属性,接受依赖
public IDataAccess Ida
{
set { _ida = value; }
get { return _ida; }
} public void Add()
{
_ida.Add();
}
}

然后在控制台程序中,给属性赋值,从而传递依赖:

 class Program
{
static void Main(string[] args)
{
AccessDal dal = new AccessDal();//在外部创建依赖对象
Order order = new Order();
order.Ida = dal;//给属性赋值 order.Add(); Console.Read();
}
}

我们可以得到上述同样的结果。

  这是原文章的写法,但是我个人的理解这个属性注入方法应该和setter注入方法是一个意思,我没有看懂上边这个Order中的代码,但我通过其他资料理解了如何实现setter方法注入:针对这个例子,我们可以在Order类中设置一个setIDataAccess(IDataAccess _ida)的方法,还要设置一个private IDataAccess _ida;//定义一个私有变量保存抽象,这样我们就可以Order类外部实现对依赖类引用的注入。

方法三 接口注入

  相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见(但我个人觉得这个和构造函数注入的区别不是太大,最主要的不同点就是在接口注入方法中被依赖的高层模块的类还需要实现一个接口,而在构造函数注入方法中直接把被依赖对象作为构造函数的参数传进去了)。具体思路是先定义一个接口,包含一个设置依赖的方法。然后依赖类,继承并实现这个接口。

首先定义一个接口:

 public interface IDependent
{
void SetDependence(IDataAccess ida);//设置依赖项
}

依赖类实现这个接口:

 public class Order : IDependent
{
private IDataAccess _ida;//定义一个私有变量保存抽象 //实现接口
public void SetDependence(IDataAccess ida)
{
_ida = ida;
} public void Add()
{
_ida.Add();
} }

控制台程序通过SetDependence方法传递依赖:

 class Program
{
static void Main(string[] args)
{
AccessDal dal = new AccessDal();//在外部创建依赖对象
Order order = new Order(); order.SetDependence(dal);//传递依赖 order.Add(); Console.Read();
}
}

我们同样能得到上述的输出结果。

  虽说上述三种注入形式避免了修改Order类,但仍然避免不了修改控制台输出的方法。这样来看,减轻了order类的负担,但控制台输出端就必须判断使用的数据库,因此:上述三种注入形式的代码违背了设计模式的开闭原则,仍然有不小的瑕疵,如果解决这个问题呢?

IoC容器

前面所有的例子中,都需要通过手动的方式来创建依赖对象,并将引用传递给被依赖模块。比如:

SqlServerDal dal = new SqlServerDal();//在外部创建依赖对象
Order order = new Order(dal);//通过构造函数注入依赖

对于大型项目来说,相互依赖的组件比较多。如果还用手动的方式,自己来创建和注入依赖的话,显然效率很低,而且往往还会出现不可控的场面。正因如此,IoC容器诞生了。IoC容器实际上是一个DI框架,它能简化我们的工作量。它包含以下几个功能:

  • 动态创建、注入依赖对象。
  • 管理对象生命周期。
  • 映射依赖关系。

目前,比较流行的Ioc容器有以下几种:Ninject、Castle Windsor、Autofac、StructureMap、Unity、Spring.NET、LightInject 等

学习 IOC 设计模式前必读:依赖注入的三种实现的更多相关文章

  1. ASP.NET MVC中使用Unity进行依赖注入的三种方式

    在ASP.NET MVC中使用Unity进行依赖注入的三种方式 2013-12-15 21:07 by 小白哥哥, 146 阅读, 0 评论, 收藏, 编辑 在ASP.NET MVC4中,为了在解开C ...

  2. spring的ioc依赖注入的三种方法(xml方式)

    常见的依赖注入方法有三种:构造函数注入.set方法注入.使用P名称空间注入数据.另外说明下注入集合属性 先来说下最常用的那个注入方法吧. 一.set方法注入 顾名思义,就是在类中提供需要注入成员的 s ...

  3. 在ASP.NET MVC中使用Unity进行依赖注入的三种方式

    在ASP.NET MVC4中,为了在解开Controller和Model的耦合,我们通常需要在Controller激活系统中引入IoC,用于处理用户请求的 Controller,让Controller ...

  4. Spring依赖注入的三种方式

    看过几篇关于Spring依赖注入的文章,自己简单总结了一下,大概有三种方式: 1.自动装配 通过配置applicationContext.xml中的标签的default-autowire属性,或者标签 ...

  5. spring4之依赖注入的三种方式

    1.Setter注入 <bean id="helloWorld" class="com.jdw.spring.beans.HelloWorld"> ...

  6. SSH深度历险记(八) 剖析SSH核心原则+Spring依赖注入的三种方式

           于java发育.一类程序猿必须依靠类的其他方法,它是通常new依赖类的方法,然后调用类的实例,这样的发展问题new良好的班统一管理的例子.spring提出了依赖注入的思想,即依赖类不由程 ...

  7. Spring注解依赖注入的三种方式的优缺点以及优先选择

    当我们在使用依赖注入的时候,通常有三种方式: 1.通过构造器来注入: 2.通过setter方法来注入: 3.通过filed变量来注入: 那么他们有什么区别吗?应该选择哪种方式更好? 三种方式的区别小结 ...

  8. SSH深度历险(八) 剖析SSH核心原理+Spring依赖注入的三种方式

           在java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依 ...

  9. Spring基础05——Spring依赖注入的三种方式

    Spring支持3种依赖注入的方式:属性注入.构造器注入.工厂 1.属性注入 属性注入即通过setter方法注入Bean的属性或依赖的对象.使用<property>元素,使用name属性指 ...

随机推荐

  1. C#学习-析构函数

    析构函数用于在类销毁之前释放类实例所使用的托管和非托管资源. 对于C#应用程序所创建的大多数对象,可以依靠.NET Framework的垃圾回收器(GC)来隐式地执行内存管理任务. 但是,若创建封装了 ...

  2. jquery .On()绑定事件的触发机制

    选择器只能选择已存在元素,其他元素需要作为参数传递给on

  3. 关于文件I/o的原子操作

    [摘自<Linux/Unix系统编程手册>] 所有系统调用都是以原子操作方式执行的.这里是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性执行,其间不会为其它进程或线程所中断. 原 ...

  4. windows配置tomcat https访问

    主要参考两篇: http://blog.csdn.net/minute_man/article/details/53787682 http://blog.csdn.net/chow__zh/artic ...

  5. P1233 木棍加工 dp LIS

    题目描述 一堆木头棍子共有n根,每根棍子的长度和宽度都是已知的.棍子可以被一台机器一个接一个地加工.机器处理一根棍子之前需要准备时间.准备时间是这样定义的: 第一根棍子的准备时间为1分钟: 如果刚处理 ...

  6. rest模式get,post,put,delete简单讲解

    1.请求方法为get时,会向数据库请求数据,类似于select语言,只能达到查询的效果,不会添加,修改,不会影响资源的内容,无副作用 2.请求方法为post时,该方法,用于向服务器添加数据,可以改变数 ...

  7. maven聚合工程无法install

    对于maven聚合工程,有时候执行maven命令进行mvn clean install时会出一些莫名奇妙的错误: 一直报告找不到符号,仔细看了项目源代码也不知道是什么原因.首先确保项目所在路径为英文路 ...

  8. html 水平竖直居中

    line-height:容器高度 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...

  9. Sort功能极强!

    Sort功能极强! 可以排string:  sort(a.begin(),a.end()); 普通数组 结合结构体 逆序 而且贼快

  10. shell && and ||

    2013-04-08 17:40:47   shell中&&和||的使用方法 &&运算符:   command1  && command2   & ...