4

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

[命令名称] [参数]

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

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

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

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

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

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

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

4

2 回答 2

7

您正在寻找的是 OOP 中的常见模式。 设计模式 (四人合一书)将此称为命令模式

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

在另一个答案中,Rafael Baptista 提出了基本设计。以下是我将如何修改他的设计以使其更完整:

命令对象和 CommandDispatcher

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

系统使用 注册一个实例CommandCommandDispatcher并将每个实例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.
}

这里ReadFileCommand和是实现所需行为WriteFileCommand的子类。Command

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

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

于 2012-06-28T19:24:34.297 回答
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 派生的任何类。

于 2012-06-28T18:38:03.063 回答