一、依赖倒置(Dependency Inversion Principle)

依赖倒置是面向对象设计领域的一种软件设计原则。(其他的设计原则还有:单一职责原则、开放封闭原则、里式替换原则、接口分离原则,合称SOLID)

话说设计原则有什么用呢?

设计原则是无数编程前辈总结下来的经验,好似编程界的金科玉律。在我看来就像是武侠小说中武林秘籍,内功心法。熟练掌握设计原则,必定会在编程道路上顺风顺水,独霸一方。

言归正传,依赖倒置原则,依赖指的是什么?倒置又是什么呢?

先说说依赖,依赖是一种关系,A在某种情况下存在对B的需求关系,我们就可以看作A依赖B。

在生活中,鱼依赖于水而生存,水被鱼依赖;程序中,业务层依赖逻辑层,逻辑层依赖于数据层...

我们用面向对象编程来展示一下上面依赖关系:

    /// <summary>
/// 河水
/// </summary>
public class RiverWater
{
public void GiveNutrition()
{
Console.WriteLine("我是河水,我给小鱼提供养分。");
}
}
/// <summary>
/// 鱼
/// </summary>
public class Fish
{
private RiverWater riverWater;
public void Live()
{
riverWater = new RiverWater();
riverWater.GiveNutrition();
}
}

Fish内部存在对RiverWater的引用,也就是说Fish 依赖于RiverWarter。

依赖关系整明白了,我们再来看看依赖倒置原则的定义:(敲黑板,划重点)

1.上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2.抽象不应该依赖于细节,细节应该依赖于抽象。

问题又来了,什么是上层模块和底层模块?

对于任何一个组织机构而言,它一定有架构的设计,有职能的划分。按照职能的重要性,自然而然就有了上下之分。并且,随着模块的粒度划分不同这种上层与底层模块会进行变动,也许某一模块相对于另外一模块它是底层,但是相对于其他模块它又可能是上层。拿一个公司架构来看,管理层就是上层,管理层之下就是底层。然后,我们再以部门为体系划分,各个部门经理以上部分是上层,之下的组织都可以称为底层。

由此,我们可以看到,在一个特定体系中,上层模块与底层模块可以按照决策能力高低为准绳进行划分。

映射到我们软件实际开发中,一般我们也会将软件进行模块划分,比如业务层、逻辑层和数据层。 业务层中是软件真正要进行的操作,也就是做什么;逻辑层是软件现阶段为了业务层的需求提供的实现细节,也就是怎么做;数据层指业务层和逻辑层所需要的数据模型。

因此,按照决策能力的高低进行模块划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。

什么是抽象和细节?

抽象就是对一类事物或行为的概括,总结其共性。抽象往往是相对具体而言,具体也就是这里的细节。比如:人是抽象,张三、李四就是具体;水是抽象,河水,井水就是具体的;武功秘籍是抽象的,独孤九剑,葵花宝典是具体的;运动是抽象的,跑步,游泳是具体的...

映射到软件开发中,抽象可以是接口或者抽象类的形式:

    public abstract class Water
{
public abstract void GiveNutrition();
}
/// <summary>
/// 河水
/// </summary>
public class RiverWater : Water
{
public override void GiveNutrition()
{
Console.WriteLine("河水-提供养分。");
}
}
/// <summary>
/// 井水
/// </summary>
public class WellWater : Water
{
public override void GiveNutrition()
{
Console.WriteLine("井水-提供养分。");
}
}

Warter是抽象类,是抽象的,RiverWarter、WellWater继承了Water,它们是具体的。

现在,搞清楚了上层模块、底层模块、抽象和具体。可以正式开始学习依赖倒置原则这个概念了。

先来看看我们平时开发的编码逻辑:

/// <summary>
/// 鱼
/// </summary>
public class Fish
{
private RiverWater riverWater;
public Fish()
{
riverWater = new RiverWater();
}
public void Live()
{
Console.WriteLine("我的生活靠:");
riverWater.GiveNutrition();
}
}

我们创建了一条小鱼fish,它的生活靠河水riverWater。  

    class Program
{
static void Main(string[] args)
{
Fish fish = new Fish();
fish.Live();
}
}

执行结果:

有一天,河水干涸了,小鱼的生活要靠井水wellWater。于是代码就要修改

