2.面向對象設計原則
對於面向對象軟件系統的設計而言,在支持可維護性的同時,提高系統的可複用性是一個至關重要的問題,如何同時提高一個軟件系統的可維護性和可複用性是面向對象設計需要解決的核心問題之一。在面向對象設計中,可維護性的複用是以設計原則為基礎的。每一個原則都藴含一些面向對象設計的思想,可以從不同的角度提升一個軟件結構的設計水平。
面向對象設計原則為支持可維護性複用而誕生,這些原則藴含在很多設計模式中,它們是從許多設計方案中總結出的指導性原則。面向對象設計原則也是我們用於評價一個設計模式的使用效果的重要指標之一。
原則的目的: 高內聚,低耦合
2.1面向對象設計原創表
| 名稱 | 定義 |
|---|---|
| 單一職責原則
(Single Responsibility Principle, SRP) ★★★★☆ |
類的職責單一,對外只提供一種功能,而引起類變化的原因都應該只有一個。 |
| 開閉原則
(Open-Closed Principle, OCP) ★★★★★ |
類的改動是通過增加代碼進行的,而不是修改源代碼。 |
| 里氏代換原則
(Liskov Substitution Principle, LSP) ★★★★★ |
任何抽象類出現的地方都可以用他的實現類進行替換,實際就是虛擬機制,語言級別實現面向對象功能。 |
| 依賴倒轉原則
(Dependence Inversion Principle, DIP) ★★★★★ |
依賴於抽象(接口),不要依賴具體的實現(類),也就是針對接口編程。 |
| 接口隔離原則
(Interface Segregation Principle, ISP) ★★☆☆☆ |
不應該強迫用户的程序依賴他們不需要的接口方法。一個接口應該只提供一種對外功能,不應該把所有操作都封裝到一個接口中去。 |
| 合成複用原則
(Composite Reuse Principle, CRP) ★★★★☆ |
如果使用繼承,會導致父類的任何變換都可能影響到子類的行為。如果使用對象組合,就降低了這種依賴關係。對於繼承和組合,優先使用組合。 |
| 迪米特法則
(Law of Demeter, LoD) ★★★☆☆ |
一個對象應當對其他對象儘可能少的瞭解,從而降低各個對象之間的耦合,提高系統的可維護性。例如在一個程序中,各個模塊之間相互調用時,通常會提供一個統一的接口來實現。這樣其他模塊不需要了解另外一個模塊的內部實現細節,這樣當一個模塊內部的實現發生改變時,不會影響其他模塊的使用。(黑盒原理) |
2.1.1開閉原則案例
本節課圍繞設計模式中開閉原則(Open/Closed Principle,OCP) 展開,以計算器案例為核心,對比傳統實現與符合開閉原則的實現,講解開閉原則的核心思想、應用場景及落地方式。
(1)開閉原則核心概念
①定義(必須掌握)
開閉原則是設計模式中最核心、最重要的原則(“其他原則可以不知道,但這個必須掌握”),核心是:對擴展開放,對修改關閉。
- 「開」:針對功能擴展開放 —— 新增功能時,允許通過新增代碼實現;
- 「閉」:針對原有代碼修改關閉 —— 已編寫完成的源代碼不允許修改。
②核心思想
增加軟件功能的正確方式是新增代碼,而非修改已有源代碼;修改已有代碼易引發未知 bug,增加出錯風險,且會提高調試、維護成本。
(2)反例:傳統計算器實現的問題
①傳統實現思路
將加減乘除所有運算邏輯集中寫在同一個計算器類中:
- 類內定義兩個操作數(a、b)、運算符號(operator);
- 通過判斷運算符號,在
getResult方法中實現所有運算邏輯。
②核心問題
- 耦合度高:所有運算邏輯混雜,修改一處可能影響其他運算;
- 違反開閉原則:新增運算(如取模、平方)時,必須修改原有類的代碼;
- 維護成本高:出錯後難以定位問題(如加法出錯需排查整個類的所有邏輯)。
(3)正例:基於開閉原則的計算器實現
1. 設計思路
通過「抽象類 + 多態」解耦,將不同運算拆分為獨立類(單一職責),每個類僅負責一種運算,新增運算時僅需新增類,無需修改原有代碼。
2. 實現步驟(C++ 代碼落地)
步驟 1:定義抽象計算器基類(AbstractCaculator)
作為所有具體計算器的統一接口,約束核心行為:
class AbstractCalculator{
public:
// 純虛函數:強制子類實現“獲取運算結果”邏輯
virtual int getResult() = 0;
// 純虛函數:強制子類實現“設置操作數”邏輯
virtual void setOperatorNumber(int a, int b) = 0;
};
- 作用:固定核心接口,保證所有具體計算器類的行為一致性,同時屏蔽具體運算細節。
步驟 2:實現具體運算子類(每個類僅負責一種運算)
每個運算(加法 / 減法 / 乘法 / 取模)對應一個子類,繼承抽象基類並實現純虛函數:
| 子類名稱 | 核心實現邏輯 |
|---|---|
| AdditionCalculator(加法) | 重寫setOperatorNumber初始化操作數,getResult返回mA + mB |
| SubtractionCalculator(減法) | 僅修改getResult為mA - mB,其餘邏輯複用 |
| MultiplicationCalculator(乘法) | 僅修改getResult為mA * mB,其餘邏輯複用 |
| DivisionCalculator (除法) | 僅修改getResult為mA / mB,其餘邏輯複用 |
| ModuloCaculator(取模) | 僅修改getResult為mA % mB,其餘邏輯複用 |
子類:
//加法計算器
class AdditionCalculator : public AbstractCalculator
{
public:
// 重寫:設置操作數
virtual void setOperatorNumber(int a, int b)
{
this->mA = a;
this->mB = b;
}
// 重寫:實現加法邏輯
virtual int getResult()
{
return mA + mB;
}
public:
int mA;
int mB;
};
//減法計算器
class SubtractionCalculator : public AbstractCalculator
{
public:
// 重寫:設置操作數
virtual void setOperatorNumber(int a, int b)
{
this->mA = a;
this->mB = b;
}
// 重寫:實現減法邏輯
virtual int getResult()
{
return mA - mB;
}
public:
int mA;
int mB;
};
//乘法計算器
class MultiplicationCalculator : AbstractCalculator
{
public:
// 重寫:設置操作數
virtual void setOperatorNumber(int a, int b)
{
this->mA = a;
this->mB = b;
}
// 重寫:實現乘法邏輯
virtual int getResult()
{
return mA * mB;
}
public:
int mA;
int mB;
};
//除法計算器
class DivisionCalculator : AbstractCalculator
{
public:
// 重寫:設置操作數
virtual void setOperatorNumber(int a, int b)
{
this->mA = a;
this->mB = b;
}
// 重寫:實現除法邏輯
virtual int getResult()
{
return mA * mB;
}
public:
int mA;
int mB;
};
//取模計算器 通過增加代碼來實現
class ModuloCaculator :public AbstractCalculator
{
public:
// 重寫:設置操作數
virtual void setOperatorNumber(int a, int b)
{
this->mA = a;
this->mB = b;
}
// 重寫:實現取模邏輯
virtual int getResult()
{
return mA % mB;
}
public:
int mA;
int mB;
};
步驟 3:通過多態調用具體運算
使用抽象基類指針指向具體子類對象,統一調用接口,新增運算僅需新增子類,無需修改調用邏輯:
void test01()
{
// 加法運算:抽象指針指向加法子類
AbstractCalculator* calculatorPtr = new AdditionCalculator;
calculatorPtr->setOperatorNumber(10, 20);
cout << "ret:" << calculatorPtr->getResult() << endl;
// 減法運算:僅替換子類對象,調用邏輯不變
calculatorPtr = new SubtractionCalculator;
calculatorPtr->setOperatorNumber(10, 20);
cout << "ret:" << calculatorPtr->getResult() << endl;
delete calculatorPtr;
}
(4)代碼注意事項
- 抽象基類中的純虛函數(
=0)強制子類實現,保證接口統一; - 使用堆內存創建子類對象後,需手動
delete釋放,避免內存泄漏; - 子類僅需修改運算邏輯(
getResult),操作數設置邏輯可複用,符合 “修改關閉” 原則。
(5)開閉原則核心價值
- 解耦:單一職責,每個類僅負責一種運算,調試時問題定位精準(如加法出錯僅需排查
AdditionCalculator); - 可擴展:新增運算(如平方、開根)僅需新增子類,無需修改已有代碼,降低出錯風險;
- 長期收益:初期編寫代碼看似 “把簡單問題複雜化”,但後期維護、擴展成本大幅降低。
(6)總結
開閉原則的本質是通過抽象化設計,將 “可變部分”(不同運算)封裝為獨立擴展單元,“不變部分”(抽象接口)固定不修改。核心實踐是:抽象基類定義統一接口,具體實現通過子類擴展,利用多態實現靈活調用,最終達到 “對擴展開放、對修改關閉” 的設計目標。
2.1.2迪米特法則案例
(1)迪米特法則(Law of Demeter)基本概念
-
別名:最少知識原則(Least Knowledge Principle)
-
核心思想:
-
一個類 / 對象應當儘可能少地瞭解其他類 / 對象的細節(“知道越少,耦合越弱”);
-
當兩個類需要交互時,應通過必要的接口實現,避免直接暴露內部細節;
-
核心目標:降低類之間的耦合度,提高系統的可維護性和擴展性。
-
(2)案例場景:樓盤購買問題(從 “高耦合” 到 “低耦合”)
①無中介的 “高耦合” 方案(問題場景)
- 場景描述:
買房人需直接與每個樓盤(如樓盤 A、樓盤 B)打交道:逐個判斷樓盤品質(高品質 / 低品質),符合需求才購買。
-
核心問題:
-
買房人需 “知道所有樓盤的細節”(如樓盤類的存在、品質屬性);
-
若新增樓盤(如樓盤 C、D),需修改買房人的代碼(新增判斷邏輯),耦合度極高;
-
對應代碼:test01() 函數(直接創建 BuildingA/BuildingB 對象,硬編碼判斷品質)。
②引入 “中介” 的 “低耦合” 方案(解決方案)
- 改進思路:
新增 “中介類”,由中介維護所有樓盤信息,買房人僅需向中介傳遞 “需求(如高品質)”,無需關注具體樓盤;
-
中介類的核心作用:
-
封裝 “樓盤管理” 細節(初始化、存儲、查找);
-
對外暴露統一接口(如 “找符合品質的樓盤”),隔絕買房人與具體樓盤的直接交互;
-
優勢:
-
買房人僅需 “知道中介”,無需知道具體樓盤;
-
新增樓盤時,僅需修改中介類,無需修改買房人代碼(符合開閉原則)。
(3)代碼詳細解析(C++ 實現)
①版本定位與核心邏輯梳理
| 版本 | 定位 | 核心邏輯(對應 “樓盤購買” 場景) | 設計思想 |
|---|---|---|---|
| test01 | 初始版本(未應用迪米特法則) | 客户端直接創建BuildingA/BuildingB對象,硬編碼判斷樓盤品質,符合需求則調用sale() | 直接交互,客户端 “包辦一切” |
| test02 | 迭代版本(應用迪米特法則) | 客户端僅通過Mediator(中介類)傳遞需求(如 “高品質”),由中介負責查找樓盤並返回結果,客户端僅調用sale() | 間接交互,中間層 “協調統籌” |
// 02 面向對象設計原則-迪米特法則.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//
#include <iostream>
#include <string>
#include <vector>
using namespace std;
//迪米特法則 又叫最少知識原則
class AbstractBuilding
{
public:
virtual void sale() = 0;
virtual string getQuality() = 0;
};
//樓盤A
class BuildingA : public AbstractBuilding
{
public:
BuildingA()
{
mQulity = "高品質";
}
virtual void sale()
{
cout << "樓盤A" << mQulity << "被售賣" << endl;
}
virtual string getQuality()
{
return mQulity;
}
public:
string mQulity;
};
//樓盤B
class BuildingB : public AbstractBuilding
{
public:
BuildingB()
{
mQulity = "低品質";
}
virtual void sale()
{
cout << "樓盤B" << mQulity << "被售賣" << endl;
}
virtual string getQuality()
{
return mQulity;
}
public:
string mQulity;
};
//中介類
class Mediator
{
public:
Mediator()
{
AbstractBuilding* aBuidingPtr = new BuildingA;
vBuilding.push_back(aBuidingPtr);
aBuidingPtr = new BuildingB;
vBuilding.push_back(aBuidingPtr);
}
~Mediator()
{
for (vector<AbstractBuilding*>::iterator it = vBuilding.begin(); it != vBuilding.end(); it++)
{
if ( *it != NULL)
{
delete* it;
}
}
}
//對外提供接口
AbstractBuilding* findMyBuilding(string quality)
{
for (vector<AbstractBuilding*>::iterator it = vBuilding.begin(); it != vBuilding.end(); it++)
{
if ((*it)->getQuality() == quality)
{
return *it;
}
}
return NULL;
}
public:
vector<AbstractBuilding*> vBuilding;
};
//客户端
void test01()
{
BuildingA* mAbstractBuildingAPtr = new BuildingA;
if (mAbstractBuildingAPtr->mQulity == "低品質")
{
mAbstractBuildingAPtr->sale();
}
BuildingB* mAbstractBuildingBPtr = new BuildingB;
if (mAbstractBuildingBPtr->mQulity == "低品質")
{
mAbstractBuildingBPtr->sale();
}
}
void test02()
{
Mediator* mediatorPtr = new Mediator;
AbstractBuilding* buildingPtr = mediatorPtr->findMyBuilding("高品質");
if (buildingPtr != NULL)
{
buildingPtr->sale();
}
else
{
cout << "沒有符合您條件的樓盤樓盤!" << endl;
}
}
②多維度對比分析
A.設計原則契合度:是否符合迪米特法則(最少知識原則)
a.test01:完全違反迪米特法則
-
“知識範圍過載”:客户端需掌握所有具體樓盤類的細節
-
必須知道BuildingA、BuildingB的存在(需直接創建其對象);
-
必須知道具體樓盤的內部屬性mQulity(直接訪問mQulity判斷品質);
-
若新增樓盤(如BuildingC),客户端需額外學習BuildingC的類名、屬性,違背 “最少知識”。
-
-
無接口隔離:客户端直接操作具體類的內部屬性(如mQulity),而非通過統一接口,暴露了樓盤類的實現細節。
b.test02:完全符合迪米特法則
-
“知識範圍最小化”:客户端僅需瞭解 2 個 “直接朋友”(符合 “只與直接朋友交談” 口訣)
-
Mediator(中介類):客户端唯一交互對象,僅需調用其findMyBuilding()接口傳遞需求;
-
AbstractBuilding(抽象基類):僅需知道其sale()接口(中介返回抽象指針,客户端無需知道具體是BuildingA還是BuildingB);
-
客户端無需瞭解任何具體樓盤類(BuildingA/BuildingB)的細節,完全隔絕 “陌生人”(具體樓盤類)。
-
-
接口隔離實現:通過中介的findMyBuilding()和抽象基類的sale()統一接口,隱藏了樓盤的創建、存儲、查找邏輯,符合 “不暴露內部細節” 的要求。
B.耦合度:類間依賴關係的強弱
(1)test01:強耦合(客户端與具體類深度綁定)
-
依賴對象:客户端直接依賴BuildingA、BuildingB兩個具體類(代碼中顯式new BuildingA()/new BuildingB());
-
耦合表現:
-
若BuildingA的屬性名修改(如mQulity改為quality),客户端代碼需同步修改(mAbstractBuildingAPtr->mQulity需改為mAbstractBuildingAPtr->quality);
-
若刪除BuildingB,客户端需刪除new BuildingB()及對應的判斷邏輯,“牽一髮而動全身”。
-
(2)test02:弱耦合(客户端與抽象 / 中間層綁定)
-
依賴對象:客户端僅依賴Mediator(中間層)和AbstractBuilding(抽象基類),不依賴任何具體樓盤類;
-
耦合表現:
-
具體樓盤類的修改(如BuildingA的mQulity改名),僅需同步修改BuildingA的getQuality()實現(返回新屬性名),客户端無感知;
-
新增 / 刪除具體樓盤類,客户端代碼完全無需修改,僅需調整中介類的內部邏輯(如Mediator構造函數中新增BuildingC)。
-
C.擴展性:應對 “新增需求” 的成本
以 “新增樓盤BuildingC(品質為 “中品質”)” 為例,對比兩個版本的擴展成本:
a.test01:擴展成本極高(客户端需全量修改)
需新增 3 處代碼:
// 1. 新增BuildingC對象創建
BuildingC* mAbstractBuildingCPtr = new BuildingC;
// 2. 新增品質判斷邏輯
if (mAbstractBuildingCPtr->mQulity == "中品質")
{
mAbstractBuildingCPtr->sale();
}
// 3. (若遺漏)可能還需手動釋放`mAbstractBuildingCPtr`,否則內存泄漏
- 問題:擴展時客户端代碼需 “重複造輪子”,且修改點與原有邏輯混在一起,易出錯。
b.test02:擴展成本極低(僅修改中間層)
僅需修改 1 處代碼(Mediator構造函數):
Mediator()
{
// 原有代碼保留
AbstractBuilding* aBuidingPtr = new BuildingA;
vBuilding.push_back(aBuidingPtr);
aBuidingPtr = new BuildingB;
vBuilding.push_back(aBuidingPtr);
// 新增:加入BuildingC
aBuidingPtr = new BuildingC;
vBuilding.push_back(aBuidingPtr);
}
- 優勢:客户端無需任何修改,直接調用mediator->findMyBuilding("中品質")即可獲取BuildingC,符合 “開閉原則”(對擴展開放,對修改關閉)。
D.職責劃分:是否符合 “單一職責原則”
a.test01:職責混亂(客户端承擔過多非自身職責)
- 客户端本應僅負責 “提出購買需求”,但實際承擔了 3 項職責:
- 創建具體樓盤對象(new BuildingA());
- 判斷樓盤是否符合需求(if (mQulity == "低品質"));
- 調用售賣邏輯(sale());
- 後果:客户端代碼臃腫,若需求變化(如判斷邏輯調整),需大面積修改客户端。
b.test02:職責清晰(各角色各司其職)
-
客户端:僅負責 “提出需求”(傳遞品質參數)和 “觸發售賣”(調用sale()),職責單一;
-
Mediator(中介):負責 “樓盤管理”(創建、存儲、查找)和 “內存釋放”,承擔 “協調交互” 的核心職責;
-
具體樓盤類(BuildingA/BuildingB):僅負責 “自身售賣邏輯”(重寫sale())和 “提供品質查詢”(重寫getQuality()),不關心外部交互;
-
優勢:某一角色的邏輯修改(如中介查找規則調整),不影響其他角色,可維護性大幅提升。
E.內存管理:安全性與便捷性
a.test01:內存管理混亂,易泄漏
-
問題:客户端需手動創建BuildingA/BuildingB對象,但代碼中未顯式釋放(如delete mAbstractBuildingAPtr),會導致內存泄漏;
-
隱患:若客户端忘記釋放,或釋放邏輯與創建邏輯分離(如創建在函數開頭,釋放在結尾),易因代碼修改導致 “漏釋放”。
b.test02:內存管理統一,安全可控
- 優勢:Mediator通過析構函數統一管理內存,遍歷vBuilding容器釋放所有動態創建的樓盤對象:
~Mediator()
{
for (vector<AbstractBuilding*>::iterator it = vBuilding.begin(); it != vBuilding.end(); it++)
{
if (*it != NULL) delete *it; // 統一釋放,無泄漏風險
}
}
- 好處:客户端無需關心內存釋放,僅需釋放Mediator對象(或由棧自動回收),降低內存管理成本。
③迭代價值總結:為什麼從test01升級到test02?
| 迭代目標 | test01的痛點 | test02的解決方案 | 最終價值 |
|---|---|---|---|
| 降低耦合度 | 客户端與具體類強綁定,修改牽一髮而動全身 | 引入中介層,客户端僅依賴抽象 / 中間層 | 系統更靈活,修改影響範圍最小化 |
| 提升擴展性 | 新增樓盤需修改客户端代碼 | 新增樓盤僅修改中介構造函數 | 支持快速擴展,符合 “開閉原則” |
| 簡化客户端邏輯 | 客户端承擔創建、判斷、釋放等多職責 | 客户端僅提需求,中介包辦 “找樓盤” | 客户端代碼精簡,易維護 |
| 保障內存安全 | 易漏釋放,內存泄漏風險高 | 中介析構函數統一釋放,無泄漏 | 提升程序穩定性,避免內存問題 |
| 契合迪米特法則 | 客户端 “知道過多”,違反最少知識原則 | 客户端僅與 “直接朋友” 交互,隔絕陌生人 | 符合面向對象設計規範,代碼可複用性更高 |
2.1.3合成複用原則案例
(1)合成複用原則核心定義
- 核心結論:繼承和組合優先使用組合(講課反覆強調 “不要所有地方都用繼承”“優先用組合”)
- 原則本質:避免過度依賴繼承導致的高耦合、低擴展性問題,通過 “組合”(將對象作為類的成員)實現代碼複用,降低類與類之間的依賴關係
(2)代碼案例分析(C++)
①基礎前提:抽象類與具體類設計
所有案例均基於 “車” 的場景,先定義核心類結構:
// 抽象車類(抽象基類,定義共性行為)
class AbstractCar
{
public:
// 純虛函數:所有車的共性行為——啓動
virtual void run() = 0;
};
// 具體車類1:大眾車(繼承抽象車,實現啓動邏輯)
class Dazhong : public AbstractCar
{
public:
virtual void run()
{
cout << "大眾車啓動..." << endl;
}
};
// 具體車類2:拖拉機(繼承抽象車,實現啓動邏輯)
class Tuolaji :public AbstractCar
{
public:
virtual void run()
{
cout << "拖拉機啓動..." << endl;
}
};
②不符合合成複用原則的實現(版本 1)
A.代碼實現
// 問題:通過“繼承具體類”實現“人開車”,違反合成複用原則
class Person : public Tuolaji // 人繼承拖拉機
{
public:
void Doufeng() { run(); } // 兜風=調用拖拉機的啓動方法
};
class PersonB : public Dazhong // 另一個人繼承大眾車
{
public:
void Doufeng() { run(); } // 兜風=調用大眾車的啓動方法
};
B.問題分析(對應講課內容)
- 依賴具體類而非抽象類:Person 依賴 Tuolaji(具體)、PersonB 依賴 Dazhong(具體),不符合 “依賴倒置原則”,也違背合成複用的核心
- 擴展性極差:若新增 “寶馬車”“奔馳車”,需新建PersonC“PersonD” 等類(“每一種具體情況都需要寫一個類”)
- 繼承濫用:將 “人” 與 “具體車型” 強綁定,耦合度高,無法靈活切換車型(“想開大眾就要再繼承大眾,不能複用同一 Person 類”)
③符合合成複用原則的實現(版本 2)
A.代碼實現(核心:用 “組合” 替代 “繼承”)
// 優化:通過“組合”(抽象車指針作為成員)實現“人開車”
class Person
{
public:
// 1. 設置車型(傳入抽象車指針,支持所有具體車型)
void setCar(AbstractCar* aCarPtr)
{
this->carPtr = aCarPtr;
}
// 2. 兜風行為(調用當前車型的啓動方法,自動適配具體車型)
void Doufeng()
{
this->carPtr->run(); // 多態調用:傳什麼車就執行什麼車的run
// 內存管理:避免內存泄漏(使用後釋放車對象)
if (this->carPtr != NULL)
{
delete this->carPtr;
this->carPtr = NULL;
}
}
private:
// 組合核心:依賴抽象車類指針(而非具體類)
AbstractCar* carPtr;
};
// 測試函數:驗證組合的靈活性
void test02()
{
Person* p = new Person;
// 情況1:開大眾車兜風
p->setCar(new Dazhong);
p->Doufeng(); // 輸出“大眾車啓動...”
// 情況2:切換開拖拉機兜風(無需修改Person類)
p->setCar(new Tuolaji);
p->Doufeng(); // 輸出“拖拉機啓動...”
// 釋放Person對象
delete p;
}
int main()
{
test02();
return 0;
}
B.優勢分析(對應講課內容)
- 依賴抽象,解耦具體:Person 僅依賴 AbstractCar(抽象),不依賴任何具體車型,符合 “依賴倒置原則”
- 擴展性極強:新增車型(如寶馬)只需新建Baoma類繼承 AbstractCar,無需修改 Person 類(“給我設置哪個車,我就開哪個車”“類不用變”)
- 靈活切換行為:同一 Person 對象可通過setCar切換不同車型(“開完小客車開拖拉機,不用新建人類”)
- 內存安全:在Doufeng中釋放車對象,避免內存泄漏(講課提到 “類內或類外釋放看需求,此處類內釋放更簡單”)
C.完整代碼:
// 02 面向對象設計原則-合成複用原則.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//
#include <iostream>
using namespace std;
// 抽象車類(抽象基類,定義共性行為)
class AbstractCar
{
public:
// 純虛函數:所有車的共性行為——啓動
virtual void run() = 0;
};
// 具體車類1:大眾車(繼承抽象車,實現啓動邏輯)
class Dazhong : public AbstractCar
{
public:
virtual void run()
{
cout << "大眾車啓動..." << endl;
}
};
// 具體車類2:拖拉機(繼承抽象車,實現啓動邏輯)
class Tuolaji :public AbstractCar
{
public:
virtual void run()
{
cout << "拖拉機啓動..." << endl;
}
};
#if 0
//針對具體類 不使用繼承
class Person : public Tuolaji {
public:
void Doufeng() { run(); }
};
class PersonB : public Dazhong {
public:
void Doufeng() { run(); }
};
#endif
//可以使用組合
class Person
{
public:
void setCar(AbstractCar* aCarPtr)
{
this->carPtr = aCarPtr;
}
void Doufeng()
{
this->carPtr->run();
if (this->carPtr != NULL)
{
delete this->carPtr;
this->carPtr = NULL;
}
}
public:
AbstractCar* carPtr;
};
void test02()
{
Person* p = new Person;
p->setCar(new Dazhong);
p->Doufeng();
p->setCar(new Tuolaji);
p->Doufeng();
delete p;
}
//繼承和組合 優先使用組合
int main()
{
test02();
return 0;
}
(3)繼承 vs 組合(基於本節課案例)
| 對比維度 | 繼承(版本 1) | 組合(版本 2) |
|---|---|---|
| 依賴對象 | 具體類(Tuolaji/Dazhong) | 抽象類(AbstractCar) |
| 耦合度 | 高(強綁定) | 低(鬆耦合) |
| 擴展性 | 差(新增車型需新建人類) | 好(新增車型僅需加具體車類) |
| 靈活性 | 無法切換車型 | 可通過 setCar 靈活切換 |
| 符合原則 | 違反合成複用原則 | 符合合成複用原則 |
(4)本節課核心要點總結
- 合成複用原則核心:繼承和組合優先用組合,拒絕濫用繼承
- 組合的實現關鍵:在類中定義 “抽象類指針” 作為成員,通過方法(如 setCar)注入具體對象,利用多態實現行為複用
- 設計思路:“依賴抽象不依賴具體”,讓類的行為可靈活切換(如 Person 不綁定具體車,而是 “接收” 任意車)
- 內存管理:組合模式中需注意動態對象的釋放(如版本 2 中Doufeng內 delete 車指針,避免泄漏)
- 最終結論:合成複用原則的本質是 “降低耦合、提升擴展”,是後續學習設計模式的基礎(“學設計模式就不要所有地方都用繼承”)
2.1.4依賴倒轉原則案例
傳統的設計模式通常是自頂向下逐級依賴,這樣,底層模塊,中間層模塊和高層模塊的耦合度極高,若任意修改其中的一個,很容易導致全面積的修改,非常麻煩,那麼依賴倒轉原則利用多態的先天特性,對中間抽象層進行依賴,這樣,底層和高層之間進行了解耦合。
(1)課程核心主題
本節課圍繞面向對象設計原則中的依賴倒轉原則展開講解,結合單一職責原則,通過銀行業務辦理的代碼案例,對比傳統開發方式與遵循依賴倒轉原則開發方式的差異,闡述該原則的核心思想與實踐價值。
(2)傳統開發方式的問題
①傳統開發的依賴關係
傳統開發採用自上而下的層級依賴模式:高層業務邏輯模塊依賴中層功能模塊,中層功能模塊依賴底層具體實現模塊,呈現 高層→中層→底層 的單向依賴鏈,類似函數調用的層層嵌套。
②核心弊端
- 耦合度高:層級間依賴具體的實現類,一旦某一層的代碼發生變化(如方法返回值、業務邏輯修改),其上層依賴的模塊都需要同步調整,出現牽一髮而動全身的情況。
- 不利於維護與擴展:新增業務時,需要修改原有類的代碼,違背開閉原則;同時一個類承擔多個職責(如版本 1 中
BankWorker類負責存款、支付、轉賬 3 個業務),違背單一職責原則。
③代碼實例(版本 1)分析
// 04 面向對象設計原則-依賴倒轉原則原則.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//
#include <iostream>
using namespace std;
//銀行工作人員
class BankWorker
{
public:
void saveService()
{
cout << "辦理存款業務..." << endl;
}
void payService()
{
cout << "辦理支付業務.." << endl;
}
void tranferService()
{
cout << "辦理轉賬業務.." << endl;
}
};
//中層模塊
void doSaveBusiness(BankWorker* worker)
{
worker->saveService();
}
void doPayBusiness(BankWorker* worker)
{
worker->payService();
}
void doTransferBusiness(BankWorker* worker)
{
worker->tranferService();
}
void test01()
{
BankWorker* worker = new BankWorker;
doSaveBusiness(worker);//辦理存款業務
doPayBusiness(worker);//辦理支付業務
doTransferBusiness(worker);//辦理轉賬業務
}
int main()
{
std::cout << "Hello World!\n";
}
- 問題 1:
BankWorker類包攬 3 項業務,職責過重。 - 問題 2:中層業務函數
doSaveBusiness等直接依賴BankWorker具體類,若新增 “理財業務”,需要修改BankWorker類並新增對應的中層函數,擴展性差。
(3)依賴倒轉原則的核心思想
- 核心定義
- 高層模塊不應該依賴底層模塊,二者都應該依賴於抽象。
- 抽象不應該依賴於細節,細節應該依賴於抽象。
- 核心目標:通過引入抽象層,打破層級間的具體依賴關係,降低模塊耦合度,提高代碼的擴展性和可維護性。
- 實現關鍵
- 提取抽象層:定義抽象類或接口,包含具體實現類的通用方法。
- 具體類實現抽象:讓不同的具體業務類繼承抽象類並實現抽象方法,遵循單一職責原則。
- 業務模塊依賴抽象:中層業務函數不再依賴具體類,而是依賴抽象類,利用多態特性實現不同業務邏輯。
(4)基於依賴倒轉原則的改進方案(版本 2)
①改進步驟拆解
-
步驟 1:定義抽象層
定義抽象類
AbstractWorker,聲明統一的業務接口doBusiness,作為所有具體業務類的父類。class AbstractWorker { public: virtual void doBusiness() = 0; // 純虛函數,定義抽象接口 }; -
步驟 2:拆分具體業務類(遵循單一職責原則)
將原
BankWorker類的 3 項業務拆分為 3 個獨立的具體類,每個類只負責一項業務,並繼承抽象類實現接口。// 存款業務類 class SaveBanker : public AbstractWorker { public: virtual void doBusiness() { cout << "辦理存款業務..." << endl; } }; // 支付業務類 class PayBanker : public AbstractWorker { public: virtual void doBusiness() { cout << "辦理支付業務..." << endl; } }; // 轉賬業務類 class TransferBanker : public AbstractWorker { public: virtual void doBusiness() { cout << "辦理轉賬業務..." << endl; } }; -
步驟 3:中層函數依賴抽象層
重構中層業務函數
doNewBusiness,參數類型改為抽象類指針,利用多態性,傳入不同的子類對象即可執行對應的業務。void doNewBusiness(AbstractWorker * worker) { worker->doBusiness(); // 多態調用,具體執行邏輯由子類決定 delete worker; } -
步驟 4:業務調用
調用時直接傳入具體業務類的對象,無需修改原有函數即可靈活切換業務。
void test02() { doNewBusiness(new TransferBanker); doNewBusiness(new SaveBanker); doNewBusiness(new PayBanker); }
②改進後的優勢
- 降低耦合:中層函數不再依賴具體業務類,只依賴抽象層
AbstractWorker,具體業務類的修改不會影響中層函數。 - 提高擴展性:新增業務(如 “理財業務”)時,只需新增一個繼承
AbstractWorker的FinancingBanker類,無需修改任何原有代碼,符合開閉原則。 - 遵循單一職責:每個具體業務類只負責一項業務,職責清晰,便於維護。
(5)核心總結
- 依賴倒轉原則的本質是通過抽象層解耦模塊間的依賴關係,讓高層和底層模塊都面向抽象編程。
- 實踐依賴倒轉原則時,通常需要結合單一職責原則,先拆分臃腫類的職責,再抽象出統一接口。
- 核心好處:降低耦合度、提高代碼擴展性、便於維護,是設計模式中實現 “開閉原則” 的重要基礎。
(6)關鍵編程技巧
在需要傳遞類對象作為函數參數時,優先使用抽象類 / 接口類型作為參數類型,而非具體類類型,利用多態特性提升代碼的靈活性。
參考資料來源:黑馬程序員