高层模块不能依赖于一个“具体化、细节化”的低层模块,而是通过一个抽象的“规范/标准”建立两者之间的依赖关系,简言之就是:不依赖于实现,而是依赖于抽象。这里“实现”一词有的地方也称为“细节”,在编码中主要体现的是我们根据业务模型具体自定义的普通类,比如:员工类、商品类等。而其中的“抽象”一词是指定的接口或抽象类。
1.2.高层与低层
下面我们通过传统的三层架构作为背景来理解“依赖倒置原则”中的高层与低层的含义。
在分层架构中,高层是相对而言的,对于上面三层架构图中而言最高层是“表示层”,相对于“业务逻辑层”它的高层是“表示层UI”,相对于“数据访问层”它的高层则是“业务逻辑层”。
低层同样也是相对而言的,对于上面三层架构图中而言最低层是“数据访问层”,相对于“业务逻辑层”它的低层是“数据访问层”,相对于“表示层”它的低层则是“业务逻辑层”。
那么简单来说高层就相对于一个使用者,低层就相当于一个被使用者。
1.3.依赖倒置原则在分层架构中的体现
在早期比较传统的项目中,三层架构的分层通常都是上图的形式:表示层直接依赖于一个具体实现、非抽象的业务逻辑层,业务逻辑层对下层的依赖同样如此。这种分层实际上在依赖上就是违背了“依赖倒置原则”,因为它都是依赖的一些具体的实现,而非抽象。
那么对于传统的三层架构,使用了“依赖倒置原则”的思想,就可以发挥其中的优势,使其易于维护和扩展。比如说你负责的某个项目的DAL层是使用SqlServer数据库,而需求要变更为Mysql数据库,如果使用了“依赖倒置原则”建立了抽象的规范,那么你就可以在不影响其他层的情况下,单独实现抽象规范就可以进行变更,这样的变更对于“依赖于具体实现”改动是最小的。
1.4.依赖倒置原则体现的缓冲性例如你写了一个计算统计的程序,其中某个数值的计算比较复杂,你将算法封装成了一个具体实现类,作为比参数变量引入到计算统计的程序。由于第一次写的算法不是很理想,你后面会面临尝试更多新的算法,这就意味着每个新算法的使用,都要去“计算统计程序”中进行协调改动。
如果你在算法设计之初就通过接口或抽象类建立一个算法规范/标准,那么计算统计程序通过接口或抽象类作为参数变量引入算法对象就能起到一个缓冲性,也可以更好的支持算法的迭代,利于程序扩展和优化。
2.通过代码示例说明依赖倒置重要性
下面通过两种代码示例来理解依赖倒置原则以及它的重要性,第一种介绍的是“未使用”依赖倒置原则的代码,并描述在“未使用”依赖倒置原则会出现怎样的利害关系。第二种则是针对第一种出现的问题使用依赖倒置原则对其进行改良,从而体现出依赖倒置原则的优势。
2.1.示例一(依赖实现)1 //武器剑 2 class Sword 3 { 4 //攻击的方法 5 public void Attack() 6 { 7 Console.WriteLine("使用剑进行刺杀"); 8 } 9 } 10 11 //游戏角色类 12 class GameRole 13 { 14 //使用武器 15 public void UseWeapon(Sword sword) 16 { 17 sword.Attack(); 18 } 19 } 20 21 internal class Program 22 { 23 static void Main(string[] args) 24 { 25 GameRole gameRole = new GameRole(); 26 gameRole.UseWeapon(new Sword()); 27 } 28 }
上面代码中分别定义了两个类,一个是游戏角色类另一个是剑类,游戏角色类其中有一个“使用武器”的方法,该方法需要传入一个剑类的对象,并调用剑类中的攻击方法。
这个示例使用了我们日常玩电子游戏来作为背景,玩过游戏的朋友应该知道,游戏的更新迭代也是习以为常的事情。如果游戏中使用了以上的代码,并且游戏中的角色使用的武器经常会发生一些改变,那么代码中的游戏角色类要针对每一个新武器新增对应的使用方法。如果无法穷举使用的武器,那我们将会写N个使用新武器的方法。
方式一这种形式的代码,实际上就是在依赖具体的实现,体现在使用武器的方法上,方法总是必须要传入一个具体的武器类(剑、斧头等),而不是依赖一个抽象的标准/规范。我们可以通过示例很直观的看出来,这种依赖具体实现的代码并不适应需求变化。接下来我们则通过使用依赖倒置原则来消除这种“被病”。
2.2.示例二(依赖抽象)
1 //武器接口 2 public interface IWeapon 3 { 4 void Attack(); 5 } 6 7 //武器剑 8 class Sword : IWeapon 9 { 10 public void Attack() 11 { 12 Console.WriteLine("使用剑进行刺杀"); 13 } 14 } 15 16 //斧头 17 class Axe : IWeapon 18 { 19 public void Attack() 20 { 21 Console.WriteLine("使用斧头进行劈砍"); 22 } 23 } 24 25 //游戏角色类 26 class GameRole 27 { 28 //使用武器 29 public void UseWeapon(IWeapon weapon) 30 { 31 weapon.Attack(); 32 } 33 34 } 35 36 37 internal class Program 38 { 39 static void Main(string[] args) 40 { 41 GameRole gameRole = new GameRole(); 42 gameRole.UseWeapon(new Sword()); 43 gameRole.UseWeapon(new Axe()); 44 } 45 }
示例二基于依赖倒置原则对示例一进行改良,将多种武器抽象成了一个接口,并且根据武器种类现状创建实现了“武器接口”的实现类(剑、斧头),游戏角色类将多个使用不同武器的方法缩减成一个方法,该方法接收一个“武器接口”类型的实例对象(也就是实现了该接口的类)作为参数。
游戏角色类从原本依赖的具体武器转变成依赖一个抽象武器(依赖具体类转变为依赖接口),从而体现出了从依赖实现到依赖抽象的转变。那么此时不管游戏对角色使用的武器进行新增或删减,“游戏角色类”不用做大量的改动变化,而是将这个变化单独抽离了出去,作为了一个独立的接口。而这个接口不在单单只对游戏角色类服务,还可以用于其他的类(妖怪类、魔兽类等),从而降低了代码的变化性和耦合性,且提高代码的复用性。
2.3.示例总结
基于上面的两个示例的分析对比,我们就应该更加谨记“依赖倒置原则”在编码中的重要性,如果我们的代码只是使用面向具体实现编程,那么程序结构上并不能更好的适应变化,更好的扩展、更好的维护,大量的冗余代码会导致程序越来越臃肿。
3.总结
一开始学习“依赖倒置原则”的时候,在明白其中的含义和作用后,往往都在纠结这个“倒置”到底是什么意思,它倒置的是个啥,如果感觉不搞懂总感觉差点意思。经过反复的思考,我个人的理解是:将原本高层对低层依赖具体细节,颠倒反义,转为依赖于抽象。
依赖倒置原则在面向对象编程和框架的设计上都广泛运用,它其中主要的核心思想就是:面向接口编程,而不是面向具体实现编程。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
知识改变命运