一段示例代碼(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();

對於其他幾種原則沒有細說,後面會有系列文章,針對每一種原則結合代碼詳解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章