01 依赖倒置原则(c++)
Posted 欢乐的企鹅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了01 依赖倒置原则(c++)相关的知识,希望对你有一定的参考价值。
依赖倒置原则
面向对象设计原则
上一篇中提到,变化是面向对象设计中最大的挑战。使用了抽象原则的面向对象设计最大的优势是抵御这种变化。抵御并不是说有变化时不做任何应对性修改,而是将这种变化带来的影响降到最低。
如何从抽象层面认识面向对象?
- 理解隔离变化
面向对象的构建方式更能够使用软件的变化,将变化带来的影响降到最小 - 各司其职
面向对象的方式强调各个类的责任,由于需求变化导致的新增类型不应该影响原有类型的实现,每个类各负其责。实际上,各负其责实现的背后是面向对象的多态机制,即通过统一的接口,能够调用不同的实现方式。 - 对象是什么?
对象封装了接口和数据,提供了一系列可以使用的公共接口,是拥有某种责任的抽象。
接下来开始学习八大设计原则,这些设计原则比具体的设计模式更重要,为什么呢?
理解了这八大设计原则,你有可能发明属于自己的设计模式,也能够更容易地看懂其他领域的设计模式。所有的设计模式都是依赖这些原则推导出来的。简单的说,后面要学习的设计模式是这些设计原则在某种特定场景下的具体表现。在学习具体的设计模式时,可以拿这些设计原则衡量其品质,检查该模式有没有违背设计原则。
依赖倒置原则(DIP, Dependency Inversion Principle)
High level modules should not depend upon low level modules. Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions.
高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定);
抽象(稳定)不应该依赖于实现细节(变化),实现细节(变化)应该依赖于抽象(稳定)。
如何理解这三句话呢?不妨举一个简单的例子(以《设计模式之禅》中的司机驾驶汽车场景为例):
现在呢,有一个司机驾驶奔驰汽车的场景。
奔驰汽车提供了run方法,代表车辆的运行; 司机通过调用奔驰车的run方法开动奔驰车。client1函数是具体的业务逻辑代码,描述了司机驾驶奔驰车的场景。
通过以上的代码,确实完成了司机驾驶奔驰车的业务场景。但是现在业务需求变更了,司机不仅要开奔驰车,还要开宝马车,按照上面的实现方式,该如何修改呢?
[1] 需要新增宝马车类型
[2] 需要让司机支持开宝马车的接口
此时细心的你已经发现了,为了让司机能够驾驶宝马车,不仅需要增加宝马车,还要让司机增加新的接口,并且也需要修改业务逻辑代码。这里仅仅增加了一个车类型,就需要修改司机类,导致司机类是不稳定的。后果就是系统的可维护性和可读性大大降低。这里汽车是不稳定的,会有不同的汽车类型不断地出现,而司机又依赖于不稳定的汽车,因此汽车也是不稳定的,需要频繁地增加针对不同汽车类型的接口,才能满足不断变化的业务需求。
那么如何让司机是相对稳定的,即不管汽车如何变化,司机都不需要调整呢?
根据依赖倒置原则,我们给不稳定的汽车和司机之间引入一个抽象车类型ICar,ICar是稳定的,提供run方法代表汽车的运行。不同类型的汽车不管怎么变化,最终就是用来驾驶的,因此可以把run方法提取出来,作为抽象层接口。而司机只需依赖稳定的抽象接口ICar,不同的车类型也只需依赖稳定的抽象接口ICar,这样ICar抽象接口就隔离了变化,使得司机类是相对稳定的。
这里的ICar是抽象接口,提供了抽象车类型必须要有的run功能,不需要实现,因此定义成纯虚函数。run接口必须是虚函数,因为司机类的drive方法内部会调用抽象层ICar的run接口,此时通过编程语言的多态机制,car.run()
会自动调用派生自ICar接口的具体实现类的run方法。
回过头来再分析,高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定);这里的高层模块便是司机类,低层模块是变化的具体车类型,我们引入了稳定的抽象接口ICar,让高层模块司机类和低层模块具体车类型都依赖于抽象接口;司机类操作抽象接口ICar,屏蔽了具体类型的车的实现细节,因此司机类是相对稳定的。抽象(稳定)不应该依赖于实现细节(变化),实现细节(变化)应该依赖于抽象(稳定);ICar不依赖具体的车类型实现,而具体类型的车依赖于抽象接口ICar。
司机需要开宝马车,只需要增加低层模块宝马车和调整业务场景即可,不需要修改高层模块司机类,把业务需求变化带来的影响降到了最低。
依赖倒置原则本质就是通过抽象的接口和类,使得各个类或模块的实现彼此独立,实现了模块之间的松耦合。
依赖导致原则有哪些优点呢?
[1] 通过抽象接口使各模块的实现彼此独立,不会互相影响,实现了模块间的松耦合(DIP原则的本质)
[2] 规避一些非技术因素引起的问题(项目越大,需求变化的概率也越大,通过DIP原则设计的接口对实现类进行约束,可以减少需求变化引起的工作量剧增问题。同时,人员发生变动时,只要文档完善,也可让维护人员轻松地扩展和维护)
[3] 促进并行开发(两个类之间有依赖关系,只需制定出两者之间的接口就能独立开发,规范定好了,而且项目间的单元测试也能独立进行)
如何理解依赖倒置原则中的"倒置"呢?
下面是维基百科对依赖倒置原则的定义(https://zh.wikipedia.org/zh-hans/依赖反转原则):
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系建立在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。该原则规定:
[1] 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
[2] 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
一个复杂的系统必然存在高层与低层,并且,高层使用低层提供的“服务”来实现自己的业务逻辑,即所谓的高层依赖低层。但如果高层直接使用这些低层对象,当业务变化导致低层的已有功能无法满足高层的服务需求时,就需要“伤筋动骨”。为了避免这样的问题,人们提出了面向接口编程,即只要接口不发生变化,低层的实现不会影响高层的使用。按照常理,通常将相同语义的元素放在一起,因此接口与其实现(类)应该处于同一层模块之中(如下面左侧图所示)。这看似好像没问题,但是软件的高层应用可能会发生变化,即来自客户的需求会发生变化。当高层应用发生变化,那它依赖的低层提供的服务也很可能需要发生变化。那么问题来了,谁来约定这个接口提供什么样的服务呢?按照前面的逻辑,接口和实现放在低层,那接口约束应该由低层提供,可是低层开发人员并不负责高层的应用逻辑。低层应该应该只关心自己那点事儿,即负责响应高层的需求,按照需求提供实现服务。但现在接口放在低层维护,就应该由低层的开发人员负责体现需求接口的“变更”(提供新的服务)。此时会发现,负责实现高层应用的开发人员拥有需求,但无法约束定义描述需求的接口;负责低层的开发人员则不管需求,只应该提供具体实现,却在维护跟应用需求有关的接口。这不就出现了矛盾吗?为了解决这种矛盾,人们提出将本应放在低层的接口放在高层,低层的实现依赖高层提供的接口,去实现相应的服务(下面右侧图所示),这才是DIP中“倒置”的真正含义(参考https://www.jianshu.com/p/8d7723cd4e24, 作者:Aftremath_为了冰激凌)。
那么实际工作或学习中,如何有效地使用依赖倒置原则呢?
[1] 首先分析出有哪些高层模块和低层模块,注意高层模块是依赖于低层模块的;
[2] 接着分析低层模块的稳定性,如果低层模块一直是稳定的,未来也不会发生任何变化,那没有必要采用DIP原则;如果低层模块是变化的,未来可能会被新的低层模块替代,此时就需要引入抽象接口,让高层模块和低层模块都依赖于抽象层;
[3] 接口负责定义public属性和方法,并且声明和高层模块和低层模块之间的依赖关系;抽象类负责实现所有低层模块都必须具备的公共部分;实现类负责实现业务逻辑,对抽象接口进行扩展与细化。
[4] 子类尽量不要覆写抽象类中已经实现的方法,因为高层模块依赖于抽象类中已经实现的这个方法,而子类如果覆写了,可能对高层模块的稳定性产生一定的影响。
总结时刻
依赖倒置原则是非常重要的设计原则,几乎贯穿于所有的设计模式。本篇文章通过讨论司机驾驶汽车的场景,分析了使用和不使用DIP原则之间的区别,找出了问题所在(即如何让高层模块是相对稳定的,从而改善系统的可维护性和可读性);接着根据DIP原则的三句话定义,实现了相对稳定的,可维护性和可读性较好的版本,最后在对比中得出了依赖导致原则的本质和优点。
如果这篇文章内容你只能记住一件事,那请记住:面向接口编程,让高层和低层模块都依赖于抽象接口,使得抽象接口把低层模块的变化隔离起来,从而实现高层模块的相对稳定。
参考资料
[1] 《设计模式:可复用面向对象软件的基础》
[2] 《设计模式之禅》
[3] 《依赖反转(依赖倒置)原则之“反转(倒置)”》(https://www.jianshu.com/p/8d7723cd4e24)
以上是关于01 依赖倒置原则(c++)的主要内容,如果未能解决你的问题,请参考以下文章