一、行为型设计模式:让对象之间的协作更优雅

说实话,刚接触设计模式那会儿,我也觉得这东西有点玄乎。不就是写代码吗?搞那么多模式干嘛?直到我在一个大型项目中,被一堆if-else和回调函数折磨得欲仙欲死……嗯,从那以后,我才真正理解了设计模式的价值。

行为型设计模式,说白了就是解决「对象之间怎么通信、怎么协作」的问题。今天我要讲的这五个模式——观察者、策略、命令、迭代器、状态——是我在实际项目中用得最多的。每个模式都解决一类特定的问题,用对了,代码会变得特别清爽。

核心思想:行为型模式关注的是对象之间的责任分配和算法封装。它们让对象间的交互更灵活、更可复用。

行为型设计模式知识体系 行为型模式 观察者模式 策略模式 命令模式 迭代器模式 状态模式 核心特点 ✅ 对象间松耦合 ✅ 算法可替换 ✅ 请求可封装 ✅ 遍历与容器分离 ✅ 行为随状态改变 适用于:需要灵活应对变化、降低对象间耦合度的场景

二、观察者模式:一对多的依赖关系

观察者模式,我习惯叫它「发布-订阅」模式。什么意思呢?就是一个对象(主题)状态变了,所有依赖它的对象(观察者)都能自动收到通知。

我在做游戏引擎的时候用过这个模式。玩家角色血量变化,UI的血条、声音系统、成就系统都要响应。如果用硬编码去通知,代码会乱成一锅粥。观察者模式完美解决了这个问题。

2.1 核心结构

  • Subject(主题):维护观察者列表,提供注册、移除、通知接口
  • Observer(观察者):定义更新接口,主题变化时被调用
  • ConcreteSubject(具体主题):状态变化时通知所有观察者
  • ConcreteObserver(具体观察者):实现更新接口,处理通知

2.2 代码示例

// 观察者接口
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void update(const std::string& message) = 0;
};

// 主题基类
class Subject {
private:
    std::vector<std::shared_ptr<IObserver>> observers_;
public:
    void attach(std::shared_ptr<IObserver> obs) {
        observers_.push_back(obs);
    }
    void detach(std::shared_ptr<IObserver> obs) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), obs),
            observers_.end()
        );
    }
    void notify(const std::string& msg) {
        for (auto& obs : observers_) {
            obs->update(msg);
        }
    }
};

// 具体观察者:日志系统
class Logger : public IObserver {
public:
    void update(const std::string& msg) override {
        std::cout << "[日志] " << msg << std::endl;
    }
};

// 具体观察者:UI更新
class UIUpdater : public IObserver {
public:
    void update(const std::string& msg) override {
        std::cout << "[UI] 刷新界面,原因:" << msg << std::endl;
    }
};

避坑指南:我曾经在观察者模式中犯过一个低级错误——在通知过程中动态增删观察者,导致迭代器失效。后来我改用「先拷贝列表再遍历」的方式,问题就解决了。

三、策略模式:算法的自由切换

策略模式,说白了就是把「做什么」和「怎么做」分开。你想想看,一个排序函数,今天用快排,明天用归并,难道每次都要改函数内部代码吗?

我个人习惯用策略模式来处理「多种相似算法」的场景。比如支付系统,微信支付、支付宝、银行卡,每种支付方式就是一个策略。

3.1 核心结构

  • Strategy(策略接口):定义算法的公共接口
  • ConcreteStrategy(具体策略):实现具体的算法
  • Context(上下文):持有策略对象,调用策略执行算法

3.2 代码示例

// 策略接口
class CompressionStrategy {
public:
    virtual ~CompressionStrategy() = default;
    virtual void compress(const std::string& file) = 0;
};

// 具体策略:ZIP压缩
class ZipCompression : public CompressionStrategy {
public:
    void compress(const std::string& file) override {
        std::cout << "使用ZIP压缩: " << file << std::endl;
    }
};

// 具体策略:RAR压缩
class RarCompression : public CompressionStrategy {
public:
    void compress(const std::string& file) override {
        std::cout << "使用RAR压缩: " << file << std::endl;
    }
};

// 上下文
class Compressor {
private:
    std::unique_ptr<CompressionStrategy> strategy_;
public:
    void setStrategy(std::unique_ptr<CompressionStrategy> s) {
        strategy_ = std::move(s);
    }
    void compress(const std::string& file) {
        if (strategy_) strategy_->compress(file);
    }
};

使用场景:表单验证、数据导出格式、游戏AI行为、支付渠道选择……只要算法可以互相替换,就用策略模式。

四、命令模式:把请求封装成对象

命令模式,就是把一个操作封装成一个对象。这样做的好处是:你可以把命令排队、记录日志、支持撤销重做。

我记得在做一个编辑器的时候,用户每次操作都要支持Ctrl+Z撤销。如果用函数调用,撤销逻辑会非常难写。用了命令模式后,每个操作都是一个命令对象,撤销就是执行反向命令,简单多了。

4.1 核心结构

  • Command(命令接口):定义执行和撤销接口
  • ConcreteCommand(具体命令):绑定接收者,实现具体操作
  • Invoker(调用者):持有命令对象,触发执行
  • Receiver(接收者):真正执行操作的对象

4.2 代码示例

// 命令接口
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

// 接收者:文本编辑器
class TextEditor {
public:
    void insertText(const std::string& text) {
        content_ += text;
        std::cout << "插入: " << text << std::endl;
    }
    void deleteText(size_t len) {
        if (len > content_.size()) len = content_.size();
        deleted_ = content_.substr(content_.size() - len);
        content_.erase(content_.size() - len);
        std::cout << "删除: " << deleted_ << std::endl;
    }
    void undoDelete() {
        content_ += deleted_;
        std::cout << "撤销删除: " << deleted_ << std::endl;
    }
private:
    std::string content_;
    std::string deleted_;
};

