【转载】C++11 改进我们的模式之改进命令模式

106 阅读5分钟

参考原文地址:(原创) C++11 改进我们的模式之改进命令模式

我对文章的格式和错别字进行了调整,并补充并标注出了重要的部分。以下是正文。

正文

模式虽然精妙,却难完美,比如观察者模式中观察者生命周期的问题;比如访问者模式中循环依赖的问题等等;其它很多模式也存在这样那样的一些不足之处,如使用场景受限、实现复杂、不够简洁、不够通用等。但我觉得不足之处大都是可以采取一些手法去弥补去改进的,比如用 C++11 的新特性来改进。因此,便有了 C++11 改进我们的模式这个系列。这次我要讲的是如何使用 C++11 改进命令模式。

关于命令模式

命令模式的作用是将请求封装为一个对象,将请求的发起者和执行者解耦,支持对请求排队以及撤销和重做。它的类图如下:

image.png

由于将请求都封装成一个个命令对象了,使得我们可以集中处理或者延迟处理这些命令请求,而且不同的客户对象可以共享这些命令,还可以控制请求的优先级、排队、支持请求命令撤销和重做等等。命令模式的这些好处是显而易见的,但是,在实际使用过程中它的问题也暴露出来了。随着请求的增多,请求的封装类 —— 命令类也会越来越多,尤其是 GUI 应用中,请求是非常多的。越来越多的命令类会导致类爆炸,难以管理。关于类爆炸这个问题, GOF 很早就意识到了,他们提出了一个解决方法:对于简单的不能取消和不需要参数的命令,可以用一个命令类模板来参数化(特化)该命令的接收者,用接收者类型来参数化(特化)命令类,并维护一个接收者对象和一个动作之间的绑定,而这一动作是用指向同一个接收者类的成员函数的指针存储的。具体代码是这样的:

简单命令类的定义:

image.png

构造器存储接收者和(对应接收者实例变量中的)动作。Execute 操作实施接收者的这个动作。

image.png

为创建一个调用 MyClass 类的一个(接收者)实例上的 Action 动作的 Command 对象,仅需要如下代码:

image.png

通过一个泛型的简单命令类来避免不断创建新的命令类,是一个不错的办法,但是,这个办法不完美,即它只能是简单的命令类,不能对复杂的,甚至所有的命令类泛化,这是它的缺陷,所以,它只是部分的解决了问题。我想我可以改进这个办法缺陷,完美的解决类爆炸的问题。在 C++11 之前我不知道有没有人解决过这个问题,至少我没看到过。现在可以通过 C++11 来完美的解决这个问题了。

C++11 改进命令模式

要完美的解决命令模式类爆炸问题的关键是如何定义个通用的泛化的命令类,这个命令类可以泛化所有的命令,而不是 GOF 提到的简单命令。我们再回过头来看看 GOF 中那个简单的命令类的定义,它只是泛化了没有参数和返回值的命令类,命令类内部引用了一个接收者和接收者的函数指针,如果接收者的行为函数指针有参数就不能通用了,所以我们要解决的关键问题是如何让命令类能接受所有的成员函数指针或者函数对象。

有没有一个能接受所有成员函数、普通函数和函数对象的类呢?有,在 C++11 中可以有,我上一篇博文中提到了一个万能的函数包装器,它可以接受所有的函数对象、 fucntion 和 lambda 表达式,它行不行呢?不行,因为它还不够完美,它还不能接受成员函数呢,所以它还不是真正的万能的函数包装器。我打算在它的基础上再扩展一下,让它为一个真正的万能的函数包装器。

接受 function 、函数对象、lambda 和普通函数的包装器:

template <class F, class... Args, class = typename std::enable_if<!std::is_member_function_pointer<F>::value>::type>
void Wrap(F &&f, Args &&...args)
{
    return f(std::forward<Args>(args)...);
}

接受成员函数的包装器:

template <class R, class C, class... DArgs, class P, class... Args>
void Wrap(R (C::*f)(DArgs...), P &&p, Args &&...args)
{
    return (*p.*f)(std::forward<Args>(args)...);
}

通过重载的 Wrap 让它能接收成员函数。这样一个真正意义上的万能的函数包装器就完成了。现在再来看,它是如何应用到命令模式中,完美的解决类爆炸的问题。

一个通用的泛化的命令类

#include <functional>
#include <type_traits>
template <typename R = void>
struct CommCommand
{
private:
    std::function<R()> m_f;

public:
    template <class F, class... Args, class = typename std::enable_if<!std::is_member_function_pointer<F>::value>::type>
    void Wrap(F &&f, Args &&...args)
    {
        m_f = [&]
        { return f(args...); };
    }

    template <class C, class... DArgs, class P, class... Args>
    void Wrap(R (C::*f)(DArgs...) const, P &&p, Args &&...args)
    {
        m_f = [&, f]
        { return (*p.*f)(args...); };
    }

    // non-const member function
    template <class C, class... DArgs, class P, class... Args>
    void Wrap(R (C::*f)(DArgs...), P &&p, Args &&...args)
    {
        m_f = [&, f]
        { return (*p.*f)(args...); };
    }

    R Excecute()
    {
        return m_f();
    }
};

测试代码:

struct STA
{
    int m_a;
    int operator()() { return m_a; }
    int operator()(int n) { return m_a + n; }
    int triple0() { return m_a * 3; }
    int triple(int a) { return m_a * 3 + a; }
    int triple1() const { return m_a * 3; }
    const int triple2(int a) const { return m_a * 3 + a; }

    void triple3() { cout << "" << endl; }
};

int add_one(int n)
{
    return n + 1;
}

void TestWrap()
{

    CommCommand<int> cmd;
    // free function
    cmd.Wrap(add_one, 0);

    // lambda function
    cmd.Wrap([](int n)
             { return n + 1; },
             1);

    // functor
    cmd.Wrap(bloop);
    cmd.Wrap(bloop, 4);
    STA t = {10};
    int x = 3;
    // member function
    cmd.Wrap(&STA::triple0, &t);
    cmd.Wrap(&STA::triple, &t, x);
    cmd.Wrap(&STA::triple, &t, 3);

    cmd.Wrap(&STA::triple2, &t, 3);
    auto r = cmd.Excecute();

    CommCommand<> cmd1;
    cmd1.Wrap(&Bloop::triple3, &t);
    cmd1.Excecute();
}

我们在通用的命令类内部定义了一个万能的函数包装器,使得我们可以封装所有的命令,增加新的请求都不需要重新定义命令了,完美的解决了命令类爆炸的问题。