C++装饰器模式实战:实例代码+避坑指南,新手也能快速上手

C++装饰器模式实战:实例代码+避坑指南,新手也能快速上手 一

文章目录CloseOpen

其实装饰器模式没那么玄乎,今天我就用“说人话”的方式,结合我做过的3个真实项目案例,带你从“看懂”到“会用”,最后还会告诉你5个新手必踩的坑和验证方法,保证你学完就能上手写代码。

从“手机装壳”到代码:装饰器模式到底解决什么问题?

先问你个问题:你给手机装过壳吗?比如一开始用裸机,后来想防摔,加个硅胶壳;觉得不够好看,再套个透明亚克力壳;想无线充电,又加个磁吸壳——手机本身没动过,但功能一点点变多了。装饰器模式干的就是这事儿:不修改原有对象,却能动态给它加新功能

为什么需要这种模式?你想啊,如果你写了一个“订单支付”类,后来要加日志记录、权限校验、性能监控,难道每次都改原来的代码?去年我在电商项目里就遇到过这种情况:原来的支付流程只有基础功能,后来产品经理说要加“支付前验券”“支付后发通知”“异常时记录日志”,团队里有个小伙直接在原有类里加了三个函数,结果不到两周,产品又说要加“风控检查”,代码越改越乱,最后测试时发现“验券”和“风控”的执行顺序反了,排查半天才找到是硬编码导致的。

这就是违反了“开闭原则”——对扩展开放,对修改关闭。而装饰器模式就是来遵守这个原则的。《设计模式:可复用面向对象软件的基础》这本书里早就说过,装饰器模式的核心价值在于“通过组合而非继承来扩展功能”,比直接改代码或用继承灵活多了。你看,手机壳就是“组合”:壳和手机是分开的,想换壳随时换;如果用“继承”,相当于买个新手机,成本高还麻烦。

那在C++里怎么实现这种“手机装壳”逻辑?我用一个最简单的例子给你拆解。假设你有一个“咖啡”基类,有个“cost()”方法返回价格:

class Coffee {

public:

virtual double cost() = 0;

virtual ~Coffee() = default;

};

class SimpleCoffee public Coffee {

public:

double cost() override { return 10.0; } // 基础咖啡10元

};

现在想加牛奶、糖、奶泡,你可能会想:写MilkCoffee继承SimpleCoffeeSugarCoffee继承MilkCoffee……但这样如果要“牛奶+糖”“糖+奶泡”,得写多少个类?用装饰器就简单了:定义一个“调料装饰器”基类,继承Coffee,里面存一个Coffee指针(被装饰的对象),然后每个调料(牛奶、糖)继承这个装饰器:

class CoffeeDecorator public Coffee {

protected:

Coffee coffee; // 被装饰的咖啡

public:

CoffeeDecorator(Coffee c) coffee(c) {}

~CoffeeDecorator() { delete coffee; } // 记得释放被装饰对象

double cost() override = 0;

};

class MilkDecorator public CoffeeDecorator {

public:

MilkDecorator(Coffee c) CoffeeDecorator(c) {}

double cost() override { return coffee->cost() + 3.0; } // 牛奶+3元

};

class SugarDecorator public CoffeeDecorator {

public:

SugarDecorator(Coffee c) CoffeeDecorator(c) {}

double cost() override { return coffee->cost() + 1.5; } // 糖+1.5元

};

用的时候就像“套壳”:Coffee coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));,最后cost()就是10+3+1.5=14.5元。你看,想加新调料,只要写个新的装饰器类,原有代码一行不动——这就是装饰器的魅力。

3个实战场景+完整代码:从日志到性能监控,装饰器能怎么用?

光说咖啡太简单了,接下来我带你看3个真实项目里的场景,每个场景都给你完整代码,你照着写一遍,保证印象深刻。

场景1:日志装饰器——给函数调用加“行为记录”

去年做一个后台管理系统时,我们需要记录“用户操作日志”:谁在什么时间点调用了哪个接口,传了什么参数。当时团队有人说“在每个接口函数里加log语句”,我直接否决了——20多个接口,每个都改一遍?以后要加“日志脱敏”怎么办?

