多态命令解析器设计

时间:2012-06-28 18:24:30

标签: c++

我会喜欢这个问题的一些意见,我正在努力锻炼。我正在努力提高我的OO体验并充分利用C ++的多态功能。我正在尝试为基本命令解析器编写一些代码。他们的命令结构如此:

[命令名称] [参数]

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

每个命令和参数列表都可以指向我系统中的各种软件对象。因此,例如,我可以将rtp statistics命令映射到我的rtp模块,用户统计信息到我的用户模块。这样的事情。

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

我真的想避免使用解析器函数然后执行if if then else交易。所以我在想这样的事情:

  1. 我会有一个名为command的基类。它的构造函数将采用string命令,stdout和它需要与之交互的对象的接口。
  2. 我会创建一个命令工厂,它将命令名称与处理它的对象相匹配。这将为正确的命令实例化正确的命令对象。
  3. 每个单独的命令对象将解析给定的参数,并为此命令做出正确的选择。
  4. 我正在努力的是如何为正确的命令提供正确的模块。这是我应该使用模板参数的地方吗?这样每个命令都可以接受任何接口,我会让工厂决定将哪个模块传入命令对象?

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

2 个答案:

答案 0 :(得分:7)

您正在寻找的是OOP中的常见模式。 Design Patterns(“四人帮”一书)将此称为Command Pattern

通常不需要模板。所有内容都在运行时进行解析和分派,因此动态多态(虚函数)可能是更好的选择。

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

命令对象和CommandDispatcher

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

系统使用Command注册CommandDispatcher的实例,并将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或其他中心位置使用Singleton模式实例化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后,您的代码可以确定是否存在这样的函数,然后调用它。

答案 1 :(得分:2)

模板太过分了。我想你需要一些命令解释器只能从可用的对象中找出可能的命令。

对于每个想要支持这个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的类。