一段示例代码(C++)让你完全理解面向对象的七个原则

本章为概述。后续章节将针对每一种原则给出对应的应用场景和代码示例。
知名软件大师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();

对于其他几种原则没有细说,后面会有系列文章,针对每一种原则结合代码详解。

原创文章 18 获赞 53 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qq_27096221/article/details/106077043