a). 高层模块不应该依赖低层模块,它们都应该依赖抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
b). 要针对接口类/抽象类编程,不要针对实现编程。
高层模块包含了一个应该程序中的重要的策略选择和业务模型,
正是这些高层模块才使得其所有的应用程序区别于其他,
如果高层依赖于低层,那么对低层模块的改动就会直接影响到高层模块,从而迫使它们依次做出改动。
a) 简单来说,依赖倒转原则就是指:
代码要依赖于抽象的类,而不要依赖于具体的类;
要针对接口或抽象类编程,而不是针对具体类编程。
b) 实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。
c) 依赖倒转原则的常用实现方式之一是:
在代码中使用抽象类,而将具体类放在配置文件中。
d) 类之间的耦合:
> 零耦合关系 : 无关系
> 具体耦合关系 : 强关系
> 抽象耦合关系。 : 弱关系
依赖倒转原则要求客户端与实现类之间是 通过抽象类进行耦合。
以抽象耦合是依赖倒转原则的关键。
e) 依赖注入:
构造注入:通过构造函数注入实例变量
设值注入:通过Setter方法注入实例变量
接口注入:通过接口方法注入实例变量
某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如,
可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),
转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)
依赖倒转原则-图-1
依赖倒转原则-图-2
图(一)和图(二)分析:
因为该系统可能需要增加新的数据源或者新的文件格式,
(图一)中,每增加一个新的类型的数据源或者新的类型的文件格式,
客户类MainClass都需要修改源代码,以便使用新的类,这违背了开闭原则。
(图二)使用依赖倒转原则对其进行重构:
a) 客户类MainClass 依赖于两个抽象类变量: AbstractSource, AbstractTransformer;
其中,抽象类AbstractSource的子类 DatatbaseSource , TextSource是不同输出入源的实现;
抽象类AbstractTransformer的子类XMLTransformer, XLSTransformer是不同输出格式的实现。
而客户类通过配置文件config.xml来确定到底如何处理数据;
反面例子:
缺点:耦合太紧密,Light发生变化将影响ToggleSwitch。
解决办法一:
将Light作成Abstract,然后具体类继承自Light。
优点:ToggleSwitch依赖于抽象类Light,具有更高的稳定性,
而BulbLight与TubeLight继承自Light,可以根据"开放-封闭"原则进行扩展。
只要Light不发生变化,BulbLight与TubeLight的变化就不会波及ToggleSwitch。
缺点:如果用ToggleSwitch控制一台电视就很困难了。
总不能让TV继承自Light吧。
解决方法二:
优点:更为通用、更为稳定。
上面的计算器例子用这个原则更进一步的优化的代码如下:
#include
#include
#include
#include
using namespace std;
/* Step1:
* 使用vitual定义函数的功能模块,相当于实现公共界面设计
*/
class IGetResult {
public:
//virtual double get_result(double numberA, double numberB) = 0;
virtual double get_result() = 0;
public:
/* 注意一定要不要忘记添加虚析构函数 */
virtual ~IGetResult(){}
};
/* Step2 :
* 继承基类,定义具体的功能模块,并声明具体需要的私有成员变量
* 实现具体的功能模块
*/
class Add : public IGetResult {
public:
Add(double a, double b){
std::cout << "Add" << endl;
m_numberA = a;
m_numberB = b;
};
~Add(void) {};
public:
double get_result();
private:
double m_numberA;
double m_numberB;
};
/* 实现具体的功能模块 */
double Add::get_result() {
std::cout << "Add::get_result()" << endl;
return m_numberA + m_numberB;
}
class Del : public IGetResult {
public:
Del(double a, double b){
std::cout << "Del" << endl;
m_numberA = a;
m_numberB = b;
};
virtual ~Del(void) {};
public:
double get_result();
private:
double m_numberA;
double m_numberB;
};
double Del::get_result(){
std::cout << "Del::get_result()" << endl;
return m_numberA - m_numberB;
}
class Mul: public IGetResult {
public:
Mul(double a, double b){
std::cout << "Mul" << endl;
m_numberA = a;
m_numberB = b;
};
virtual ~Mul(void) {};
public:
double get_result();
private:
double m_numberA;
double m_numberB;
};
double Mul::get_result(){
std::cout << "Mul::get_result()" << endl;
return m_numberA * m_numberB;
}
/* Step4:
* 将功能函数接口注入封装类中
*/
class MainOperate {
public:
MainOperate(IGetResult *get_result_ptr){
this->m_get_result = get_result_ptr;
}
public:
double Operate();
private:
IGetResult *m_get_result;
};
/* 实现封装类中的接口函数的具体调用方式 */
double MainOperate::Operate(){
double ret = 0;
ret = this->m_get_result->get_result();
return ret;
}
int main(int argc, char* argv[]){
std::cout << "main()" << endl;
int a = 22;
int b = 10;
int ret = 0;
/* Step5: 主函数中定义调用模块接口函数*/
IGetResult *get_result = new Add(a, b);
/* Step6: 主函数调用封装类中接口函数*/
MainOperate *main_operate = new MainOperate(get_result);
ret = main_operate->Operate();
std::cout << "ret = " << ret << endl;
delete get_result;
get_result = new Del(a, b);
main_operate = new MainOperate(get_result);
ret = main_operate->Operate();
std::cout << "ret = " << ret << endl;
delete get_result;
get_result = new Mul(a, b);
main_operate = new MainOperate(get_result);
ret = main_operate->Operate();
std::cout << "ret = " << ret << endl;
delete get_result;
/*
Add add = Add(a, b);
ret = add.get_result();
std::cout << "ret = " << ret << endl;
Del del = Del(a, b);
ret = del.get_result();
std::cout << "ret = " << ret << endl;
*/
return 0;
}
特点:
代码要依赖于抽象的类,而不要依赖于具体的类;
针对接口或抽象类编程,而不是针对具体类编程。
实现了接口注入;
高层模块不应该依赖底层模块,两个都应该依赖与抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。
客户端与实现类通过抽象类进行耦合。
DIP优点:
使用传统过程化程序设计所创建的依赖关系,策略依赖于细节,这是糟糕的,因为策略受到细节改变的影响。
依赖倒置原则使细节和策略都依赖于抽象,抽象的稳定性决定了系统的稳定性。
DIP注意点:
1)、任何变量都不应该持有一个指向具体类的指针或者引用
2)、任何类都不应该从具体类派生(始于抽象,来自具体)
3)、任何方法都不应该覆写它的任何基类中的已经实现了的方法