当前位置:  开发笔记 > 编程语言 > 正文

寻找设计模式以减少虚拟方法过载

如何解决《寻找设计模式以减少虚拟方法过载》经验,为你挑选了1个好方法。

我有一个从公共基类(Device)派生的大量(~100)个类.每个设备都可以接受类似大量命令的某个子集.不同的命令可以具有不同的数量和类型的参数,因此每个命令都由其自己的类型封装(如果需要可以更改).

有哪些模式允许我将命令传递给设备,只给出对Device基类的指针/引用,以便设备可以访问命令的类型和参数?

我想出的选项:

    最直接的方法是添加一个单独的虚方法,接受每个命令类型到基类Device.但是,这将导致基类中的大量虚拟方法仅在极少数派生类中被覆盖.

    我考虑了访问者模式,但由于命令类的数量大约等于设备类的数量,所以这并没有真正获得任何东西.

    使用RTTI(或每个命令唯一的枚举/标识符)来确定命令类型,然后切换/ if分支到适当的代码.这感觉非常脏,因为它绕过了正常的C++多态性.此外,dynamic_cast在这里非常不满意,所以这个选项几乎完全没有了.

对于一个模式的任何建议,如果在Device基类中没有大量的虚方法,就可以干净利落地使用它?



1> Yakk - Adam ..:

您的系统由设备网络和分配给每个设备的命令列表组成.

您有代码反序列化命令并将它们分派给设备.我认为这是错误的顺序.

相反,Command应该被序列化(或者,以"通用形式"或"未解析的形式" - 参数的字符串向量,以及用于命令id的int).设备在"自身"中使用通用(模板可能)代码来反序列化Command并在其自身上调用它.

在调用时,设备知道自己的类型.模板反序列化代码可以被告知设备理解什么类型的命令,并且给定无效命令可以错误输出并静态失败.给定一个有效的命令,它可以以类型安全的方式在Device上调用它.

如果需要,可以在将命令传递给设备的位置对命令进行部分反序列化.

如果添加新命令或设备类型,则不需要重新编译任何现有设备.旧设备解析器应足够健壮,以检测和丢弃无效命令.新设备解析器将使用新的命令类型.

"execute serialized command"接口必须有一个返回值,指示命令是否是无效命令,因此您可以在Device接口之外处理它.这可能涉及错误代码,std::experimental::expected类型模式或异常.


这是一个实现的草图.

从序列化数据中有效地(在无DRY和运行时效率方面)编写"执行命令"代码有点棘手.

假设我们有一个名为" command_type" 的命令枚举.已知命令类型的数量是command_type::number_known- 所有命令类型都需要具有严格小于该值的值.

接下来,添加一个如下所示的函数:

template
using command_t = std::integral_constant;

template
error_code execute_command( command_t, T const&, std::vectorconst&){
  return error_code::command_device_mismatch;
}

这是默认行为.默认情况下,命令类型和设备类型不能一起使用.参数将被忽略,并返回错误.

我们还编写了一个帮助器类型,供以后使用.它是一种以ADL(依赖于参数的查找)友好方式templateclass调用execute_command的方法.此类模板应与此命名空间相同execute_command.

template
struct execute_command_t {
  template
  error_code operator()( T const& t, std::vectorconst& a){
    return execute_command(command_t(N)>{}, t, a);
  }
};

它们应该是非常普遍可见的.

然后,我们继续创建execute_command只对各种Device子类型的实现私有可见的重载.

假设我们有一个类型Bob,我们想要Bob了解该命令command_type::jump.

我们在Bob's文件中定义一个如下所示的函数:

error_code execute_command( command_t, Bob& bob, std::vector const& args );

这应该与Bob类型在同一名称空间中.

然后我们写一个魔术开关.魔术开关获取运行时值(在这种情况下为枚举值),并映射到一个函数表,该函数表实例化具有该运行时值的编译时模板数组.这是一个实现草图(它没有编译,我只是把它写在我的头顶,因此它可能包含错误):

namespace {

templateclass Target, size_t I, class...Args>
std::result_of_t(Args...)> helper( Args&&... args ) {
  using T=Target;
  return T{}(std::forward(args)...);
}

}

templateclass Target>
struct magic_switch {
private:
  template
  using R=std::result_of_t(Args...)>;