// 具体命令:插入命令
class InsertCommand : public Command {
private:
    TextEditor* editor_;
    std::string text_;
public:
    InsertCommand(TextEditor* e, const std::string& t) 
        : editor_(e), text_(t) {}
    void execute() override { editor_->insertText(text_); }
    void undo() override { editor_->deleteText(text_.size()); }
};

注意:命令模式虽然强大,但不要滥用。如果只是简单的函数调用,没必要硬套命令模式。我见过有人把每个getter都封装成命令……那真是杀鸡用牛刀。

五、迭代器模式:统一遍历方式

迭代器模式,C++程序员应该最熟悉了。STL里的迭代器就是最经典的实现。它的核心思想是:把遍历逻辑从容器中抽离出来。

为什么要这么做?你想想看,vector用下标遍历,list用指针遍历,map用键值对遍历……如果没有统一接口,每种容器都要写不同的遍历代码,多麻烦。

5.1 核心结构

  • Iterator(迭代器接口):定义hasNext、next等遍历方法
  • ConcreteIterator(具体迭代器):实现特定容器的遍历逻辑
  • Aggregate(聚合接口):定义创建迭代器的方法
  • ConcreteAggregate(具体聚合):返回对应的迭代器实例

5.2 代码示例

// 自定义迭代器
template<typename T>
class Iterator {
public:
    virtual ~Iterator() = default;
    virtual bool hasNext() = 0;
    virtual T next() = 0;
};

// 自定义容器
template<typename T>
class MyCollection {
private:
    std::vector<T> data_;
public:
    void add(const T& item) { data_.push_back(item); }
    
    // 返回迭代器
    std::unique_ptr<Iterator<T>> createIterator() {
        return std::make_unique<MyIterator<T>>(data_);
    }
};

// 具体迭代器
template<typename T>
class MyIterator : public Iterator<T> {
private:
    const std::vector<T>& data_;
    size_t index_ = 0;
public:
    MyIterator(const std::vector<T>& d) : data_(d) {}
    bool hasNext() override { return index_ < data_.size(); }
    T next() override { return data_[index_++]; }
};

小技巧:在C++中,尽量使用STL自带的迭代器,不要自己造轮子。除非你正在实现一个自定义容器,否则标准库的迭代器已经够用了。

六、状态模式:行为随状态而变

状态模式,说白了就是「一个对象在不同状态下有不同的行为」。最常见的例子就是TCP连接:连接态、监听态、关闭态,每个状态下收到数据包的处理方式都不一样。

我曾经写过一个工作流引擎,每个工单有「待审批」「审批中」「已通过」「已驳回」等状态。如果用if-else去判断,代码会膨胀到没法看。状态模式让每个状态独立成一个类,清爽多了。

6.1 核心结构

  • State(状态接口):定义状态相关的行为
  • ConcreteState(具体状态):实现特定状态下的行为
  • Context(上下文):持有当前状态对象,状态变化时切换

6.2 代码示例

// 状态接口
class State {
public:
    virtual ~State() = default;
    virtual void handle() = 0;
    virtual std::string getName() = 0;
};

// 具体状态:空闲状态
class IdleState : public State {
public:
    void handle() override {
        std::cout << "空闲状态:等待任务..." << std::endl;
    }
    std::string getName() override { return "空闲"; }
};

// 具体状态:忙碌状态
class BusyState : public State {
public:
    void handle() override {
        std::cout << "忙碌状态:正在处理任务..." << std::endl;
    }
    std::string getName() override { return "忙碌"; }
};

// 上下文
class Worker {
private:
    std::unique_ptr<State> state_;
public:
    Worker() : state_(std::make_unique<IdleState>()) {}
    
    void setState(std::unique_ptr<State> s) {
        state_ = std::move(s);
    }
    
    void work() {
        state_->handle();
    }
    
    std::string currentState() {
        return state_->getName();
    }
};

状态模式 vs 策略模式:这两个模式结构很像,但意图不同。策略模式是「算法可替换」,状态模式是「行为随状态变化」。策略模式由客户端选择策略,状态模式由对象内部自动切换状态。

七、总结对比

模式 核心意图 典型场景 关键点
观察者模式 一对多通知 事件系统、消息推送 主题与观察者解耦
策略模式 算法可替换 支付方式、压缩算法 策略接口统一
命令模式 请求封装为对象 撤销重做、任务队列 支持undo/redo
迭代器模式 统一遍历接口 STL迭代器、自定义容器 遍历与容器分离
状态模式 行为随状态改变 工作流、游戏状态机 状态自动切换

这五个模式,说白了就是解决「对象之间怎么协作」这个问题的不同角度。观察者解决通知问题,策略解决算法选择问题,命令解决请求封装问题,迭代器解决遍历问题,状态解决行为变化问题。

我个人建议,刚开始学的时候不要贪多。先搞懂观察者和策略模式,这两个最常用。等用熟了,再慢慢引入命令模式和状态模式。迭代器模式嘛……C++程序员每天都在用,只是你可能没意识到它是个设计模式。

最后说一句:设计模式不是银弹。不要为了用模式而用模式。我见过最糟糕的代码,就是把简单的if-else硬套成状态模式,结果代码量翻了三倍,可读性反而下降了。模式是工具,不是目的。

公众号:蓝海资料掘金营,微信deep3321