《C++20设计模式》学习笔记——第13章职责链模式

本文详细探讨了职责链模式在电子游戏中的应用,使用指针链实现修改器的堆叠和顺序处理。同时介绍了代理链,结合中介者和观察者模式,通过事件机制临时修改生物属性。
摘要由CSDN通过智能技术生成


职责链模式是一种非常简单的设计模式,允许组件依次处理命令(或查询)。职责链(Chain of Responsibility, CoR)的一个简单示例:系统中有许多个不同的元素,它们可以一个接一个地处理消息。

1. 预想方案

想象一款电子游戏,其中的每个生物都具有名字和两个特性值——攻击值(attack)和防御值(defense):

struct Creature
{
    std::string name_;
    int attack_, defense_;
};

在游戏过程中,生物的攻击值和防御值会被CreatureModifier(生物特性修改器)修改(如获取了道具或被施法等),并且可以同时受到多个修改器的影响。因此我们需要能够将修改器堆叠在另一个修改器之上,允许它们按照添加的顺序应用,可以使用指针链实现这种功能。

2. 指针链
class CreatureModifier
{
    CreatureModifier* next_{nullptr};
protected:
    Creature& creature_;
public:
    explicit CreatureModifier(Creature& c) : creature_{c} {}
    CreatureModifier* add(CreatureModifier* cm)
    {
        if(next_) {
            next_->add(cm);
        }
        else {
            next_ = cm;
            return next_;
        }
    }
    virtual void handle()
    {
        if(next_) {
            next_->handle();
        }
    }
};

上面这段代码中发生了很多事,我们来逐个讨论它们:

  • 构造函数接受并保存一个Creature对象的引用,这个对象也就是CreatureModifier要修改的对象。
  • 这个类做的工作不多,但它也并不是抽象类,它的所有接口都提供了实现。
  • next成员指向此成员之后的可选CreatureModifier对象。当然,这意味着它指向的修改器是CreatureModifier的派生类对象。
  • 函数add()向修改器添加另一个修改器,这个操作是递归的。
  • 函数handle()只处理链中的下一项(如果存在的话):它没有自己的行为,它是虚函数,这意味着它必须被覆写。

到目前为止,我们所拥有的只是一个简陋版本的在单链表中添加元素的实现。但当我们开始继承它时,整个层次结构将变得更加清晰。下面的代码示范了如何定义使生物的攻击值翻倍的修改器:

class DoubleAttackModifier : public CreatureModifier
{
public:
    explicit DoubleAttackModifier(Creature& c) : CreatureModifier{c} {}

    void handle() override
    {
        creature_.attack_ *= 2;
        CreatureModifier::handle();
    }
};

重点在于handle()函数的重写,将攻击值翻倍,并调用基类的handle()。第二件事很关键:使修改器链发挥链式作用的唯一方法是每个继承者在自己的handle()实现结束时不要忘记调用基类的handle()方法。

我们可以类似地定义其他更加复杂的修改器,并将一系列修改应用到一个生物对象上。

class IncreaseDefenseModifier : public CreatureModifier
{
public:
    explicit IncreaseDefenseModifier(Creature& c) : CreatureModifier{c} {}

    void handle() override
    {
        if(creature_.attack_ <= 4) {
            creature_.defense_ += 1;
        }
        CreatureModifier::handle();
    }
};

void test_simple_CoR()
{
    Creature luban{"luban", 1, 1};
    std::cout << luban << std::endl;
    CreatureModifier m0{luban};
    DoubleAttackModifier m1{luban};
    DoubleAttackModifier m2{luban};
    IncreaseDefenseModifier m3{luban};
    DoubleAttackModifier m4{luban};

    m0.add(&m1)->add(&m2)->add(&m3);  //注意:同一个Modifier不能add两次,否则会陷入死循环!
    m0.add(&m4);
    m0.handle();

    std::cout << "=============" << std::endl;
    std::cout << luban << std::endl;
}

如果我们决定对一个生物施加咒语,使其无法获得任何奖励,这容易吗?实际上非常简单,因为我们要做的就是避免调用基类的handle()函数,这样可以避免执行整个链:

class NoBonusesModifier : public CreatureModifier
{
public:
    explicit NoBonusesModifier(Creature& c) : CreatureModifier{c} {}

    void handle() override
    {
        // nothing here!
    }
};

这样就完成了!在大多数情况下,我们遇到的职责链都是单链表,通常在链表的末尾附加其他的项目元素。但在某些情况下,我们也可以自定义链表,例如在某种map<int, Modifier*>或类似结构中按优先级对项目元素进行排序。

3. 代理链