/// <summary>
/// 鱼
/// </summary>
public class Fish
{
private RiverWater riverWater;
private WellWater wellWater;
public Fish()
{
//riverWater = new RiverWater();
wellWater = new WellWater();
}
public void Live()
{
Console.WriteLine("我的生活靠:");
//riverWater.GiveNutrition();
wellWater.GiveNutrition();
}
}

我们就要修改Fish类的代码。哪天,小鱼游到了湖水lakeWater里。代码又要修改

    /// <summary>
/// 湖水
/// </summary>
public class LakeWater : Water
{
public override void GiveNutrition()
{
Console.WriteLine("湖水-提供养分。");
}
}
/// <summary>
/// 鱼
/// </summary>
public class Fish
{
private RiverWater riverWater;
private WellWater wellWater;
private LakeWater lakeWater;
public Fish()
{
//riverWater = new RiverWater();
//wellWater = new WellWater();
lakeWater = new LakeWater();
}
public void Live()
{
Console.WriteLine("我的生活靠:");
//riverWater.GiveNutrition();
//wellWater.GiveNutrition();
lakeWater.GiveNutrition();
}
}

我们添加了LakeWater这个新的实现类,再次修改Fish类。

这是最基础的演示代码,如果工程大了,代码复杂了,Fish面对需求变动时改动的地方会更多。那么问题来了:

有没有方法让Fish类变动的少一些?

依赖倒置原则正好适用于解决这类情况。下面,我们尝试运用依赖倒置原则对代码进行改造。

首先是上层模块和底层模块的拆分。按照决策能力高低或者重要性划分,Fish属于上层模块,RiverWater、WellWater 和 LakeWater 属于底层模块。

上层模块不应该依赖于底层模块。 Fish 这个类显然是依赖于 RiveWater/WellWater/LakeWater。Fish 类中 Live() 的能力完全依赖于属性riveWater/wellWater/lakeWater 对象。

上层和底层都应该依赖于抽象。因此我们要引入抽象——Water类。Fish类中 Live() 这个方法依赖于 Water的抽象方法,它没有限定养分的提供方式,任何 RiverWater、WellWater 或者是 LakeWater 都可以的。

    /// <summary>
/// 鱼
/// </summary>
public class Fish
{
private Water water;
public Fish()
{
water = new LakeWater();
}
public void Live()
{
Console.WriteLine("我的生活靠:");
warter.GiveNutrition();
}
}

运行结果:

到这一步,我们可以说是符合了上层不依赖于底层,依赖于抽象的准则了。

最后来说说我对倒置的理解:在未使用依赖倒置原则编码以前,鱼依赖具体的河水/井水,河水/井水是被依赖的。使用依赖倒置以后,鱼依赖于抽象的水,具体的河水/井水不再被依赖,反而它们要求实现抽象的水(细节依赖于抽象),这种依赖关系的改变称之为倒置。

二、控制反转(Inversion Of Control)

控制反转(IOC)意思是对控制权的反转。

那么控制权指的是什么?又是怎么反转的?

以上面的例子来说,鱼(Fish类)依赖于水(Water类),所以Fish类内部控制着Water类的实例创建,这种方式可以理解为控制正转。虽然鱼已经依赖于抽象的水,Live()方法不会再因为生活水域的改变而改变,但水域变化时,我们还是要修改Fish类:

==>

现在我们改变这种方式,将Water类的实例化移到Fish外面:

    /// <summary>
/// 鱼
/// </summary>
public class Fish
{
private Water water;
public Fish(Water _water)
{
water = _water;
}
public void Live()
{
Console.WriteLine("我的生活靠:");
water.GiveNutrition();
}
}
class Program
{
static void Main(string[] args)
{
Water water = new RiverWater();
Fish fish = new Fish(water);
fish.Live();
Console.ReadKey();
}
}

这样,不论水域怎么变化,Fish 列都不需要修改了。Fish 把内部依赖的创建权力移交给了 Program 这个类中的 Main() 方法。也就是说 Fish 只关心依赖提供的功能,但并不关心依赖的创建。

这种思想其实就是 IOC,IOC 是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。比如上面代码,Fish 不再亲自创建 Water 对象,它将依赖的实例化的权力交接给了 Program。而 Program 在 IOC 中又指代了 IOC 容器 这个概念。

