SOLID设计原则--依赖倒置原则
- 背景
- SOLID设计原则
- 依赖倒置原则(DIP)
-
- 什么是依赖倒置原则
-
- 定义
- 解释说明
- 依赖倒置原则的使用
-
- 需求描述
- 解决方案一
-
- 优缺点分析
- 解决方案二
- 谁和谁的依赖被倒置了?
- 总结
背景
- 设计原则–>设计模式–>程序语言语法机制,是编程思考和实施的三个层次。由左向右抽象层次越来越低,工作内容越来越具体。语法机制提供了机制和实施的可能性,设计模式是如何操作这些机制,设计模式可以看做是设计原则的具现化,设计模式遵循了设计原则,提供针对重复问题的最佳解决方案。设计原则指导设计模式的产生。
- 设计原则是心法,设计模式是招式,程序语言语法机制是基本能力(能动、能跳、能推)。
- 违背程序语言语法机制的代码段是不能正确工作的,不违背程序语言语法机制是作为程序员的
;违背设计原则而写出的程序,会使程序的质量属性[可扩展,可维护,可测试,可重用,…]急剧下降。理解设计原则,并会使用设计模式的程序员是成为基本格
的必要条件。中高级工程师
- 设计原则影响的范围最广,影响效果最长久,也最潜移默化。不经历一个较长时间的软件生长和演化过程,设计原则的影响很难体现出来。
- 比设计原则更抽象的思维层次是设计思想,比如
设计思想、面向过程、面向对象、函数式编程思想。高内聚低耦合
SOLID设计原则
- 经过面向对象程序设计领域几十年的发展和总结,诞生了五个重要的设计原则(单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则),后来这几个原则被Bob大叔调整了下顺序,其首字母刚好是单词
,因此后来就被被称为solid
原则,SOLID
原则起初主要是在OOP中针对接口设计的指导原则,其实这些原则还可以作为函数、类以及模块的设计原则。遵循这几个原则可以编写出扩展性好,可维护性好,透明性好,可读性好的软件系统。SOLID
- 总的来说
设计原则还是程序设计方面的具体原则。在软件工程中针对的SOLID
或level
大概是处于架构设计和编码实现的中间阶段 ------- 程序详细设计阶段。phase
依赖倒置原则(DIP)
什么是依赖倒置原则
定义
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
- 抽象不能依赖于实现细节,实现细节应该依赖于抽象。
解释说明
- 依赖倒置原则(DIP)虽然是位于
最后一个,但其实,DIP是非常基础和重要的设计原则。其余四个原则几乎都或多或少有DIP的影子,比如违反了DIP,也大概率会违反开闭原则(OCP)。SOLID
依赖倒置原则的使用
我们通过对一个具体需求的分析、设计和实现来说明如何遵循依赖倒置原则。违背和不违背DIP原则分别对扩展性有什么影响。比如有一个业务需求:
需求描述
我期待通过一个二值自锁开关类来控制一个两状态的台灯。按下开关,台灯打开;抬起开关,台灯关闭。
解决方案一
- 这里我们通过最直接的做法来满足需求,作为做原始的迭代版本,保留程序的演化和成长过程。
- 通过一个二值自锁开关类来控制一个两状态的台灯,我们对这个需求经过简单的面向对象分析后,把关键名词挑出来作为类名,把动词作为方法命。于是有了
和class Switch
class Lamp
两个类。
我们先定义客户端的使用程序:
#include "lamp.hpp"
#include "switch.hpp"
// wrapper 类,组装两个功能类对象
class Client {
public:
Client() {
// 创建一个位于卧室的台灯对象
lamp_ = Lamp("bed room");
// 将创建到好的台灯对象交给Swich对象来管理,继而实现控制
switch_ = Switch(lamp_); // 注意这里用的是引用或指针而不是值拷贝
}
// 开灯
bool openLamp() { return switch_.open(); }
// 关灯
bool closeLamp() { return switch_.close(); }
private:
Lamp lamp_;
Switch switch_;
};
// Application main entry point
void main() {
Client client;
client.openLamp();
client.closeLamp();
}
- 下面给出
和class Switch
两个类实现。class Lamp
- 类
Lamp
// file lamp.hpp
class Lamp {
public:
Lamp(const std::string &location) : location_(location) {}
public:
bool open() {
bool flag = false;
// 具体实现细节
// ...
return flag;
}
bool close() {
bool flag = false;
// 具体实现细节
// ...
return flag;
}
private:
std::string location_;
};
- 类
Switch
// file switch.
#include <lamp.hpp>
class Switch {
public:
// 构造域
Switch(Lamp *lamp) : lamp_(lamp) {}
Switch(Lamp &lamp) : lamp_(&lamp) {}
// 接口域
public:
bool open() { return lamp_->open(); }
bool close() { return lamp_->close(); }
private:
Lamp *lamp_;
};
优缺点分析
- 优点是简单直接,没有思维负担,也满足了功能。我们画下当前的类图来看下当前依赖关系。
- 这里我们重点关注类
和类Switch
。类Lamp
聚合(Aggregation)到类Lamp
。类Switch
是高层模块,类Swith
是低层模块。依赖关系为类Lamp
到Switch
,任何导致Lamp
变化的原因都会导致Lamp
的变动,这种依赖关系是静态的,编译时的依赖。大部分情况下高层模块Switch
的代码相对稳定,只具有Swith
和open
两种操作,而这两种操作都是转调用底层模块的具体实现close
。而低层模块{open(); close()}
相对不稳定,目前的依赖关系违背了依赖导致原则,高层模块Lamp
直接依赖的低层模块,导致高层Switch
不易变化的模块的代码和低层Switch
容易变化的代码静态绑定到一起,继而导致高层模块Lamp
的代码不可复用。为了解决Switch
可复用性问题,我们引入解决方案二。Switch
解决方案二
- 为了复用
代码,我们将引入一个抽象的中间类(引入一个中间层[分层]是计算机领域解决问题的好方法),名称为Switch
来抽象IControlable
可以操作的所有对象,包括Switch
对象,甚至未来的Lamp
、Motor
等所有具有二元[0-1]操作的可控制对象。Vehicle
// file icontrolable.hpp
// 抽象类 或 接口类
class IControlable {
public:
virtual ~IControlable() {}
public:
virtual bool open() = 0;
virtual bool close() = 0;
};
- 我们让
依赖接口类Switch
,让类IControlable
实现接口类Lamp
,解耦IControlable
和Switch
的直接依赖关系,解除Lamp
对象只能控制Switch
对象的尴尬局面(😃-😃-😃)。此时类图如下Lamp
- 现在的依赖关系变成了
依赖接口类Switch
;同时IControlable
也依赖接口类Lamp
。IControlable
不在依赖Switch
了,这就是DIP原则的两种定义所描述的完整内涵----高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖实现细节,实现细节应该依赖于抽象。这里Lamp
是高层模块,Switch
是低层模块也是实现细节,而Lamp
是抽象。至此,该设计完全遵循DIP原则了。IControlable
- 对于C++语言来说,此时在编译时,类
文件Switch
也只依赖switch.hpp
,不会有IControlable.hpp
之Lamp
的影子。lamp.hpp
谁和谁的依赖被倒置了?
- 一般来说控制器
的能力由Switch
来描述和限制,我们会把IControlable
也看做高层模块的一部分,即IControlable
属于高层模块。类图如下右边所示:IControlable
- 依赖关系由之前的 高层
模块直接依赖低层和实现细节模块Switch
, 进化为 低层Lamp
模块依赖了高层Lamp
模块,由之前的稳定依赖于不稳定进化为不稳定依赖于稳定。这就是依赖倒置中倒置的深刻含义。Switch
总结
- 今天分析了
原则 ----依赖倒置原则(SOLID
)的含义,并从复用性和可扩展性方面给出了一个例子,希望读者能认真思考,举一反三,将设计原则转化为自己的编程内功,修炼和提高自己的编程素养。DIP