C#编程之依赖倒置原则DIP
一、前言
我们先来看看传统的三层架构,如下图所示:
从上图中我们可以看到:在传统的三层架构中,层与层之间是相互依赖的,UI层依赖于BLL层,BLL层依赖于DAL层。分层的目的是为了实现“高内聚、低耦合”。传统的三层架构只有高内聚没有低耦合,层与层之间是一种强依赖的关系,这也是传统三层架构的一种缺点。这种自上而下的依赖关系会导致级联修改,如果低层发生变化,可能上面所有的层都需要去修改,而且这种传统的三层架构也很难实现团队的协同开发,因为上层功能取决于下层功能的实现,下面功能如果没有开发完成,则上层功能也无法进行。
传统的三层架构没有遵循依赖倒置原则(DIP)来设计,所以就会出现上面的问题。
二、依赖倒置
依赖倒置(DIP):Dependence Inversion Principle的缩写,主要有两层含义:
- 高层次的模块不应该依赖低层次的模块,两者都应该依赖其抽象。
- 抽象不应该依赖于具体,具体应该依赖于抽象。
我们先来解释第一句话:高层模块不应该直接依赖低层模块的具体实现,而是应该依赖于低层模块的抽象,也就是说,模块之间的依赖是通过抽象发生的,实现类之间不应该发生直接的依赖关系,他们的依赖关系应该通过接口或者抽象类产生。
在来解释第二句话:接口或者抽象类不应该依赖于实现类。举个例子,假如我们要写BLL层的代码,直接就去实现了功能,等到开发完成以后发现没有使用依赖倒置原则,这时候在根据实现类去写接口,这种是不对的,应该首先设计抽象,然后在根据抽象去实现,应该要面向接口编程。
我们在上面说过,在传统的三层架构里面没有使用依赖倒置原则,那么把依赖倒置原则应用到传统的三层架构里面会如何呢?我们知道,在传统的三层架构里面,UI层直接依赖于BLL层,BLL层直接依赖于DAL层,由于每一层都是依赖下一层的实现,所以说当下层发生变化的时候,它的上一层也要发生变化,这时候可以根据依赖倒置原则来重新设计三层架构。
UI、BLL、DAL三层之间应该没有直接的依赖关系,都应该依赖于接口。首先应该先确定出接口,DAL层抽象出IDAL接口,BLL层抽象出IBLL接口,这样UI层依赖于IBLL接口,BLL实现IBLL接口。BLL层依赖于IDAL接口,DAL实现IDAL接口。如下图所示:
我们上面讲了依赖倒置原则,那么依赖倒置原则的目的是什么呢?
有了依赖倒置原则,可以使我们的架构更加的稳定、灵活,也能更好地应对需求的变化。相对于细节的多变性,抽象的东西是稳定的。所以以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定的多。
在传统的三层架构里面,仅仅增加一个接口层,我们就实现了依赖倒置,目的就是降低层与层之间的耦合。有了这样的接口层,三层架构才真正实现了“高内聚、低耦合”的思想。
依赖倒置原则是架构层面上的,那么如何在代码层面上实现呢?下面看控制反转。
三、控制反转
控制反转(IOC):Inversion of Control的缩写,一种反转流、依赖和接口的方式,它把传统上由程序代码直接操控的对象的控制器(创建、维护)交给第三方,通过第三方(IOC容器)来实现对象组件的装配和管理。
IOC容器,也可以叫依赖注入框架,是由一种依赖注入框架提供的,主要用来映射依赖,管理对象的创建和生存周期。IOC容器本质上就是一个对象,通常会把程序里面所有的类都注册进去,使用这个类的时候,直接从容器里面去解析。
四、依赖注入
依赖注入(DI):Dependency Injection的缩写。依赖注入是控制反转的一种实现方式,依赖注入的目的就是为了实现控制反转。
依赖注入是一种工具或手段,目的是帮助我们开发出松耦合、可维护的程序。
依赖注入常用的方式有以下几种:
- 构造函数注入。
- 属性注入。
- 方法注入。
其中构造函数注入是使用最多的,其次是属性注入。
看下面的一个例子:父亲给孩子讲故事,只要给这个父亲一本书,他就可以照着这本书给孩子讲故事。我们下面先用最传统的方式实现一下,这里不使用任何的设计原则和设计模式。
首先定义一个Book类:
namespace DipDemo1 { public class Book { public string GetContent() { return "从前有座山,山上有座庙....."; } } }
然后在定义一个Father类:
using System; namespace DipDemo1 { public class Father { public void Read() { Book book = new Book(); Console.WriteLine("爸爸开始给孩子讲故事了"); Console.WriteLine(book.GetContent()); } } }
然后在Main方法里面调用:
using System; namespace DipDemo1 { class Program { static void Main(string[] args) { Father father = new Father(); father.Read(); Console.ReadKey(); } } }
我们来看看关系图:
我们看到:Father是直接依赖于Book类。
这时需求发生了变化,不给爸爸书了,给爸爸报纸,让爸爸照着报纸给孩子读报纸,这时该怎么做呢?按照传统的方式,我们这时候需要在定义一个报纸类:
namespace DipDemo1 { public class NewsPaper { public string GetContent() { return "新闻"; } } }
这时依赖关系变了,因为爸爸要依赖于报纸了,这就导致还要修改Father类:
using System; namespace DipDemo1 { public class Father { public void Read() { // 读书 // Book book = new Book(); //Console.WriteLine("爸爸开始给孩子讲故事了"); //Console.WriteLine(book.GetContent()); // 报纸 NewsPaper paper = new NewsPaper(); Console.WriteLine("爸爸开始给孩子讲新闻"); Console.WriteLine(paper.GetContent()); } } }
假设后面需求又变了,又不给报纸了,换成杂志、平板电脑等。需求在不断的变化,不管怎么变化,对于爸爸来说,他一直在读读物,但是具体读什么读物是会发生变化,这就是细节,也就是说细节会发生变化。但是抽象是不会变的。如果这时候还是使用传统的OOP思想来解决问题,那么会导致程序不断的在修改。下面使用工厂模式来优化:
首先创建一个接口:
namespace DipDemo2 { public interface IReader { string GetContent(); } }
然后让Book类和NewsPaper类都继承自IReader接口,Book类
namespace DipDemo2 { public class Book : IReader { public string GetContent() { return "从前有座山,山上有座庙....."; } } }
NewsPaper类:
namespace DipDemo2 { public class NewsPaper : IReader { public string GetContent() { return "王聪聪被限制高消费......"; } } }
然后创建一个工厂类:
namespace DipDemo2 { public static class ReaderFactory { public static IReader GetReader(string readerType) { if (string.IsNullOrEmpty(readerType)) { return null; } switch (readerType) { case "NewsPaper": return new NewsPaper(); case "Book": return new Book(); default: return null; } } } }
里面方法的返回值是一个接口类型。最后在Father类里面调用工厂类:
using System; namespace DipDemo2 { public class Father { private IReader Reader { get; set; } public Father(string readerName) { // 这里依赖于抽象 Reader = ReaderFactory.GetReader(readerName); } public void Read() { Console.WriteLine("爸爸开始给孩子讲故事了"); Console.WriteLine(Reader.GetContent()); } } }
最后在Main方法里面调用:
using System; namespace DipDemo2 { class Program { static void Main(string[] args) { Father father = new Father("Book"); father.Read(); Console.ReadKey(); } } }
我们这时候可以在看看依赖关系图:
这时Father已经和Book、Paper没有任何依赖了,Father依赖于IReader接口,还依赖于工厂类,而工厂类又依赖于Book和Paper类。这里实际上已经实现了控制反转。Father(高层)不依赖于低层(Book、Paper)而是依赖于抽象(IReader),而且具体的实现也不是由高层来创建,而是由第三方来创建(这里是工厂类)。但是这里只是使用工厂模式来模拟控制反转,而没有实现依赖的注入,依赖还是需要向工厂去请求。
下面继续优化代码,这里只需要修改Father类:
using System; namespace DipDemo3 { public class Father { public IReader Reader { get; set; } /// <summary> /// 构造函数的参数是IReader接口类型 /// </summary> /// <param name="reader"></param> public Father(IReader reader) { Reader = reader; } public void Read() { Console.WriteLine("爸爸开始给孩子讲故事了"); Console.WriteLine(Reader.GetContent()); } } }
在Main方法里面调用:
using System; namespace DipDemo3 { class Program { static void Main(string[] args) { var f = new Father(new Book()); f.Read(); Console.ReadKey(); } } }
如果以后换成了Paper,需要修改代码:
using System; namespace DipDemo3 { class Program { static void Main(string[] args) { // Book //var f = new Father(new Book()); //f.Read(); // Paprer var f = new Father(new Paper()); f.Read(); Console.ReadKey(); } } }
由于这里没有了工厂,我们还是需要在代码里面实例化具体的实现类。如果有一个IOC容器,我们就不需要自己new一个实例了,而是由容器帮我们创建实例,创建完成以后在把依赖对象注入进去。
我们在来看一下依赖关系图:
下面我们使用Unity容器来继续优化上面的代码,首先需要在项目里面安装Unity,直接在NuGet里面搜索即可:
这里只需要修改Main方法调用即可:
using System; using Unity; namespace UnityDemo { class Program { static void Main(string[] args) { // 创建容器 var container = new UnityContainer(); // 扫描程序集、配置文件 // 在容器里面注册接口和实现类,创建依赖关系 container.RegisterType<IReader, Book>(); // 在容器里面注册Father container.RegisterType<Father>(); // 从容器里拿出要使用的类,容器会自行创建father对 // 还会从容器里去拿到他所依赖的对象,并且注入进来 // var father = container.Resolve<Father>(); // 调用方法 father.Read(); Console.ReadKey(); } } }