IOC 模式最核心的地方就是在于依赖方与被依赖方之间,也就是上文中说的上层模块与底层模块之间引入了第三方,这个第三方统称为 IOC 容器,因为 IOC 容器的介入,导致上层模块对于它的依赖的实例化控制权发生变化,也就是所谓的控制反转的意思。

三、依赖注入(Dependency Injection)

依赖注入(DI),它是实现IOC的实现方式,动态地将某种依赖关系注入到对象之中。

回顾上面的例子,Fish 不在实例化创建Water,它就需要在外部(IOC 容器)赋值给它,这个赋值的动作有个专门的术语叫做注入(injection)。类似于,鱼生活在鱼缸里了,外接一个水龙头,水龙头另一头连接着水塔,这个水塔就好比是IOC容器,鱼不用关心生活在什么水里了,需要水时,打开水龙头(注入)就好了,至于水塔里装的是河水、井水还是海水,都不用考虑。

实现依赖注入有 3 种方式:

1. 构造函数中注入

    public class Fish
{
private Water water;
public Fish(Water _water)
{
water = _water;
}
public void Live()
{
Console.WriteLine("我的生活靠:");
water.GiveNutrition();
}
}

优点:在 Person 一开始创建的时候就确定好了依赖。

缺点:后期无法更改依赖。

2. setter 方式注入 

    public class Fish
{
private Water water;
public Fish()
{
}
public void setWater(Water _water)
{
water = _water;
}
public void Live()
{
if (water != null)
{
Console.WriteLine("我的生活靠:");
water.GiveNutrition();
}
}
}

优点:Fish 对象在运行过程中可以灵活地更改依赖。

缺点:Fish 对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态

3. 接口注入

    public class Fish : ISetWater
{
private Water water;
public Fish()
{
}
public void Live()
{
if (water != null)
{
Console.WriteLine("我的生活靠:");
water.GiveNutrition();
}
} public void SetWater(Water _water)
{
water = _water;
}
} public interface ISetWater
{
void SetWater(Water water);
}

这种方式和 Setter 方式很相似,接口的存在,表明了一种依赖配置的能力。比如,鱼有 活鱼和死鱼,接口注入方式,我们就可以控制,只给活鱼注入配置。

四、总结

  1. 依赖倒置是面向对象开发领域中的软件设计原则,它倡导上层模块不依赖于底层模块,抽象不依赖细节。
  2. 依赖反转是遵守依赖倒置这个原则而提出来的一种设计模式,它引入了 IoC 容器的概念。
  3. 依赖注入是为了实现依赖反转的一种手段之一。
  4. 它们的本质是为了代码更加的“高内聚,低耦合”。

个人对【依赖倒置(DIP)】、【控制反转(IOC)】、【依赖注入(DI)】浅显理解的更多相关文章

  1. java依赖的斗争:依赖倒置、控制反转和依赖注入

    控制反转(Inversion Of Controller)的一个著名的同义原则是由Robert C.Martin提出的依赖倒置原则(Dependency Inversion Principle),它的 ...

  2. 依赖倒置原则DIP&控制反转IOC&依赖注入DI

    依赖倒置原则DIP是软件设计里一个重要的设计思想,它规定上层不依赖下层而是共同依赖抽象接口,通常可以是上层提供接口,然后下层实现接口,上下层之间通过接口完全透明交互.这样的好处,上层不会因依赖的下层修 ...

  3. spring(3)------控制反转(IOC)/依赖注入(DI)

    一.spring核心概念理解 控制反转: 控制反转即IoC (Inversion of Control).它把传统上由程序代码直接操控的对象的调用权交给容器.通过容器来实现对象组件的装配和管理. 所谓 ...

  4. PHP依赖倒置和控制反转

    判断代码的好坏,我们有自己的标准:高内聚,低耦合.为了解决这一问题,php中有许多优秀的设计模式,比如工厂模式,单例模式. 而在代码中体现出来的设计模式,就如依赖注入和控制反转. 那什么是依赖注入? ...

  5. Spring 之 控制反转(IoC), 依赖注入(DI)和面向切面(AOP)

    关于依赖注入, 这篇博文写的非常简单易懂. https://github.com/android-cn/blog/tree/master/java/dependency-injection 此外, 博 ...

  6. 控制反转IOC的依赖注入方式

    引言: 项目中遇到关于IOC的一些内容,因为和正常的逻辑代码比较起来,IOC有点反常.因此本文记录IOC的一些基础知识,并附有相应的简单实例,而在实际项目中再复杂的应用也只是在基本应用的基础上扩展而来 ...

  7. ASP.NET MVC 中 Autofac依赖注入DI 控制反转IOC 了解一下

    先简单了解一这个几个 名词的意思. 控制反转(IOC) 依赖注入(DI) 并不是某种技术. 而是一种思想.一种面向对象编程法则 什么是控制反转(IOC)?  什么是依赖注入(DI) 可以点击下面链接 ...

  8. ADO.NET .net core2.0添加json文件并转化成类注入控制器使用 简单了解 iTextSharp实现HTML to PDF ASP.NET MVC 中 Autofac依赖注入DI 控制反转IOC 了解一下 C# AutoMapper 了解一下

    ADO.NET   一.ADO.NET概要 ADO.NET是.NET框架中的重要组件,主要用于完成C#应用程序访问数据库 二.ADO.NET的组成 ①System.Data  → DataTable, ...

  9. 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) 依赖注入和控制反转的理解,写的太好了。

    轻松学,浅析依赖倒置(DIP).控制反转(IOC)和依赖注入(DI) 2017年07月13日 22:04:39 frank909 阅读数:14269更多 所属专栏: Java 反射基础知识与实战   ...

