2.面向对象设计原则
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)关键编程技巧
在需要传递类对象作为函数参数时,优先使用抽象类 / 接口类型作为参数类型,而非具体类类型,利用多态特性提升代码的灵活性。
参考资料来源:黑马程序员

浙公网安备 33010602011771号