在现实世界中,我们希望生物能够更加灵活地增加或者减少属性值,这仅仅依靠在末尾附加项目的链表不足以完成。此外,我们不想永久地修改生物底层的统计数据——相反只是想做些临时修改。

实现职责链的一种方式是使用某个集中式组件。该组件可以将游戏中所有可用的修改器保留在列表中,并且可以通过确保应用所有相关的修改器操作来方便查询特定生物的攻击值或防御值。

我们将要构建的组件称为事件代理(event broker)。因为它与每个参与的组件连接,所以它代表了中介者模式,而且由于它通过事件响应查询,因此它利用了本书后面讨论的观察者模式。

首先,我们定义一个名为Game的数据结构,使它代表一个正在进行的游戏:

struct Game
{
    signal<void(Query&)> queries_;
};

我们使用Boost.Signals2库保存名为quries的信号。这样就可以发射这个信号,并由对应的槽(监听组件)处理这个信号。假设我们想查询生物的统计数据。当然,我们可以尝试读取某个成员,但请记住,在知道最终值之前,我们需要应用所有修改器。因此,我们将把查询封装在单独的对象中(这是命令模式),其定义如下:

struct Query
{
    std::string creature_name_;
    enum Argument
    {
        attack,
        defense
    }argument_;
    int result_;
};

我们在这里完成的工作是将从某个生物中查询特定值的概念进行封装。要进行查询,我们需要提供该生物的名称,并指定感兴趣的统计数据。Game::quries将构造和使用这个值(对该值的一个引用),对其应用修改器,并返回最终的值。

现在,Creature2的定义中增加了game成员的引用:

class Creature2
{
    Game& game_;
    int attack_, defense_;
public:
    std::string name_;
    Creature2(Game& g, const std::string& n, int a, int d) : game_{g}, name_{n}, attack_{a}, defense_{d} {}

    int get_attack() const
    {
        Query q{name_, Query::Argument::attack, attack_};
        game_.queries_(q);
        return q.result_;
    }
    //other members here
};

此时,attackdefense都是私有成员,要获取最终的属性值,需要单独调用getter函数:我们所做的不是仅仅返回一个值或静态地应用一些基于指针的链,而是使用正确的参数创建一个Query对象,然后将该查询发送给订阅了Game::quries的组件来处理,每个订阅组件都有机会修改这个属性值。

现在我们来实现修改器。同样我们将创建一个基类,但这一次,它没有handle()方法:

class CreatureModifier2
{
    Game& game_;
    Creature2& creature_;
public:
    CreatureModifier2(Game& g, Creature2& c) : game_{g}, creature_{c} {}
};

然后继承出实际的修改器:

class DoubleAttackModifier2 : public CreatureModifier2
{
    connection conn_;
public:
    DoubleAttackModifier2(Game& g, Creature2& c) : CreatureModifier2(g,c)
    {
        conn_ = g.queries_.connect([&](Query& q)
        {
            if(q.creature_name_ == c.name_ &&
                q.argument_ == Query::Argument::attack)
            {
                q.result_ *= 2;
            }
        });
    }

    ~DoubleAttackModifier2()
    {
        conn_.disconnect();
    }
};

所有的神奇之处都发生在构造函数(和析构函数)中,不需要其他方法。在构造函数中,我们使用Game引用来捕获Game::queries信号并连接到它,在lambda函数中将攻击值翻倍。当然,在lambda函数中必须做一些检查:我们需要确保操作的是正确的生物,并且将要修改的是攻击值。这两条信息都保存在Query引用对象中,我们修改后的结果值也保存在这个引用对象中。
还需要注意保存信号连接,以便在对象销毁时断开连接。这样,我们就可以临时应用修改器,并在修改器对象退出作用域时使其失效。

综合上述内容,我们可以编写如下的应用代码:

void test_broker_chain()
{
    Game wzry;
    Creature2 houzi{wzry, "Strong houzi", 2, 2};
    std::cout << houzi << std::endl;
    DoubleAttackModifier2 dam0{wzry, houzi};
    std::cout << houzi << std::endl;
    {
        DoubleAttackModifier2 dam{wzry, houzi};
        std::cout << houzi << std::endl;
    }
    std::cout << houzi << std::endl;
}
4. 总结

职责链模式是一种非常简单的设计模式,允许组件依次处理命令(或查询)。职责链最简单的实现只需要维护一个指针链。理论上,如果想快速删除链上的对象,也可以用普通的vector或者list来代替指针链。

代理链的实现则更加复杂,它利用了中介者模式和观察者模式,允许通过事件(信号)处理查询,让每个订阅者在将查询值返回给用户之前对最初传递的值(它是贯穿整个链的单个引用)进行修改。

  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值