如何将此“命令处理程序映射”从派生类重构为基类?

Posted

技术标签:

【中文标题】如何将此“命令处理程序映射”从派生类重构为基类?【英文标题】:How to refactor this 'command handler map' from derived classes to the base class? 【发布时间】:2020-08-19 07:01:23 【问题描述】:

我试图通过将某些通用功能从派生类移动到基类来重构一些 C++ 代码。假设我有一个基类Fruit 和两个派生类AppleOrange。两个派生类目前都拥有一个私有映射,将命令映射到成员函数,例如对于Apple 类,这将是

typedef void (Apple::*CommandHandler)();
static const std::map<std::string, CommandHandler> commandhandlers;

由于commandhandlers 映射不会随时间变化,我将其设为静态,并且对于每个派生类,它都使用静态函数填充命令处理程序,例如对于Apple 类:

static std::map<std::string, Apple::CommandHandler> mpInitCommandHandlerMap()

    std::map<std::string, Apple::CommandHandler> commandhandlers;
    commandhandlers.insert(std::make_pair("eat", &Apple::eat));
    // ... and so on...
    return commandhandlers;

在哪里

void eat()  std::cout << "eating apple" << std::endl; 

Apple 类中eat 命令的(私有)命令处理程序示例。

AppleOrange 派生类也都有一个handle 函数来处理不同的命令:

void handle(const std::string& command)

    const auto handler = commandhandlers.at(command);
    (this->*handler)();

由于这个handle 函数对于两个派生类都是相同的,我想将它移到Fruit 类中。但是,这就是我卡住的地方。 commandhandlers 映射当前存在于AppleOrange 类中,并且具有不同类型的命令处理函数(typedef void (Apple::*CommandHandler)(); 用于Apple 类和typedef void (Orange::*CommandHandler)(); 用于Orange 类) .

所以我的问题是:我希望在Fruit 类中只有一个commandhandler 映射和一个handle 函数。我该怎么做(现在最好使用 C++14)?完整代码可在线获取https://godbolt.org/z/87zbGa

【问题讨论】:

std:map&lt;std:string, std::function&lt;void()&gt; &gt; commandhandlers_; 放在您的基类中,并在您的AppleOrange 构造函数中正常初始化它:commandhandlers_["eat"] = std::bind(&amp;Apple::eat, this); @pptaszni 这可能与拥有static 命令处理程序映射的要求相冲突。否则,Apple 可能会“看到”Orange 的命令。 "可能会看到Oragne的命令" - 我认为不可能。 Apple::Apple(params ... ) 将使用只有Apple 可能知道的命令填充地图,反之亦然。如果我错了,请给我一些例子。在派生类中有static 映射的要求与能够从基类调用该映射有点冲突。 还有两点说明: 1) 请注意,从长远来看,Fruit 类还会将命令处理程序添加到命令处理程序映射中。这些将是 Fruit 类中定义的命令处理函数(= Apple 和 Orange 类的相同行为)。 2)我不完全确定命令处理程序映射是否确实需要是静态的......无论如何,Apple 命令处理程序不应该与 Orange 命令处理程序混淆! 是的,考虑到您的言论,我仍然认为我建议的解决方案是您所需要的。您无需担心混淆Apple和Orange命令,以防在对象上调用fruit-&gt;handle("apple_specific_command"); Orange 类型的键 apple_specific_command 不会出现在 commandhandlers_ 映射中。 【参考方案1】:

您可以使用 CRTP 进行因式分解,例如:

template <typename Derived>
class Fruit

public:
    void handle(const std::string& command)
    
        const auto handler = commandhandlers.at(command);
        (static_cast<Derived*>(this)->*handler)();
    

protected:
    using CommandHandler = void (Derived::*)();
    static const std::map<std::string, CommandHandler> commandhandlers;
;

template <typename Derived>
const std::map<std::string, typename Fruit<Derived>::CommandHandler>
Fruit<Derived>::commandhandlers = Derived::mpInitCommandHandlerMap();

然后

class Apple : public Fruit<Apple>

friend class Fruit<Apple>;
private:
   
    static std::map<std::string, CommandHandler> mpInitCommandHandlerMap()
    
        return 
            "eat", &Apple::eat
            // ... and so on...
        ;
    

    void eat()  std::cout << "eating apple" << std::endl; 
;

Demo

我删除了 AppleOrange 之间的通用基类,如果需要,您可以重新引入。

【讨论】:

这是一个非常有趣的解决方案。【参考方案2】:

将命令处理程序映射和句柄函数移至 Fruit 基类

class Fruit

public:
    
    void handle(const std::string& command)
    
        const auto handler = commandhandlers_.at(command);
        handler();
    

protected:
    std::map<std::string, std::function<void()>> commandhandlers_;
;

然后在派生类中初始化命令处理程序映射,例如苹果为

    Apple()
    
        commandhandlers_ = mpInitCommandHandlerMap();
    

mpInitCommandHandlerMap 是非静态的似乎可以工作。另请参阅https://godbolt.org/z/jx1cs8的完整解决方案

【讨论】:

这里有个小缺点:commandhandlers_ 映射不会随时间改变,所以我想将其设为 const,但这是不可能的,因为它在 Apple 构造函数中获得了初始值。尝试在 Apple 的初始化器列表中初始化时,编译器当然会抱怨 Apple 没有字段名称 commandhandlers_ :-( 不,这里没有缺点。只需在构造函数初始化程序列表中初始化地图就可以了。 example 好的,确实可以将地图作为 Fruit 构造函数的参数。谢谢。

以上是关于如何将此“命令处理程序映射”从派生类重构为基类?的主要内容,如果未能解决你的问题,请参考以下文章

无法将参数 1 从派生指针转换为基类指针引用

C++使用一个基类派生出圆形和矩形,在矩形下派生出正方形并计算所有面积

C#通过反射将派生类转换为基类异常

将派生类转换为基类

将派生类转换为基类

Part7 继承与派生 7.3基类与派生类类型转换