💻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 · 设计模式