  template
  R execute(std::index_sequence, size_t I, R err, Args&&...args)const {
    using Res = R;
    if (I >=N) return err;
    using f = Res(Args&&...);
    using pf = f*;
    static const pf table[] = {
    //      [](Args&&...args)->Res{
    //          return Target{}(std::forward(args)...);
    //      }...,
      &helper...,
      0
    };
    return table[I](std::forward(args)...);
  }

public:
  template
  R operator()(size_t I, R err, Args&&...args)const {
    return execute( std::make_index_sequence{}, I, std::forward>(err), std::forward(args)... );
  }
};

magic_switch本身就是一个模板.它需要一个可以处理的最大值N以及templateclass Target它将创建和调用的值.

(带有lambda的注释掉的代码是合法的C++ 11,但是gcc5.2和clang 3.7都不能编译它,所以请使用该helper版本.)

operator()接受一个index(size_t I),一个越界的错误err,以及一组完美前进的参数.

operator()创建一个index_sequence<0, 1, ..., N-1>并将其传递给私有execute方法. execute使用它index_sequence的整数来创建一个函数指针数组,每个函数指针实例化Target并传递它Args&&...args.

我们检查然后使用运行时参数对该列表进行数组查找I,然后运行调用的函数指针Target{}(args...).

上面的代码是通用的,不是特定于这个问题.我们现在需要一些胶水来解决这个问题.

此函数采用magic_switch上述方法,并将其与我们调度的ADL合并execute_command:

template
error_code execute_magic( command_type c, T&& t, std::vector const& args) {
  using magic = magic_switch< static_cast(command_type::number_known), execute_command_t >;
  return magic{}( size_t(c), error_code::command_device_mismatch, std::forward(t), args );
}

我们的execute_command_t模板传递给magic_switch.

最后,我们有一个函数指针的运行时"跳转表",指向execute_command( command_t, bob, args )为每个command_type枚举值执行a的代码.我们使用运行时command_type并使用它进行数组查找,并调用相应的execute_command.

如果没有execute_command( command_t, bob, args )专门编写过载,则调用默认值(本示例开头的方向),并返回命令设备不匹配错误.如果已经编写了一个,则可以通过Argument Dependent Lookup的魔力找到它,并且它比失败的泛型重载更专业,因此它被调用.

如果我们喂的command_type是超出范围的,我们也会处理.因此,在command_type创建new时不需要重新编译每个设备(它是可选的):它们仍然可以工作.

这一切都很有趣,但我们如何execute_magic使用真实的设备子类型进行调用?

我们添加一个纯虚方法Device:

virtual error_code RunCommand(command_type, std::vector const& args) = 0;

我们可以RunCommand在每个派生类型中自定义实现Device,但是这违反了DRY并且可能导致错误.

相反,编写一个CRTP(奇怪的重复模板模式)indermediarey helper,称为DeviceImpl:

template
struct DeviceImpl {
  virtual error_code RunCommand(command_type t, std::vectorconst& args) final override
  {
    auto* self = static_cast(this);
    return execute_magic( t, *self, args );
  }
};

现在,当我们定义命令时,Bob我们会:

class Bob : public DeviceImpl

而不是Device直接继承.这Derived::RunCommand为我们自动实现,并避免干燥问题.

execute_command需要Bob&重载的声明必须DeviceImpl在实例化之前可见,否则上述操作无效.

最后一点是实施execute_command.在这里,我们必须正确地std::vector const&调用它Bob.关于这个问题有很多堆栈溢出问题.

在上面,使用了一些C++ 14特性.它们可以很容易地用C++ 11编写.


使用的关键技术:

Magic Switch(我将运行时跳转表的技术称为编译时模板实例)

参数依赖查找(如何execute_command找到)

类型擦除或运行时概念(我们将命令的执行类型删除到虚拟RunCommand接口中)

Tag Dispatching(我们command_t作为标签类型传递给调度到正确的重载execute_command).

CRTP(Curiously Recurring Template Pattern),我们用它DeviceImpl来实现RunCommand虚拟方法一次,在我们知道Derived类型的上下文中,这样我们就可以正确地调度.

实例.

推荐阅读
携手相约幸福
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有