一、行为型设计模式:让对象之间的协作更优雅
说实话,刚接触设计模式那会儿,我也觉得这东西有点玄乎。不就是写代码吗?搞那么多模式干嘛?直到我在一个大型项目中,被一堆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硬套成状态模式,结果代码量翻了三倍,可读性反而下降了。模式是工具,不是目的。