C++ 观察者模式与访问者模式:构建可扩展的事件系统与操作

C++ 观察者模式与访问者模式:事件的优雅舞步与操作的灵活魔术

各位看官,今天咱们不聊那些枯燥的理论,而是来一场C++设计模式的“相声专场”。主角嘛,就是“观察者”和“访问者”这两位老兄。别看名字挺唬人,其实他们俩是构建可扩展事件系统和操作的绝佳搭档。想象一下,你的程序就像一个热闹的剧院,事件是台上表演的演员,而观察者和访问者,就像台下的观众和后台的化妆师,各司其职,让演出更加精彩!

第一幕:观察者模式——事件的广播站

话说,在一个阳光明媚的下午,你开发了一款超级流行的游戏。游戏里有个“主角”角色,他的生命值变化、位置移动,甚至放个屁(咳咳,我是说释放技能),都会引起游戏世界里其他角色的关注。

如果按照传统的方式,主角每次发生变化,都要手动通知所有相关对象,那代码就会变成一坨意大利面条,牵一发而动全身,改起来痛苦不堪,维护起来更是噩梦。

这时候,“观察者”模式就闪亮登场了!它就像一个广播站,主角就是广播员,而那些关心主角状态变化的角色,就是听众。

核心思想:

  • 主题(Subject): 也就是被观察的对象,例如我们的主角。它维护一个观察者列表,并在状态改变时通知所有观察者。
  • 观察者(Observer): 接收主题通知的对象,例如怪物、UI界面等等。它们需要实现一个更新接口,以便接收主题的通知。

代码示例(简化版):

#include <iostream>
#include <vector>

// 观察者接口
class Observer {
public:
    virtual void update(int health) = 0;
};

// 主题类
class Subject {
private:
    std::vector<Observer*> observers;
    int health;

public:
    Subject(int initialHealth) : health(initialHealth) {}

    void attach(Observer* observer) {
        observers.push_back(observer);
    }

    void detach(Observer* observer) {
        // 移除观察者(这里省略具体实现)
    }

    void setHealth(int newHealth) {
        health = newHealth;
        notify();
    }

    int getHealth() const {
        return health;
    }

private:
    void notify() {
        for (Observer* observer : observers) {
            observer->update(health);
        }
    }
};

// 具体观察者 - 怪物
class Monster : public Observer {
public:
    void update(int health) override {
        std::cout << "怪物:主角生命值变为 " << health << ", 我要调整攻击策略!" << std::endl;
    }
};

// 具体观察者 - UI界面
class UI : public Observer {
public:
    void update(int health) override {
        std::cout << "UI:主角生命值变为 " << health << ", 更新UI显示!" << std::endl;
    }
};

int main() {
    Subject* hero = new Subject(100);
    Monster* monster = new Monster();
    UI* ui = new UI();

    hero->attach(monster);
    hero->attach(ui);

    hero->setHealth(50); // 主角受到攻击,生命值降低

    delete hero;
    delete monster;
    delete ui;

    return 0;
}

幽默解读:

想象一下,主题就像一个八卦中心,一旦有什么风吹草动,它就会立刻广播给所有关注者。观察者就像那些吃瓜群众,时刻关注着主角的一举一动,然后根据情况做出反应。有了观察者模式,主角就可以专注于自己的事情,不用操心谁需要知道他的状态变化。

观察者模式的优势:

  • 松耦合: 主题和观察者之间解耦,主题不需要知道具体观察者的类型。
  • 可扩展性: 可以很容易地添加新的观察者,而无需修改主题的代码。
  • 事件驱动: 基于事件的编程模型,使得程序更加灵活和响应迅速。

第二幕:访问者模式——操作的变形金刚

如果说观察者模式是事件的广播站,那么访问者模式就是操作的变形金刚。它允许你在不修改对象结构的前提下,定义作用于这些对象的新操作。

想象一下,你的游戏里有很多种怪物,每种怪物都有不同的属性,比如生命值、攻击力、防御力等等。现在你需要对这些怪物进行各种各样的操作,比如:

  • 计算所有怪物的总生命值。
  • 给所有怪物增加经验值。
  • 统计某种类型怪物的数量。

如果直接在怪物类里添加这些操作,那怪物类就会变得臃肿不堪,而且每次添加新的操作都要修改怪物类的代码,违反了开闭原则。