最后我们用了装饰器模式,定义一个LogDecorator,专门负责日志记录,原有接口类完全不动。先看核心代码:

  • 定义抽象接口类
  • (相当于“手机”):

    class UserService {
    

    public:

    virtual void updateUser(const std::string& userId, const std::string& data) = 0;

    virtual ~UserService() = default;

    };

  • 实现基础功能
  • (相当于“裸机”):

    class RealUserService public UserService {
    

    public:

    void updateUser(const std::string& userId, const std::string& data) override {

    // 实际更新逻辑:查数据库、改数据...

    std::cout << "更新用户[" << userId << "]数据: " << data << std::endl;

    }

    };

  • 写日志装饰器
  • (相当于“手机壳”):

    class LogDecorator public UserService {
    

    private:

    UserService service; // 被装饰的服务

    std::string getCurrentTime() { // 获取当前时间

    auto now = std::chrono::system_clock::now();

    std::time_t time = std::chrono::system_clock::to_time_t(now);

    return std::ctime(&time);

    }

    public:

    LogDecorator(UserService s) service(s) {}

    ~LogDecorator() { delete service; } // 释放被装饰对象

    void updateUser(const std::string& userId, const std::string& data) override {

    // 装饰器功能:记录日志

    std::string time = getCurrentTime();

    std::cout << "[" << time << "] 用户操作日志: updateUser, userId=" << userId

    << ", data=" << data << std::endl;

    // 调用被装饰对象的原功能

    service->updateUser(userId, data);

    }

    };

  • 使用时“套壳”
  • int main() {
    

    UserService service = new LogDecorator(new RealUserService());

    service->updateUser("1001", "{name: '张三', age: 25}");

    delete service; // 会自动释放LogDecorator和RealUserService

    return 0;

    }

    运行后会输出:

    [Thu Oct 12 15:30:00 2023] 用户操作日志: updateUser, userId=1001, data={name: '张三', age: 25}
    

    更新用户[1001]数据: {name: '张三', age: 25}

    你看,原有RealUserService一行没改,想加日志就套LogDecorator,以后要加“权限校验”,就再写个AuthDecorator,套两层:new AuthDecorator(new LogDecorator(new RealUserService())),灵活得很。

    场景2:性能监控装饰器——给函数加“秒表”

    今年初做一个支付系统时,我们需要监控“订单创建”接口的耗时,超过500ms就要报警。一开始用的是“在函数前后加时间戳”,但后来要加“调用次数统计”“平均耗时计算”,代码越堆越乱。最后用装饰器重构,一个PerformanceDecorator全搞定。

    核心思路和日志装饰器类似,只是装饰器里多了“计时”和“统计”逻辑。这里我直接给你看关键代码(只列和性能相关的部分):

    class PerformanceDecorator public OrderService {
    

    private:

    OrderService service;

    mutable std::chrono::duration totalTime; // 总耗时

    mutable int callCount = 0; // 调用次数

    public:

    PerformanceDecorator(OrderService s) service(s) {}

    void createOrder(const std::string& orderId) override {

    auto start = std::chrono::high_resolution_clock::now(); // 开始计时

    service->createOrder(orderId); // 调用原功能

    auto end = std::chrono::high_resolution_clock::now(); // 结束计时

    // 统计耗时

    auto duration = end

  • start;
  • totalTime += duration;

    callCount++;

    // 打印监控信息

    double ms = duration.count() 1000;

    std::cout << "接口耗时: " << ms << "ms, 累计调用: " << callCount

    << ", 平均耗时: " << (totalTime.count()1000/callCount) << "ms" << std::endl;

    // 超时报警

    if (ms > 500) {

    std::cout << "警告: 接口耗时超过500ms!" << std::endl;

    }

    }

    };

    用的时候直接套上去:

    OrderService service = new PerformanceDecorator(new RealOrderService());
    

    service->createOrder("ORDER12345"); // 调用时自动计时、统计

    这个装饰器后来帮我们发现了一个隐藏问题:某个时间段接口耗时突然变高,查日志发现是数据库连接池满了,及时扩容才没影响线上业务。你看,用装饰器把“业务逻辑”和“监控逻辑”分开,代码清爽多了。

    新手必踩的5个坑+验证方法:别让你的装饰器“翻车”

    虽然装饰器模式好用,但新手很容易踩坑。我带过5个实习生,他们写的装饰器有80%都出过错,要么是内存泄漏,要么是功能混乱。下面这5个坑,你一定要避开。

    坑1:继承层级混乱——装饰器和被装饰对象必须“长得一样”

    有个实习生第一次写装饰器时,LogDecorator直接继承RealUserService(具体实现类),而不是继承UserService(抽象接口)。结果后来RealUserService改了接口,装饰器直接编译报错。

    为什么错?

    装饰器必须和被装饰对象实现同一个接口,这样使用者才能“把装饰器当被装饰对象用”(里氏替换原则)。就像手机壳必须和手机型号匹配,不然套不上去。 怎么验证? 写完装饰器后,用“多态”测试:定义一个接口指针,指向装饰器对象,调用接口方法看是否正常。比如:

    UserService service = new LogDecorator(new RealUserService());
    

    service->updateUser("1001", "data"); // 如果能正常调用,说明接口一致

    坑2:内存泄漏——忘记释放被装饰对象

    去年有个项目上线后,监控发现内存一直在涨,查了半天才发现:装饰器析构函数里没删service指针!比如这样的错误代码:

    // 错误示例:没释放被装饰对象
    

    class BadLogDecorator public UserService {

    private:

    UserService service;

    public:

    BadLogDecorator(UserService s) service(s) {}

    // 漏写析构函数,service指针没释放!

    };

    为什么错?

    装饰器持有service指针,如果不释放,会导致内存泄漏。特别是多层装饰时(A装饰B,B装饰C),只要有一层没释放,整个链条都会漏。 怎么验证? 用Valgrind工具检查:valgrind leak-check=full ./your_program,如果显示“definitely lost”,就是有内存泄漏。正确做法是在装饰器析构函数里删service

    ~LogDecorator() override { delete service; } // 记得加override,确保覆盖基类析构

    坑3:装饰顺序错误——先穿“内衣”还是先穿“外套”?

    装饰器的调用顺序很重要!比如“先日志后权限”和“先权限后日志”,结果完全不同。今年做一个管理系统时,有个小伙把AuthDecorator(权限校验)套在LogDecorator外面,导致“未登录用户调用接口”时,日志先记录了“调用接口”,然后权限校验才报错——日志里多了一条无效记录。

    为什么错?

    装饰器的执行顺序是“从外到内”。比如A(B(C())),调用时是A先执行,然后B,然后C,最后返回来。所以权限校验这种“前置检查”应该套在最外面,先验权限,通过了再记录日志。 怎么验证? 写单元测试时,故意反序装饰,看是否符合预期。比如:

    // 正确顺序:先权限后日志(先验权限,通过了再记录)
    

    UserService service = new LogDecorator(new AuthDecorator(new RealUserService()));

    // 错误顺序:先日志后权限(日志会记录未授权的调用)

    UserService badService = new AuthDecorator(new LogDecorator(new RealUserService()));

    坑4:过度设计——用装饰器解决“简单问题”

    有个实习生学了装饰器后“走火入魔”,连一个简单的“给字符串加前缀”功能都用装饰器,结果代码多了5个类,反而更难维护。

    为什么错?

    装饰器适合“需要动态扩展多个功能”的场景,如果只是加一个固定功能,直接改代码或用函数包装更简单。就像你给手机贴个膜,没必要用“装饰器”,直接贴就行;但如果你要随时换壳、加磁吸、挂绳,才需要“装饰器思维”。 怎么验证? 问自己:“ 会加3个以上类似功能吗?”如果答案是“否”,别用装饰器;如果是“是”,再考虑。

    坑5:接口不一致——装饰器偷偷改了原函数的行为

    有个项目里,LogDecoratorupdateUser方法返回值和原接口不一样,导致调用方拿不到正确结果。比如原接口返回“是否更新成功”,装饰器却返回了true(不管实际是否成功)。

    为什么错?

    装饰器只能“添加功能”,不能“改变原功能”。就像手机壳不能改变手机的通话功能,装饰器也不能改原函数的输入输出。 怎么验证? 写单元测试时,分别测试“裸对象”和“装饰后对象”的返回值,确保一致。比如:

    // 测试裸对象
    

    RealUserService realService;

    bool realResult = realService.updateUser("1001", "data");

    // 测试装饰后对象

    LogDecorator decoratedService(&realService);

    bool decoratedResult = decoratedService.updateUser("1001", "data");

    assert(realResult == decoratedResult); // 如果断言通过,说明行为一致

    最后再啰嗦一句:装饰器模式的核心是“组合优于继承”,记住“手机装壳”这个例子,你就不会跑偏。你可以先从日志装饰器开始练手,写完后用Valgrind检查内存,用单元测试验证顺序和接口,保证没问题再用在项目里。如果试了有效果,或者遇到新的坑,欢迎回来留言告诉我,咱们一起讨论!


    你知道吗?我之前带过一个实习生,他第一次做第三方系统对接时,把适配器模式和装饰器模式搞混了,结果折腾了两天才改对。当时我们要对接一个老版物流接口,对方返回的是逗号分隔的字符串(比如”12345,北京,20231012″),而我们系统需要JSON格式的物流信息对象。这明显是接口格式不匹配,该用适配器模式转格式,结果他写了个”LogisticsDecorator”,硬是把字符串解析逻辑塞进去,还说”这不就是给物流信息加个‘解析功能’吗?”后来发现,每次调用接口都要先创建装饰器对象,性能差不说,后面换了个返回JSON的新版接口,想去掉解析逻辑时,发现整个调用链都依赖这个”装饰器”,改一行代码连锁反应出三个bug——这就是没搞懂两者的核心区别:装饰器和适配器根本不是一回事,一个是给对象”加新功能”,一个是给接口”做翻译”。

    就像你生活里用的手机配件,适配器是”转接头”,比如你安卓手机想插苹果耳机,得用个3.5mm转Lightning的适配器,解决的是”接口形状不兼容”的问题,耳机本身的听歌功能没变,只是能插不同的设备了;而装饰器是”手机壳”,你给手机套个带支架的壳,手机打电话、拍照的核心功能没变,但多了”看视频不用手举着”的新功能。开发里也一样,比如你有个”用户查询”类,原来的接口是getUser(int id)返回用户对象,现在对接一个旧系统,对方只能传字符串ID(比如”user_123″),这时候用适配器模式,写个UserIdAdapter把字符串”user_123″转成数字123,再传给原接口;如果是想在查询用户时,顺便记录”谁查了哪个用户””查询耗时多久”,这就是给原功能加新职责,该用装饰器模式,套个LogDecoratorPerformanceDecorator。去年做电商项目时,我们对接微信支付接口,对方要求的签名算法参数顺序和我们系统不一样,就用适配器转了参数顺序;后来要加”支付日志+异常重试”,直接套了两个装饰器,既没改原支付逻辑,又灵活加了功能——你看,一个解决”接口对不上”,一个解决”功能不够用”,根本不是一回事。


    C++装饰器模式和继承相比,有什么优势?

    装饰器模式和继承都能扩展功能,但核心区别在于“灵活性”。继承是“静态扩展”——比如为手机添加防摔功能,需要生产一个“防摔手机”子类,想再加磁吸功能,又要生产“防摔+磁吸手机”子类,子类数量会随功能增加呈指数级增长。而装饰器模式是“动态组合”——就像手机壳,想加功能直接套新壳,功能组合随时换(比如今天用“硅胶壳+磁吸壳”,明天换“亚克力壳+挂绳壳”),不用修改原有对象。《设计模式:可复用面向对象软件的基础》中提到,装饰器模式能避免“继承层级爆炸”,尤其适合需要动态添加/移除多个功能的场景。

    什么时候应该用装饰器模式?有没有简单的判断方法?

    判断是否用装饰器模式,记住一个核心问题:“ 是否需要给对象动态添加3个以上独立功能,且功能组合可能变化?” 如果答案是“是”,优先考虑装饰器;如果只是加1-2个固定功能,直接改代码或用函数包装更简单。比如日志+权限+性能监控+风控检查(4个功能),用装饰器能灵活组合;如果只是固定加个日志,直接在函数里写log语句就行。文章里提到的“ 会加3个以上类似功能吗?”这个简单判断法,亲测能帮你避免过度设计。

    多层装饰器的调用顺序怎么确定?比如套了日志、权限、性能3个装饰器,执行顺序是怎样的?

    多层装饰器的调用顺序是“从外到内”,就像你穿衣服:先穿内衣(最内层被装饰对象),再穿毛衣(中间装饰器),最后穿外套(最外层装饰器),脱的时候则从外套开始。比如PerformanceDecorator(AuthDecorator(LogDecorator(RealService()))),调用时最外层的PerformanceDecorator先执行(比如开始计时),然后依次进入AuthDecorator(权限校验)、LogDecorator(记录日志),最后执行RealService的核心逻辑,返回时再反向执行每个装饰器的后续逻辑(比如性能装饰器停止计时)。文章里提到的“权限校验要套在日志外层”就是这个道理,确保先验权限再记录有效日志。

    C++中用装饰器模式会影响性能吗?有没有办法优化?

    装饰器模式会带来轻微性能开销,主要是“多一层指针间接调用”和“装饰器对象的构造/析构”,但对绝大多数业务场景来说可以忽略——除非你在高频调用的核心算法(比如每秒执行百万次的函数)中使用。去年我在支付系统里用装饰器监控订单接口,压测显示单次调用增加约0.5-2微秒(1微秒=0.001毫秒),完全在可接受范围内,换来的灵活性比这点开销更重要。如果实在担心性能,可以用“编译期装饰器”(比如C++11的std::function包装或模板元编程),但新手 先掌握基础的运行时装饰器,再进阶优化。

    装饰器模式和适配器模式看起来很像,怎么区分?

    两者的核心目标完全不同:装饰器模式是“扩展功能”,比如给手机套壳增加防摔功能(手机本身没变,功能变多了);适配器模式是“转换接口”,比如给苹果手机配安卓充电头转换器(解决接口不匹配问题,功能没变)。举个代码例子:如果你的类原来返回int,现在需要返回string,这是接口转换,用适配器模式;如果原来返回int,现在想在返回前记录日志、统计次数,这是扩展功能,用装饰器模式。简单说,“要不要加新功能”是判断关键——加功能用装饰器,转接口用适配器。

    0
    显示验证码
    没有账号?注册  忘记密码?