多态命令解析器设计

Polymorphic Command Parser Design

本文关键字:命令 多态      更新时间:2023-10-16

希望对我正在尝试解决的这个问题提出一些意见。 我正在努力改善我的OO体验,并充分利用C++的多态功能。 我正在尝试为基本命令解析器编写一些代码。 他们的命令结构是这样的:

[命令名称] [参数]

命令名称将仅限于一个单词字符串。参数可以是 0 到 N 的字符串列表。

每个命令和参数列表都可以定向到系统中任何类型的软件对象。 因此,例如,我可以将 rtp 统计信息命令映射到我的 rtp 模块,将用户统计信息映射到我的用户模块。 类似的东西。

现在,我的 CLI 的入口点将整个命令字符串作为标准字符串提供。 它提供了一个标准的输出流,用于向用户显示结果。

我真的很想避免使用解析器函数,然后做一个 if 然后是其他交易。 所以我在想这样的事情:

  1. 我会有一个名为命令的基类。 它的构造函数将采用字符串命令、stdout 和需要与之交互的对象的接口。
  2. 我将创建一个命令工厂,该工厂将命令名称与处理它的对象匹配。 这将实例化正确命令的正确命令对象。
  3. 每个单独的命令对象将分析给定的参数,并为此命令做出正确的选择。

我正在努力解决的是如何将正确的模块提供给正确的命令。 这是我应该使用模板参数的地方吗? 这样每个命令都可以采用任何接口,我会让工厂决定将哪个模块传递给命令对象?

我也对其他意见持开放态度。 我只是想学习,希望社区能给我一些提示:-)。

您要查找的是 OOP 中的常见模式。 设计模式(四人帮一书)将其称为命令模式。

通常不需要模板。 一切都在运行时解析和调度,因此动态多态性(虚函数)可能是更好的选择。

在另一个答案中,拉斐尔·巴普蒂斯塔提出了一个基本设计。 以下是我修改他的设计以使其更完整的方法:

命令

对象和命令调度程序

命令由 Command 类的子类处理。 命令由处理命令字符串基本解析的 CommandDispatcher 对象调度(基本上,在空格处拆分,可能处理带引号的字符串等)。

系统向CommandDispatcher注册一个Command实例,并将每个Command实例与命令名称(std::string)相关联。 关联由 std::map 对象处理,尽管可以用哈希表(或用于关联键值对的类似结构)替换。

class Command
{
  public:
    virtual ~Command(void);
    virtual void execute(FILE* in, const std::vector<std::string>& args) = 0;
};
class CommandDispatcher
{
  public:
    typedef std::map<std::string, Command*> CommandMap;
    void registerCommand(const std::string& commandName, Command* command)
    {
      CommandMap::const_iterator cmdPair = registeredCommands.find(commandName);
      if (cmdPair != registeredCommands.end())
      {
        // handle error: command already registered
      }
      else
      {
        registeredCommands[commandName] = command;
      }
    }
    // possibly include isRegistered, unregisterCommand, etc.
    void run(FILE* in, const std::string& unparsedCommandLine); // parse arguments, call command
    void dispatch(FILE* in, const std::vector<std::string>& args)
    {
      if (! args.empty())
      {
        CommandMap::const_iterator cmdPair = registeredCommands.find(args[0]);
        if (cmdPair == registeredCommands.end())
        {
          // handle error: command not found
        }
        else
        {
          Command* cmd = cmdPair->second;
          cmd->execute(in, args);
        }
      }
    }

  private:
    CommandMap registeredCommands;
};

我省略了解析和其他细节,但这是命令模式的一个非常常见的结构。 请注意std::map如何处理将命令名称与命令对象相关联。

注册命令

要使用此设计,您需要在系统中注册命令。 您需要实例化CommandDispatcher,无论是使用单例模式,还是在main中,还是在另一个中心位置。

然后,您需要注册命令对象。 有几种方法可以做到这一点。 我更喜欢的方式,因为你有更多的控制权,是让每个模块(一组相关命令)提供自己的注册功能。 例如,如果您有一个"文件 IO"模块,那么您可能有一个函数fileio_register_commands

void fileio_register_commands(CommandDispatcher* dispatcher)
{
  dispatcher->registerCommand( "readfile", new ReadFileCommand );
  dispatcher->registerCommand( "writefile", new WriteFileCommand );
  // etc.
}

在这里,ReadFileCommandWriteFileCommand是实现所需行为的Command的子类。

您必须确保在命令可用之前调用fileio_register_commands

此方法适用于动态加载的库(DLL 或共享库)。 确保注册命令的函数具有基于模块名称的常规模式:XXX_register_commands ,其中XXX是例如小写的模块名称。 加载共享库或 DLL 后,代码可以确定此类函数是否存在,然后调用它。

模板是矫枉过正的。我想你想要一些东西,命令解释器只是从可用的对象中找出哪些命令是可能的。

对于想要支持此 CLI 的每个类,我会给它一个注册该类的函数,以及触发该类的命令名称。

class CLIObject
{
   virtual void registerCli( Cli& cli ) = 0;
   virtual bool doCommand( FILE* file, char** args ) = 0;
}
class HelloWorld : public ClIObject 
{
   void registerCli( Cli& cli ) { cli.register( this, "helloworld" ); }
   bool doCommand( FILE* file, char** args ) 
   { 
       if ( !args[0] ) return false;     
       fprintf( file, "hello world! %s", args[0] ); 
       return true; 
   }
}

现在,您的 cli 可以支持从 CLIObject 派生的任何类。