总目录
前言
1 基本介绍
1. 定义
依赖倒置原则 Dependence Inversion Principle,简称:DIP。
依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
2.分析
在理解依赖倒置原则之前,我们先要理解以下几个概念:
-
依赖:表示类之间的关系总共有6种,依赖是其中的一种,当A使用了一个B的时候,就是依赖关系。依赖在代码中具体体现为在A类中申明了一个B类作为成员变量或在A类某个方法中使用B类作为参数。
-
高层模块和底层模块:低层模块 就是被使用的那一方,高层模块就是使用者,这是一个相对的概念。比如 A类使用B类,那么B类就是底层模块,A类就是高层模块
-
抽象和细节:在C#中抽象一般就是只抽象类和接口,细节就是对应的实现类
有了以上的介绍,我们可以说:
依赖倒置原则就是程序逻辑在传递参数或关联关系时,尽量引用高层次的抽象,不使用具体的类,即使用接口或抽象类来引用参数,声明变量以及处理方法返回值等。
依赖原则说简单点,就是面向接口编程(抽象类和接口 这里统称接口)。接口相对来说是稳定的,而实现类是具体的业务实现,业务的变化是不稳定的。
注:关于类之间的关系说明,详见UML类图 详解。
3. 目的
依赖倒置原则可以使我们的架构更加的稳定、灵活,也能更好地应对需求的变化。因为相对于细节的多变性,抽象的东西是稳定的。所以以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定的多。
使用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性。
2 案例分析
1. 案例1
这里我们以父亲给3岁孩子读 儿童故事书 为例
//儿童故事书类
public class Storybook
{
//获取故事书内容
public string GetContent()
{
string content = "孙悟空大闹天宫";
return content;
}
}
//父亲类
public class Father
{
// 爸爸读书 的方法
public void Read()
{
Console.WriteLine("爸爸开始给孩子讲故事啦!");
Storybook storybook = new Storybook();
Console.WriteLine($"爸爸正在读:{storybook.GetContent()}");
}
}
客户端调用:
static void Main(string[] args)
{
Father father = new Father();
father.Read();
Console.ReadKey();
}
很好实现了需求,我们来看看依赖关系,Father类中引用了Storybook类,因此
Father是直接依赖于Storybook类。
这时需求变更了,妈妈说让爸爸给孩子讲讲编程启蒙书,于是吭哧吭哧修改代码如下:
//定义一个编程启蒙书类
public class CodingBook
{
public string GetContent()
{
string content = "二进制,ascii...";
return content;
}
}
public class Father
{
//由于需求变了,因此这里的代码需要做修改
public void Read()
{
//Console.WriteLine("爸爸开始给孩子讲故事啦!");
//Storybook storybook = new Storybook();
//Console.WriteLine($"爸爸正在读:{storybook.GetContent()}");
Console.WriteLine("爸爸开始给孩子讲编程启蒙书啦!");
CodingBook codingBook = new CodingBook();
Console.WriteLine($"爸爸正在读:{codingBook.GetContent()}");
}
}
按照这种初级的编程方式我们发现,只要需求稍微发生变化,代码就需要做改动,导致程序极其的不稳定。如果后面需求又变了,需要给孩子读英语启蒙,数学启蒙书等;再者说不定爸爸要加班,需要妈妈来给孩子读书那怎么办呢?其实在这个案例中,具体读什么读物是会发生变化,这就是细节,具体谁来给孩子读也会发生变化,这些细节都是会发生变化的,不稳定的。但是给孩子读书这个抽象的行为是稳定的。如果这时候还是使用传统的OOP思想来解决问题,那么会导致程序不断的在修改,因此我们需要用到依赖倒置原则。
现在我们再来看看使用依赖倒置原则后的代码吧。
首先定义出抽象的部分
//既然是面向接口编程,我们先将可以抽象的对象和行为全部定义出来
//定义 读物 的抽象类
public abstract class AbstractBook
{
//定义实现类必须实现的抽象方法
//对于读物研而言就是输出读物内容
public abstract string GetContent();
}
//定义 负责陪孩子读书的人 的抽象类
public abstract class AbstractPerson
{
//定义一个AbstractBook变量,这就是对依赖于抽象
//无论AbstractBook的实现类如何变化,这里的代码是可以稳定运行的
public AbstractBook abstractBook;
public AbstractPerson(AbstractBook book)
{
this.abstractBook = book;
}
public abstract void Read();
}
然后定义出实现的部分
public class Storybook : AbstractBook
{
public override string GetContent()
{
string content = "孙悟空大闹天宫";
return content;
}
}
public class CodingBook:AbstractBook
{
public override string GetContent()
{
string content = "二进制,ascii...";
return content;
}
}
public class Father : AbstractPerson
{
public Father(AbstractBook book) : base(book)
{
}
public override void Read()
{
Console.WriteLine($"{this.GetType().Name}开始给孩子讲{abstractBook.GetType().Name}啦!");
Console.WriteLine($"正在读:{abstractBook.GetContent()}");
}
}
客户端调用:
static void Main(string[] args)
{
//爸爸给孩子讲故事书
AbstractBook book=new Storybook();
AbstractPerson father = new Father(book);
father.Read();
Console.ReadKey();
}
我们现在来看看,使用依赖倒置原则后的代码,应对需求变化的能力如何?
需求变化1:需要爸爸给孩子读数学启蒙书了,我们只需新增如下代码:
public class Mathsbook : AbstractBook
{
public override string GetContent()
{
string content = "认识数字,加减法...";
return content;
}
}
客户端调用:
static void Main(string[] args)
{
//爸爸给孩子讲数学启蒙书
AbstractBook book=new Mathsbook();
AbstractPerson father = new Father(book);
father.Read();
Console.ReadKey();
}
相较于之前的代码,只是将AbstractBook book=new Storybook();
替换为了AbstractBook book=new Mathsbook();
不仅改动的代码少,而且有了对应变化的能力。
需求变化2:爸爸不在家,今天需要妈妈来给孩子读故事书了,我们只需新增一个Mother类
public class Mother : AbstractPerson
{
public Mother(AbstractBook book) : base(book)
{
}
public override void Read()
{
Console.WriteLine($"{this.GetType().Name}开始给孩子讲{abstractBook.GetType().Name}啦!");
Console.WriteLine($"正在读:{abstractBook.GetContent()}");
}
}
客户端调用:
static void Main(string[] args)
{
AbstractBook book=new Storybook();
AbstractPerson father = new Mother(book);
father.Read();
Console.ReadKey();
}
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
在上面的案例中:
在传统代码中 Father 中使用到了Storybook,Father相对于Storybook是高层模块,Father依赖于Storybook,就是高层依赖低层;Father和Storybook都是细节,且它们之间的依赖是细节间的依赖,这就违背了依赖倒置原则;
因此我们定义了AbstractPerson和AbstractBook两个抽象,AbstractPerson 对于 读物的依赖,不再依赖于具体的实现类如Storybook,CodingBook等,这些属于细节,而是依赖它们抽象类AbstractBook,这就符合 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节。
我们再来看看Father类
public class Father : AbstractPerson
{
public Father(AbstractBook book) : base(book)
{
}
public override void Read()
{
Console.WriteLine($"{this.GetType().Name}开始给孩子讲{abstractBook.GetType().Name}啦!");
Console.WriteLine($"正在读:{abstractBook.GetContent()}");
}
}
Father 类是一个细节,但是在Read 方法中使用了abstractBook.GetContent()
抽象类的GetContent方法。这就是细节应该依赖抽象。
2. 案例2
在传统的三层架构中,层与层之间是相互依赖的,UI层依赖于BLL层,BLL层依赖于DAL层。分层的目的是为了实现“高内聚、低耦合”。传统的三层架构只有高内聚没有低耦合,层与层之间是一种强依赖的关系,这也是传统三层架构的一种缺点。如果低层发生变化,可能上面所有的层都需要去修改,而且这种传统的三层架构也很难实现团队的协同开发,因为上层功能取决于下层功能的实现,下面功能如果没有开发完成,则上层功能也无法进行。
当我们使用依赖倒置原则后,上面的依赖关系就变成了下图这种依赖关系。
我们知道,在传统的三层架构里面,UI层直接依赖于BLL层,BLL层直接依赖于DAL层;由于每一层都是依赖下一层的实现,所以说当下层发生变化的时候,它的上一层也要发生变化。
当我们使用依赖倒置原则后UI、BLL、DAL三层之间没有直接的依赖关系,而是依赖于接口。具体实现就是应先确定出接口,DAL层抽象出IDAL接口,BLL层抽象出IBLL接口,这样UI层依赖于IBLL接口,BLL实现IBLL接口,BLL层依赖于IDAL接口,DAL实现IDAL接口。
结语
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。