个人对【依赖倒置(DIP)】、【控制反转(IOC)】、【依赖注入(DI)】浅显理解
一、依赖倒置(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 方式很相似,接口的存在,表明了一种依赖配置的能力。比如,鱼有 活鱼和死鱼,接口注入方式,我们就可以控制,只给活鱼注入配置。
四、总结
- 依赖倒置是面向对象开发领域中的软件设计原则,它倡导上层模块不依赖于底层模块,抽象不依赖细节。
- 依赖反转是遵守依赖倒置这个原则而提出来的一种设计模式,它引入了 IoC 容器的概念。
- 依赖注入是为了实现依赖反转的一种手段之一。
- 它们的本质是为了代码更加的“高内聚,低耦合”。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 引入依赖倒置 { 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("井水-提供养分。"); } } /// <summary> /// 鱼 /// </summary> public class Fish { private RiverWater riverWater; private WellWater wellWater; public Fish() {//Fish内部存在对RiverWater的引用,也就是说Fish 依赖于RiverWarter。 //riverWater = new RiverWater(); wellWater = new WellWater(); } public void Live() { Console.WriteLine("我的生活靠:"); //riverWater.GiveNutrition(); wellWater.GiveNutrition(); } } public class LakeWater : Water { public override void GiveNutrition() { Console.WriteLine("湖水-提供养分。"); } } class Program { static void Main(string[] args) { Fish fish = new Fish(); fish.Live(); //小鱼的生活要靠井水wellWater。于是代码就要修改 //代码又要修改 //我们添加了LakeWater这个新的实现类,再次修改Fish类。 //这是最基础的演示代码,如果工程大了,代码复杂了,Fish面对需求变动时改动的地方会更多。那么问题来了: //有没有方法让Fish类变动的少一些? //依赖倒置原则正好适用于解决这类情况。下面,我们尝试运用依赖倒置原则对代码进行改造。 //首先是上层模块和底层模块的拆分。按照决策能力高低或者重要性划分,Fish属于上层模块,RiverWater、WellWater 和 LakeWater 属于底层模块。 //上层模块不应该依赖于底层模块。 Fish 这个类显然是依赖于 RiveWater/WellWater/LakeWater。Fish 类中 Live() 的能力完全依赖于属性riveWater/wellWater/lakeWater 对象。 //上层和底层都应该依赖于抽象。因此我们要引入抽象——Water类。Fish类中 Live() 这个方法依赖于 Water的抽象方法,它没有限定养分的提供方式,任何 RiverWater、WellWater 或者是 LakeWater 都可以的。 } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 依赖倒置 { //依赖倒置是面向对象设计领域的一种软件设计原则。(其他的设计原则还有:单一职责原则、开放封闭原则、里式替换原则、接口分离原则,合称SOLID) //依赖是一种关系,A在某种情况下存在对B的需求关系,我们就可以看作A依赖B。 //鱼依赖于水而生存,水被鱼依赖;程序中,业务层依赖逻辑层,逻辑层依赖于数据层... /// <summary> /// 鱼 /// </summary> public class Fish { private Water water; public Fish() { //减少修改 复用 water = new LakeWater(); } //private RiverWater riverWater; public void Live() //依赖倒置 {//Fish内部存在对RiverWater的引用,也就是说Fish 依赖于RiverWarter。 Console.WriteLine("我的生活靠:"); water.GiveNutrition(); } //上层不依赖于底层 依赖于抽象的准则了。 依赖water //最后来说说我对倒置的理解:在未使用依赖倒置原则编码以前,鱼依赖具体的河水/井水,河水/井水是被依赖的。使用依赖倒置以后,鱼依赖于抽象的水,具体的河水/井水不再被依赖,反而它们要求实现抽象的水(细节依赖于抽象),这种依赖关系的改变称之为倒置。 // 1.上层模块不应该依赖底层模块,它们都应该依赖于抽象。 //2.抽象不应该依赖于细节,细节应该依赖于抽象。 } public abstract class Water { public abstract void GiveNutrition(); } public class RiverWater : Water { public override void GiveNutrition() { Console.WriteLine("河水-提供养分。"); } } public class LakeWater : Water { public override void GiveNutrition() { Console.WriteLine("湖水-提供养分。"); } } public class WellWater : Water { public override void GiveNutrition() { Console.WriteLine("井水-提供养分。"); } } //Warter是抽象类,是抽象的,RiverWarter、WellWater继承了Water,它们是具体的。 //现在,搞清楚了上层模块、底层模块、抽象和具体。可以正式开始学习依赖倒置原则这个概念了。 class Program { static void Main(string[] args) { Fish F = new Fish(); F.Live(); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 控制反转 { public abstract class Water { public abstract void GiveNutrition(); } /// <summary> /// 河水 /// </summary> public class RiverWater : Water { public override void GiveNutrition() { Console.WriteLine("河水-提供养分。"); } } public class LakeWater : Water { public override void GiveNutrition() { Console.WriteLine("湖水-提供养分。"); } } /// <summary> /// 井水 /// </summary> public class WellWater : Water { public override void GiveNutrition() { Console.WriteLine("井水-提供养分。"); } } public class Fish { private Water water; public Fish(Water _water) { //water = new LakeWater(); water = _water; } //现在我们改变这种方式,将Water类的实例化移到Fish外面: public void Live() { Console.WriteLine("我的生活靠:"); water.GiveNutrition(); } } class Program { static void Main(string[] args) { // 控制反转(IOC)意思是对控制权的反转。 //以上面的例子来说,鱼(Fish类)依赖于水(Water类),所以Fish类内部控制着Water类的实例创建,这种方式可以理解为控制正转。虽然鱼已经依赖于抽象的水,Live()方法不会再因为生活水域的改变而改变,但水域变化时,我们还是要修改Fish类: 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 容器的介入,导致上层模块对于它的依赖的实例化控制权发生变化,也就是所谓的控制反转的意思。 //业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层 //管理层就是上层,管理层之下就是底层 } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 依赖注入 { public abstract class Water { public abstract void GiveNutrition(); } /// <summary> /// 河水 /// </summary> public class RiverWater : Water { public override void GiveNutrition() { Console.WriteLine("河水-提供养分。"); } } public class LakeWater : Water { public override void GiveNutrition() { Console.WriteLine("湖水-提供养分。"); } } /// <summary> /// 井水 /// </summary> public class WellWater : Water { public override void GiveNutrition() { Console.WriteLine("井水-提供养分。"); } } //实现依赖注入有 3 种方式: //构造函数中注入 //优点:在 Person 一开始创建的时候就确定好了依赖。 //缺点:后期无法更改依赖。 public class Fish { private Water water; public Fish(Water _water) { water = _water; } public void Live() { Console.WriteLine("我的生活靠:"); water.GiveNutrition(); } } //2. setter 方式注入 //优点:Fish 对象在运行过程中可以灵活地更改依赖。 //缺点:Fish 对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态 public class Fish2 { private Water water; public Fish2() { } public void setWater(Water _water) { water = _water; } public void Live() { if (water != null) { Console.WriteLine("我的生活靠:"); water.GiveNutrition(); } } } //3. 接口注入 //这种方式和 Setter 方式很相似,接口的存在,表明了一种依赖配置的能力。比如,鱼有 活鱼和死鱼,接口注入方式,我们就可以控制,只给活鱼注入配置。 public class Fish3 : ISetWater { private Water water; public Fish3() { } public void Live() { if (water != null) { Console.WriteLine("我的生活靠:"); water.GiveNutrition(); } } public void SetWater(Water _water) { water = _water; } } public interface ISetWater { void SetWater(Water water); } class Program { static void Main(string[] args) { Water w = new RiverWater(); Fish f = new Fish(w); f.Live(); Water w2 = new RiverWater(); Fish2 f2 = new Fish2(); f2.setWater(w2); f2.Live(); Water w3 = new RiverWater(); Fish3 f3 = new Fish3(); f3.SetWater(w3); f3.Live(); //依赖注入(DI),它是实现IOC的实现方式,动态地将某种依赖关系注入到对象之中。 //回顾上面的例子,Fish 不在实例化创建Water,它就需要在外部(IOC 容器)赋值给它,这个赋值的动作有个专门的术语叫做注入(injection)。类似于,鱼生活在鱼缸里了,外接一个水龙头,水龙头另一头连接着水塔,这个水塔就好比是IOC容器,鱼不用关心生活在什么水里了,需要水时,打开水龙头(注入)就好了,至于水塔里装的是河水、井水还是海水,都不用考虑。 //依赖倒置是面向对象开发领域中的软件设计原则,它倡导上层模块不依赖于底层模块,抽象不依赖细节。 //依赖反转是遵守依赖倒置这个原则而提出来的一种设计模式,它引入了 IoC 容器的概念。 //依赖注入是为了实现依赖反转的一种手段之一。 //它们的本质是为了代码更加的“高内聚,低耦合”。 } } }