合成复用原则(CRP)的核心理解
合成复用原则是面向对象设计的核心原则之一,核心定义可以概括为:
- 优先使用对象的「组合(Composition)」或「聚合(Aggregation)」关系实现代码复用,而非类的「继承」关系。
用 "语义 + 特性" 拆解这个原则,帮你快速区分两种复用方式的本质差异:
| 复用方式 | 核心语义 | 特性(C++ 视角) | 复用类型 |
|---|---|---|---|
| 继承 | is-a(比如 "正方形是一种矩形") | 子类继承父类的所有成员(保护 / 公有),编译期静态绑定 | 白箱复用(父类内部细节对子类可见) |
| 组合 / 聚合 | has-a(比如 "订单服务有日志器") | 一个类持有另一个类的对象,通过调用对象方法复用功能,运行时可动态替换 | 黑箱复用(被复用类的内部细节完全不可见) |
| 通俗类比: |
-
违反 CRP:你想给手机加 "拍照功能",直接让 "手机类" 继承 "相机类"(手机是一种相机?语义错误,且相机的任何修改都会影响手机);
-
符合 CRP:你给手机类里加一个 "相机对象"(手机有相机),通过调用相机对象的拍照方法实现功能,相机升级 / 替换都不影响手机核心逻辑。
文章目录
- [违反合成复用原则的反面例子(C++ 代码 + 危害)](#违反合成复用原则的反面例子(C++ 代码 + 危害))
-
- [1. 违反 CRP 的代码实现](#1. 违反 CRP 的代码实现)
- [2. 违反 CRP 带来的具体危害(结合代码分析)](#2. 违反 CRP 带来的具体危害(结合代码分析))
- 符合合成复用原则的重构示例
- 重构后的核心优势
- 总结
违反合成复用原则的反面例子(C++ 代码 + 危害)
选择 "电商订单系统的日志复用" 这个贴近实际的场景 ------ 通过继承让业务类复用日志功能,直观展示违反 CRP 的问题。
1. 违反 CRP 的代码实现
核心问题:OrderService(订单服务)、PaymentService(支付服务)通过继承Logger类复用日志功能,而非组合;继承的 "is-a" 语义不成立,且导致紧耦合。
cpp
#include <iostream>
#include <string>
using namespace std;
// 日志工具类:提供基础日志功能
class Logger {
protected:
string logLevel = "INFO"; // 保护成员,子类可直接访问(破坏封装)
public:
// 基础日志方法
void log(const string& msg) {
cout << "[" << logLevel << "] " << msg << endl;
}
// 后续需求变更:修改日志级别设置逻辑
void setLogLevel(const string& level) {
if (level != "INFO" && level != "DEBUG" && level != "ERROR") {
throw invalid_argument("无效的日志级别");
}
logLevel = level;
}
};
// 订单服务:继承Logger复用日志(违反CRP,语义错误:订单服务不是日志器)
class OrderService : public Logger {
public:
void createOrder(const string& orderId) {
// 子类直接复用父类log方法,但也能修改父类保护成员
logLevel = "DEBUG"; // 随意修改父类内部状态,破坏封装
log("创建订单:" + orderId);
cout << "订单" << orderId << "创建成功" << endl;
}
};
// 支付服务:同样继承Logger复用日志(违反CRP)
class PaymentService : public Logger {
public:
void processPayment(const string& orderId, double amount) {
log("处理支付:订单" + orderId + ",金额" + to_string(amount));
cout << "支付处理完成" << endl;
}
};
int main() {
OrderService orderService;
PaymentService paymentService;
orderService.createOrder("OD123456");
paymentService.processPayment("OD123456", 99.0);
// 父类修改引发的问题:PaymentService调用setLogLevel时抛异常
try {
paymentService.setLogLevel("WARN"); // 父类新增的校验逻辑影响子类
} catch (const invalid_argument& e) {
cout << "错误:" << e.what() << endl;
}
return 0;
}
2. 违反 CRP 带来的具体危害(结合代码分析)
这个设计看似 "简单直接",但在实际开发中会暴露一系列致命问题,每一个都源于 "用继承代替组合":
危害 1:紧耦合 ------ 父类修改会传导到所有子类,牵一发而动全身
Logger类的任何修改(比如新增日志级别校验、修改 log 方法格式),都会直接影响OrderService、PaymentService等所有子类:
- 比如Logger新增 "日志级别校验" 后,PaymentService调用setLogLevel("WARN")会直接抛异常(哪怕
PaymentService 原本不需要这个校验); - 如果Logger删除setLogLevel方法,所有子类都会编译错误,哪怕子类根本没用到这个方法。
危害 2:破坏封装性 ------ 子类可随意修改父类内部状态
Logger的logLevel是保护成员,子类OrderService可以直接修改(比如logLevel = "DEBUG"),完全突破了Logger的封装边界:
-
这会导致Logger的内部状态被意外篡改,比如其他子类依赖INFO级别,却因 OrderService 的修改导致日志输出异常;
-
后期想优化Logger的内部实现(比如把logLevel改成私有),必须修改所有子类,成本极高。
危害 3:语义错误 ------ 违背 "is-a" 的继承本质
继承的核心前提是 "子类是父类的一种特殊类型",但OrderService的核心职责是处理订单,Logger是工具类,"订单服务是一种日志器" 显然逻辑错误:
-
这种语义混乱会增加代码理解成本,新开发者会误以为OrderService具备日志器的所有特性;
-
还可能违反里氏替换原则 ------
如果用Logger指针指向OrderService对象,调用log方法看似正常,但OrderService的其他订单逻辑会成为
"多余负担"。
危害 4:复用灵活性为 0------ 运行时无法替换实现
继承是编译期静态绑定的,一旦OrderService继承了Logger,运行时无法切换日志实现(比如从控制台日志改成文件日志):
- 若产品要求"订单日志写入文件,支付日志输出到控制台",只能新增FileLogger、ConsoleLogger两个父类,让OrderService继承 FileLogger、PaymentService继承ConsoleLogger------最终导致类数量爆炸(业务类 × 日志类)。
危害 5:易引发 C++ 特有的继承陷阱
如果后续Logger继承自BaseLogger,OrderService又需要继承另一个ServiceBase类,会触发 C++ 的菱形继承问题:
cpp
// 新增BaseLogger
class BaseLogger {};
class Logger : public BaseLogger {};
// OrderService同时继承ServiceBase和Logger,引发菱形继承
class ServiceBase {};
class OrderService : public ServiceBase, public Logger {};
这会导致OrderService中出现BaseLogger的两份实例,必须用virtual继承解决,大幅增加代码复杂度。
符合合成复用原则的重构示例
核心思路:让业务类通过组合(持有 Logger 对象)复用日志功能,结合依赖倒置原则让业务类依赖日志抽象,而非具体实现 ------ 既符合 CRP,又兼顾灵活性。
cpp
#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;
// 第一步:定义日志抽象接口(依赖倒置,增强扩展性)
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const string& msg) = 0; // 抽象日志行为
};
// 第二步:具体日志实现(控制台日志)
class ConsoleLogger : public Logger {
private:
string logLevel = "INFO"; // 私有成员,封装性完整
public:
void setLogLevel(const string& level) {
if (level != "INFO" && level != "DEBUG" && level != "ERROR") {
throw invalid_argument("无效的日志级别");
}
logLevel = level;
}
void log(const string& msg) override {
cout << "[" << logLevel << "] [控制台] " << msg << endl;
}
};
// 第二步:新增文件日志实现(无需修改业务类)
class FileLogger : public Logger {
public:
void log(const string& msg) override {
cout << "[INFO] [文件] " << msg << endl;
// 实际开发中写入文件,业务类无需关心实现细节
}
};
// 第三步:订单服务------组合Logger对象复用日志(符合CRP)
class OrderService {
private:
Logger* logger; // 持有Logger对象(has-a),依赖抽象而非具体
public:
// 构造函数注入Logger实现,运行时可灵活替换
OrderService(Logger* log) : logger(log) {}
void createOrder(const string& orderId) {
// 调用Logger对象的方法实现复用,无法访问其内部状态
logger->log("创建订单:" + orderId);
cout << "订单" << orderId << "创建成功" << endl;
}
};
// 第三步:支付服务------同样用组合复用日志
class PaymentService {
private:
Logger* logger;
public:
PaymentService(Logger* log) : logger(log) {}
void processPayment(const string& orderId, double amount) {
logger->log("处理支付:订单" + orderId + ",金额" + to_string(amount));
cout << "支付处理完成" << endl;
}
};
int main() {
// 场景1:订单服务用文件日志(运行时替换)
FileLogger fileLog;
OrderService orderService(&fileLog);
orderService.createOrder("OD123456");
// 场景2:支付服务用控制台日志(运行时替换)
ConsoleLogger consoleLog;
consoleLog.setLogLevel("DEBUG");
PaymentService paymentService(&consoleLog);
paymentService.processPayment("OD123456", 99.0);
// 场景3:订单服务切换为控制台日志(无需修改OrderService代码)
OrderService orderService2(&consoleLog);
orderService2.createOrder("OD789012");
return 0;
}
重构后的核心优势
- 低耦合:Logger实现类的修改(比如ConsoleLogger加时间戳),仅影响使用该实现的业务类,其他业务类完全不受影响;
- 封装性完整:Logger的内部状态(如logLevel)是私有成员,业务类只能通过公开接口调用,无法篡改;
- 灵活性拉满:运行时可随意切换日志实现(控制台 / 文件 / 数据库),新增日志类型只需加新的Logger子类,符合开闭原则;
- 语义正确:OrderService"有"Logger对象,而非 "是"Logger,符合 "has-a" 的合理语义;
- 规避继承陷阱:无菱形继承、父类修改传导等问题,代码结构清晰,维护成本极低。
总结
- 合成复用原则的核心:优先用组合 / 聚合(has-a)实现功能复用,仅当 "is-a"
语义成立且需重写父类行为时,才考虑继承;组合是黑箱复用(低耦合、高灵活),继承是白箱复用(高耦合、低灵活); - 违反 CRP 的核心危害:父类修改传导到所有子类(紧耦合)、破坏封装性、复用语义错误、运行时无法替换实现、易引发 C++
继承陷阱(如菱形继承); - C++ 落地 CRP 的关键:
a.业务类通过成员变量持有工具类对象(组合 / 聚合);
b.结合依赖倒置原则,让业务类依赖工具类的抽象接口,而非具体实现;
c.继承仅用于 "类型扩展"(如 Square 继承 Shape),而非 "功能复用"。
简单来说,合成复用原则的本质是 "用对象组合代替类继承"------ 让代码从 "父子紧绑" 变成 "对象协作",大幅提升灵活性和可维护性。