本章为概述。后续章节将针对每一种原则给出对应的应用场景和代码示例。
知名软件大师Robert C.Martin认为一个可维护性较低的软件设计,通常是由于如下四个原因造成:
过于僵硬(Rigidity),过于脆弱(Fragility),可用率低(Immobility),黏度过高(Viscosity)。
只是知道virtual 不是面向对象,这只是语法。
理解面向对象的基本原则,才是面向对象的入门,才是设计模式的入门。
你能够根据你的业务,选择恰当的设计模式,你的设计模式开始成熟了。
注重软件的可维护性和可复用性
需要知道有哪些设计模式, 知道23种设计模式对应的场景是什么。这样遇到这种场景就可以去套用设计模式。
多人协作需要良好的类与类之间的关系。
理解了OOP的基本原则,对后续学习和认识设计模式有非常大的作用。
一、 面向对象设计原则概述
我们在设计时为什么要遵循面向对象的设计原则,为什么要合理使用设计模式?
- 软件的可维护性和可复用性
- 软件的复用或重用拥有众多优点,如可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性。
- 面向对象设计复用的目标在于实现支持可维护性的复用。
- 在面向对象的设计里,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则,遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。
- 面向对象设计原则和设计模式也是对系统进行合理重构的指南针,重构是在不改变软件现有功能的基础上,通过调整代码改善软件的质量,性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
二、 面向对象的七个基本原则
1. 单一职责原则
- 一个对象应该只包含
单一的职责
,并且该职责被完全的封装在一个类中。
就一个类而言,应该仅有一个引起它变化的原因。
类的职责要单一,不能将太多的职责放在一个类中。
2. 开闭原则
抽象化
是开闭原则的关键。
软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展功能。
3. 里氏代换原则
-
在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。
-
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此
在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象
。 -
父类指针指向子类对象,这样的好处是可以针对接口编程。
在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。
4. 依赖倒转原则
- 高层模块
不应该依赖底层模块
,它们都应该依赖抽象
。抽象不应该依赖于细节,细节应该依赖于抽象。 代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不要针对具体类编程
。(父类指针指向子类对象,针对抽象的父类编程,不要针对具体实现的子类编程。这样可以随时替换指向的子类。)- 实现开闭原则的关键是抽象化,并且从抽象化导出具体实现,如果说
开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段
。 - 为什么不依赖于集体实现,依赖于底层具体实现,当底层实现改变时高层模块也会跟着改变。耦合度高。
要针对抽象层编程,而不要针对具体类编程。
代码要依赖于抽象的类,不要依赖于具体的类。要针对接口编程,而不是针对具体类编程。
5. 接口隔离原则
- 客户端不应该依赖那些它不需要的接口。
- 一旦一个
接口太大
,则需要将它分割成一些更小的接口
,使用该接口的客户端仅需要知道与之相关的方法即可。
使用多个专门的接口来取代一个统一的接口。
6. 合成复用原则
尽量使用对象组合
,而不是继承
来达到复用的目的。
在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系。
7. 迪米特法则
- 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。
三、 一段示例代码让你理解OOP的七个原则
这里用一个工厂模式的示例说明涉及到的OOP的七个原则。
可以先看文章最后的总结,会对该小节内容有更好的理解。
示例:给出一个场景,把客户端生成的数据导出。可以导出到文本文件,数据库等。
大家可以考虑一下,我们是否可以定义一个导出类,不同的导出成员方法呢?
class Export {
public:
bool exportDataExcelFile(string data) { //生成数据到Excel文件
cout << "正在导出数据" << data << "到Excel文件" << endl;
return true;
}
bool exportDataDB(string data) { //生成数据到数据库
cout << "正在导出数据" << data << "到数据库" << endl;
return true;
}
};
int main()
{
Export *_export = new Export;
_export->exportDataExcelFile("excel");
_export->exportDataDB("DB");
return 0;
}
功能实现没有问题,我们想一下OOP的原则。
- 上面实现,如果我们每次要添加新的导出方式,每次都需要修改Export ,这有违背
开闭原则
。
开闭原则
(软件实体对扩展是开放的,但对修改是关闭的),也就是我们可以添加新的导出方法,但是导出方法的实现应该对我们是不可见的。 - 不同的导出方式定义在同一个类中,这有违背
单一原则
。
单一原则
(类的职责要单一,不同类型导出数据分别定义不同的类)。
我们现在修改类的定义,不同的导出方式定义成不同的类。
//生成数据到Excel文件
class ExportExcelFile{
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到Excel文件" << endl;
return true;
}
};
//生成数据到数据库
class ExportDB {
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到数据库" << endl;
return true;
}
};
int main()
{
ExportExcelFile *_export1 = new ExportExcelFile;
_export1->exportData("Excel");
ExportDB *_export2 = new ExportDB;
_export1->exportData("DB");
return 0;
}
- 可以看到客户端的调用,还是在依赖底层实现(ExportExcelFile 和 ExportDB ),违背了依赖倒转原则。
依赖倒转原则
(高层模块不依赖于底层模块(ExportExcelFile ,ExportDB), 而是依赖于抽象).
我们现在修改类的定义,让高层模块依赖于抽象。
// 导出数据基类
class ExportFileApi {
public:
virtual bool exportData(string data) = 0;
protected:
ExportFileApi(){}
};
//生成数据到Excel文件
class ExportExcelFile {
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到Excel文件" << endl;
return true;
}
};
//生成数据到数据库
class ExportDB {
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到数据库" << endl;
return true;
}
};
int main()
{
ExportFileApi *_export1 = new ExportExcelFile;
_export1->exportData("excel");
ExportFileApi *_export2 = new ExportDB;
_export1->exportData("DB");
return 0;
}
- 不同的导出方式定义不同的类,符合
单一原则
。 - 客户端调用依赖于抽象(ExportFileApi ),没有依赖于底层模块(ExportExcelFile,ExportDB),符合
依赖倒转原则
。 - 可以使用子类对象(ExportExcelFile,ExportDB)替换基类对象(ExportFileApi ),
符合里氏代换原则
。 - 但是我们仍然能够看到子类的中导出方法实现,不符合
开闭原则
。
我们现在修改类的定义,对导出方式子类再次封装。
// 导出数据基类
class ExportFileApi {
public:
virtual bool exportData(string data) = 0;
protected:
ExportFileApi(){}
};
//生成数据到Excel文件
class ExportExcelFile{
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到Excel文件" << endl;
return true;
}
};
//生成数据到数据库
class ExportDB {
public:
bool exportData(string data) {
cout << "正在导出数据" << data << "到数据库" << endl;
return true;
}
};
//实现一个ExportOperate,这个叫导出数据的业务功能对象
class ExportOperate {//他也是接口
public:
bool exportData(string data) {
ExportFileApi* pApi = factoryMethod();
return pApi->exportData(data);
}
protected:
virtual ExportFileApi* factoryMethod() = 0;
};
//具体的实现对象,完成导出工作
class ExportExcelFileOperate : public ExportOperate {
protected:
ExportFileApi* factoryMethod() {
return new ExportExcelFile();
}
};
class ExportDBOperate :public ExportOperate {
protected:
ExportFileApi* factoryMethod() {
return new ExportDB();
}
};
int main()
{
ExportOperate* pOperate = new ExportExcelFileOperate ();
pOperate->exportData("Hello World");
return 0;
}
这就是工厂模式的实现。
- 不同的导出方式定义不同的类,符合
单一原则
。 - 客户端调用依赖于抽象(ExportFileApi ),没有依赖于底层模块(ExportExcelFile,ExportDB),符合
依赖倒转原则
- 父类指针指向子类对象。 - 客户端对不同导出方式的调用是开放的,但是无法看到不同导出方式的具体实现,符合
开闭原则
-抽象化。
这里做一个简短的总结:
父类指针指向子类对象
class Super;// 抽象基类
class Sub1 :super;// 具体实现子类
class Sub2 :super;// 具体实现子类Super *_super1 = new Sub1();
_super1 ->fun 不变,变化的是 Sub1的实现
Super *_super2 = new Sub2();
_super1 ->fun不变
1 依赖倒转原则
:高层模块不依赖于底层模块(Sub1 ,Sub12), 而是依赖于抽象(Super)。
2 开闭原则
:抽象化是开闭原则的关键,软件实体对扩展是开放的(调用Super的方法),但对修改是关闭的(Sub1,Sub2子类的修改),即在不修改一个软件实体(对Super对象的定义和方法的的调用不用修改)的基础上去扩展功能(只要添加新的实现子类)。
3 里氏代换原则原则
:软件中如果能够使用基类对象,那么一定能够使用其子类对象。所以这里可以 Sub1*_super1 = new Sub1();
Sub2*_super2 = new Sub2();
对于其他几种原则没有细说,后面会有系列文章,针对每一种原则结合代码详解。