💻CS 设计模式
观察者与策略模式
难度:⭐⭐ | 高频指数:🔥🔥
面试回答
常见问法
- 观察者模式是什么?有什么应用场景?
- 策略模式是什么?和 if-else 有什么区别?
- C++ 中怎么实现观察者模式?
- 策略模式和多态有什么关系?
- std::function 在设计模式中怎么用?
回答
观察者模式(Observer):
定义对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。也叫发布-订阅模式。
// 简化版观察者
class EventBus {
public:
using Callback = std::function<void(const std::string&)>;
void subscribe(const std::string& event, Callback cb) {
listeners_[event].push_back(std::move(cb));
}
void publish(const std::string& event, const std::string& data) {
for (auto& cb : listeners_[event]) {
cb(data);
}
}
private:
std::unordered_map<std::string, std::vector<Callback>> listeners_;
};
策略模式(Strategy):
定义一系列算法,把它们封装起来,使它们可以互相替换。让算法的变化独立于使用它的客户端。
// 用 std::function 实现策略模式
class Sorter {
public:
using CompareStrategy = std::function<bool(int, int)>;
void setStrategy(CompareStrategy strategy) {
compare_ = std::move(strategy);
}
void sort(std::vector<int>& data) {
std::sort(data.begin(), data.end(), compare_);
}
private:
CompareStrategy compare_ = [](int a, int b) { return a < b; };
};
// 运行时切换算法
Sorter sorter;
sorter.setStrategy([](int a, int b) { return a > b; }); // 降序
sorter.sort(data);
追问
1. 观察者模式的应用场景?
- GUI 事件系统(按钮点击通知处理器)
- 消息队列(发布-订阅)
- MVC 架构(Model 变化通知 View)
- Qt 的信号槽机制
- 配置变更通知
2. 策略模式 vs if-else?
- if-else:所有逻辑写在一起,新增策略要改原代码
- 策略模式:每个策略独立封装,新增策略不改原代码(开闭原则)
- 策略模式适合算法经常变化或需要运行时切换的场景
3. 策略模式和多态的关系?
策略模式本质上就是利用多态。传统实现用继承(策略基类 + 具体策略子类),现代 C++ 可以用 std::function 更灵活地实现。
原理展开
1. 经典观察者模式
经典实现使用 Observer 基类 + Subject 管理观察者列表:
class Observer {
public:
virtual void update(const std::string& msg) = 0;
virtual ~Observer() = default;
};
class Subject {
public:
void attach(Observer* obs) { observers_.push_back(obs); }
void detach(Observer* 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);
}
private:
std::vector<Observer*> observers_;
};
2. 现代 C++ 观察者(std::function 实现)
class EventEmitter {
public:
using Handler = std::function<void(const std::string&)>;
using HandlerId = size_t;
HandlerId on(const std::string& event, Handler handler) {
auto id = nextId_++;
handlers_[event].emplace_back(id, std::move(handler));
return id;
}
void off(const std::string& event, HandlerId id) {
auto& vec = handlers_[event];
vec.erase(std::remove_if(vec.begin(), vec.end(),
[id](const auto& p) { return p.first == id; }),
vec.end());
}
void emit(const std::string& event, const std::string& data) {
if (handlers_.count(event)) {
for (auto& [id, handler] : handlers_[event]) {
handler(data);
}
}
}
private:
std::unordered_map<std::string,
std::vector<std::pair<HandlerId, Handler>>> handlers_;
HandlerId nextId_ = 0;
};
// 使用
EventEmitter emitter;
auto id = emitter.on("data", [](const std::string& msg) {
std::cout << "received: " << msg << std::endl;
});
emitter.emit("data", "hello");
emitter.off("data", id); // 取消订阅
优点: 不需要继承接口、可以用 lambda、支持取消订阅。
Qt 信号槽也是观察者模式的变体,特点是类型安全、支持跨线程、对象销毁时自动断开。
3. 经典策略模式(继承实现)
// 策略接口
class CompressionStrategy {
public:
virtual std::vector<uint8_t> compress(const std::vector<uint8_t>& data) = 0;
virtual ~CompressionStrategy() = default;
};
class GzipStrategy : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::vector<uint8_t>& data) override {
// gzip 压缩实现
return {};
}
};
// 上下文持有策略对象,运行时可替换
class FileCompressor {
public:
void setStrategy(std::unique_ptr<CompressionStrategy> strategy) {
strategy_ = std::move(strategy);
}
void compress(const std::string& filename) {
auto data = readFile(filename);
auto compressed = strategy_->compress(data);
writeFile(filename + ".compressed", compressed);
}
private:
std::unique_ptr<CompressionStrategy> strategy_;
};
4. 现代 C++ 策略模式(std::function)
class HttpClient {
public:
using RetryStrategy = std::function<bool(int attempt, int statusCode)>;
void setRetryStrategy(RetryStrategy strategy) {
retry_ = std::move(strategy);
}
HttpResponse send(HttpRequest req) {
for (int attempt = 0; ; attempt++) {
auto resp = doSend(req);
if (resp.ok() || !retry_(attempt, resp.status())) {
return resp;
}
}
}
private:
RetryStrategy retry_ = [](int, int) { return false; };
};
// 运行时切换策略
HttpClient client;
client.setRetryStrategy([](int attempt, int code) {
return attempt < 3 && code >= 500; // 5xx 重试 3 次
});
用 std::function 实现策略模式比继承更轻量,不需要为每个策略定义一个类。
5. 观察者模式的注意事项
生命周期问题: 观察者被销毁后 Subject 仍持有其指针会导致悬空。解决方案:使用 weak_ptr、析构时自动 detach、或用 ID 管理订阅。
其他注意事项:
- 通知顺序通常不保证
- 观察者在 update 中修改 Subject 可能触发循环通知
- 需要加标志位或延迟通知防止无限循环
易错点
- 混淆观察者模式和策略模式——观察者是”通知多个对象”,策略是”切换算法”。
- 观察者模式忘记处理生命周期——裸指针观察者被销毁后会悬空。
- 说”策略模式必须用继承”——现代 C++ 用 std::function 更灵活。
- 忘记观察者模式的循环通知问题——update 中修改 Subject 可能死循环。
- 说不出实际应用场景——观察者:事件系统、MVC;策略:排序算法、压缩算法、重试策略。
- 过度设计——只有一种策略时不需要策略模式,直接写就行。
记忆技巧
- 观察者一句话:一个变,多个跟着变(一对多通知)
- 策略一句话:同一件事,不同做法可以换(运行时切换算法)
- 观察者关键词:订阅、发布、通知、回调
- 策略关键词:封装算法、互相替换、消除 if-else
- 现代 C++ 实现:std::function 替代继承,更灵活更简洁
面试速答版
观察者模式定义一对多的依赖关系,当对象状态改变时通知所有观察者。典型应用:事件系统、MVC、Qt 信号槽。C++ 中可以用 std::function + 容器实现,比继承更灵活。策略模式将算法封装成独立对象,可以运行时切换。本质是用组合替代 if-else,符合开闭原则。C++ 中用 std::function 实现策略模式非常自然,比传统继承方式更轻量。两者的区别:观察者关注”通知谁”,策略关注”怎么做”。注意观察者的生命周期管理和循环通知问题。
Related · 设计模式