这时候,“访问者”模式就派上用场了!它把操作从对象结构中分离出来,使得你可以灵活地添加新的操作,而无需修改对象的代码。

核心思想:

  • 元素(Element): 也就是被访问的对象,例如我们的各种怪物。它需要定义一个 accept() 方法,接受访问者的访问。
  • 访问者(Visitor): 定义对元素进行操作的接口。每个具体的访问者都实现对特定元素的操作。

代码示例(简化版):

#include <iostream>
#include <vector>

// 前向声明
class Monster;

// 访问者接口
class Visitor {
public:
    virtual void visit(Monster* monster) = 0;
};

// 元素接口
class Element {
public:
    virtual void accept(Visitor* visitor) = 0;
};

// 具体元素 - 怪物
class Monster : public Element {
private:
    int health;
    std::string type;

public:
    Monster(int health, std::string type) : health(health), type(type) {}

    int getHealth() const {
        return health;
    }

    std::string getType() const {
        return type;
    }

    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
};

// 具体访问者 - 生命值计算器
class HealthCalculator : public Visitor {
private:
    int totalHealth = 0;

public:
    void visit(Monster* monster) override {
        totalHealth += monster->getHealth();
    }

    int getTotalHealth() const {
        return totalHealth;
    }
};

// 具体访问者 - 经验值增加器
class ExpAdder : public Visitor {
private:
    int expToAdd;

public:
    ExpAdder(int exp) : expToAdd(exp) {}

    void visit(Monster* monster) override {
        // 模拟增加经验值的操作
        std::cout << "给 " << monster->getType() << " 增加 " << expToAdd << " 经验值!" << std::endl;
    }
};

int main() {
    std::vector<Element*> monsters;
    monsters.push_back(new Monster(100, "史莱姆"));
    monsters.push_back(new Monster(200, "骷髅"));
    monsters.push_back(new Monster(150, "哥布林"));

    // 计算所有怪物的总生命值
    HealthCalculator* healthCalculator = new HealthCalculator();
    for (Element* monster : monsters) {
        monster->accept(healthCalculator);
    }
    std::cout << "所有怪物的总生命值: " << healthCalculator->getTotalHealth() << std::endl;
    delete healthCalculator;

    // 给所有怪物增加经验值
    ExpAdder* expAdder = new ExpAdder(50);
    for (Element* monster : monsters) {
        monster->accept(expAdder);
    }
    delete expAdder;

    // 清理内存
    for (Element* monster : monsters) {
        delete monster;
    }

    return 0;
}

幽默解读:

想象一下,元素就像一个个等待被“检查身体”的对象,而访问者就像医生,拿着各种各样的工具来“检查”这些对象。不同的医生(访问者)使用不同的工具(操作)来“检查”同一个对象(元素),而对象本身并不需要知道医生要做什么。

访问者模式的优势:

  • 开闭原则: 可以很容易地添加新的操作,而无需修改对象的代码。
  • 单一职责原则: 将操作从对象结构中分离出来,使得对象结构更加简洁。
  • 灵活性: 可以灵活地组合不同的操作,满足不同的需求。

第三幕:观察者与访问者的完美结合

现在,让我们把观察者模式和访问者模式结合起来,看看它们能擦出什么样的火花!

假设你需要监控游戏中所有怪物的生命值变化,并且在生命值低于某个阈值时,触发一个特殊的事件。

你可以使用观察者模式来监控怪物的生命值变化,然后使用访问者模式来处理这个特殊事件。

例如,你可以创建一个 LowHealthVisitor 访问者,当它访问到生命值低于阈值的怪物时,就触发一个警报,或者发送一个消息给玩家。

总结:

观察者模式和访问者模式都是非常有用的设计模式,它们可以帮助你构建可扩展的事件系统和操作。

  • 观察者模式: 用于事件的广播和响应,使得对象之间可以松耦合地进行通信。
  • 访问者模式: 用于操作的扩展,使得你可以灵活地添加新的操作,而无需修改对象的代码。

希望通过今天的“相声专场”,你对观察者模式和访问者模式有了更深入的理解。记住,设计模式不是银弹,不要为了使用而使用,而是要根据实际情况选择合适的模式,才能让你的代码更加优雅、可维护和可扩展。

下次有机会,我们再聊其他的C++设计模式,敬请期待!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注