本章爲概述。後續章節將針對每一種原則給出對應的應用場景和代碼示例。
知名軟件大師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();
對於其他幾種原則沒有細說,後面會有系列文章,針對每一種原則結合代碼詳解。