随机推荐

  1. 自学java难吗?一个JAVA学习者应该具备的素质

    无论是在校的学生也好,还是转行的也好,如今学JAVA开发的人越来越多,造成了如今新手越来越多,有人说前端饱和了,JAVA饱和了,JAVA才刚开始以一种好的势头发展就饱和了.我也是无语,一般说饱和的人, ...

  2. 使用MongoDB在项目中实际运用

    一.MongoDB,一个数据库,我们怎么去使用它呢?我们首先了解一下什么是MongoDb 官网的介绍是:MongoDB是专为可扩展性,高性能和高可用性而设计的数据库.它可以从单服务器部署扩展到大型.复 ...

  3. Python:轻量级 ORM 框架 peewee 用法详解(二)——增删改查

    说明:peewee 中有很多方法是延时执行的,需要调用 execute() 方法使其执行.下文中不再特意说明这个问题,大家看代码. 本文中代码样例所使用的 Person 模型如下: class Per ...

  4. 目标检测之YOLO V2 V3

    YOLO V2 YOLO V2是在YOLO的基础上,融合了其他一些网络结构的特性(比如:Faster R-CNN的Anchor,GooLeNet的\(1\times1\)卷积核等),进行的升级.其目的 ...

  5. Fast R-CNN中的边框回归

    前面对R-CNN系的目标检测方法进行了个总结,其中对目标的定位使用了边框回归,当时对这部分内容不是很理解,这里单独学习下. R-CNN中最后的边框回归层,以候选区域(Region proposal)为 ...

  6. Python基础 列表介绍、使用

    第3章 学习目标: 列表是什么以及如何使用列表元素.列表让你能够在一个地方存储成组的信息,其中可以只包含几个元素,也可以包含数百万个元素.列表是新手可直接使用的最强大的Python功能之一,它融合了众 ...

  7. xss攻击和csrf攻击的定义及区别

    1.CSRF的基本概念.缩写.全称 CSRF(Cross-site request forgery):跨站请求伪造. PS:中文名一定要记住.英文全称,如果记不住也拉倒. 2.CSRF的攻击原理 用户 ...

  8. Python之路【第四篇】:Python基础之函数

    函数的理解 面向过程:根据业务逻辑从上到下垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 函数作用是你的程序有良好的扩展性.复用性. 同样的功能要是用3次以上的话就建议使 ...

  9. mssql sqlserver 三种数据表数据去重方法分享

    摘要: 下文将分享三种不同的数据去重方法数据去重:需根据某一字段来界定,当此字段出现大于一行记录时,我们就界定为此行数据存在重复. 数据去重方法1: 当表中最在最大流水号时候,我们可以通过关联的方式为 ...

  10. 浅析C语言中的整形类型

    在C语言中,可以把 字符型.短整形.整形.长整形都看作是整形,同属于整形家族这个大类型. 这些类型的大小,默认是否有符号等一些知识点较零散,较容易混淆,所以特地整理如下.   一 类型存储字节长度说明 ...