设计原则之合成复用

合成复用原则(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;
}

重构后的核心优势

  1. 低耦合:Logger实现类的修改(比如ConsoleLogger加时间戳),仅影响使用该实现的业务类,其他业务类完全不受影响;
  2. 封装性完整:Logger的内部状态(如logLevel)是私有成员,业务类只能通过公开接口调用,无法篡改;
  3. 灵活性拉满:运行时可随意切换日志实现(控制台 / 文件 / 数据库),新增日志类型只需加新的Logger子类,符合开闭原则;
  4. 语义正确:OrderService"有"Logger对象,而非 "是"Logger,符合 "has-a" 的合理语义;
  5. 规避继承陷阱:无菱形继承、父类修改传导等问题,代码结构清晰,维护成本极低。

总结

  1. 合成复用原则的核心:优先用组合 / 聚合(has-a)实现功能复用,仅当 "is-a"
    语义成立且需重写父类行为时,才考虑继承;组合是黑箱复用(低耦合、高灵活),继承是白箱复用(高耦合、低灵活);
  2. 违反 CRP 的核心危害:父类修改传导到所有子类(紧耦合)、破坏封装性、复用语义错误、运行时无法替换实现、易引发 C++
    继承陷阱(如菱形继承);
  3. C++ 落地 CRP 的关键:
    a.业务类通过成员变量持有工具类对象(组合 / 聚合);
    b.结合依赖倒置原则,让业务类依赖工具类的抽象接口,而非具体实现;
    c.继承仅用于 "类型扩展"(如 Square 继承 Shape),而非 "功能复用"。

简单来说,合成复用原则的本质是 "用对象组合代替类继承"------ 让代码从 "父子紧绑" 变成 "对象协作",大幅提升灵活性和可维护性。

相关推荐
汉克老师15 小时前
GESP2025年12月认证C++八级真题与解析(单选题10-12)
c++·递归··gesp八级·gesp8级
bkspiderx16 小时前
C++中的map容器:键值对的有序管理与高效检索
开发语言·c++·stl·map
Hard but lovely16 小时前
Linux: 线程同步-- 基于条件变量 &&生产消费模型
linux·开发语言·c++
L_090716 小时前
【C++】高阶数据结构 -- 平衡二叉树(AVLTree)
数据结构·c++
今儿敲了吗16 小时前
C++概述
c++·笔记
C+-C资深大佬17 小时前
C++逻辑运算
开发语言·c++·算法
阿华hhh17 小时前
项目(购物商城)
linux·服务器·c语言·c++
Qhumaing17 小时前
C++学习:【PTA】数据结构 7-2 实验6-2(图-邻接表)
数据结构·c++·学习
好奇龙猫17 小时前
【日语学习-日语知识点小记-日本語体系構造-JLPT-N2前期阶段-第一阶段(1):再次起航】
学习