【问题标题】:Command Pattern : How to pass parameters to a command?命令模式:如何将参数传递给命令?
【发布时间】:2010-09-11 10:04:50
【问题描述】:

我的问题与命令模式有关,我们有以下抽象(C#代码):

public interface ICommand
{
    void Execute();
}

让我们使用一个简单的具体命令,该命令旨在从我们的应用程序中删除一个实体。例如,Person 实例。

我会有一个DeletePersonCommand,它实现了ICommand。该命令需要将Person作为参数删除,以便在调用Execute方法时将其删除。

管理参数化命令的最佳方式是什么?如何在执行命令之前将参数传递给命令?

【问题讨论】:

  • 我知道这个问题可以追溯到四年前,但 Juanma 和 bloparod 实际上给出了正确答案:使ICommand 通用(ICommand<TArgs>)。给定的TArgs 封装了所有参数(它变成了Parameter Object)。您必须为每个命令创建两个对象:一个用于消息;另一个用于消息。一为行为。起初这听起来很尴尬,但当你得到它时,你将永远不会回头。 This article 详细描述了这个模型。阅读此问题的每个人都必须阅读。
  • @Steven 感谢您提供指向您博客文章的链接。如果您能澄清您在其中描述的方法如何与此处的问题相匹配,那么您可能会很好,因为您自己承认,您“不考虑 [it] 命令模式”。人们可能会认为您的评论只是自我推销。

标签: c# design-patterns command-pattern


【解决方案1】:

您需要通过构造函数或 setter 注入(或等效方法)将参数与命令对象相关联。也许是这样的:

public class DeletePersonCommand: ICommand
{
     private Person personToDelete;
     public DeletePersonCommand(Person personToDelete)
     {
         this.personToDelete = personToDelete;
     }

     public void Execute()
     {
        doSomethingWith(personToDelete);
     }
}

【讨论】:

  • 正是我会做的。对于构造命令时不知道的任何内容,我会将接口传递给在执行命令时获取对象的服务。这可能是委托或 lambda expession 或其他对象。
  • 这是一个糟糕的解决方案,因为容器与 Person 紧密耦合,而应该使用某种包含依赖项的 Parameter 对象来打破耦合。告诉不要问是这里的主要规则。
  • @Blair Conrad 我想知道如果我们改变接收器方法会怎样。按照开闭原则,改变Command的执行方法就可以了?
【解决方案2】:

通过构造函数或设置器传递数据是可行的,但需要命令的创建者知道命令需要的数据...

“上下文”的想法非常好,我正在研究(一个内部)框架,不久前就利用了它。

如果您将控制器(与用户交互的 UI 组件、解释用户命令的 CLI、解释传入参数和会话数据的 servlet 等)设置为提供对可用数据的命名访问,则命令可以直接请求它们的数据想要。

我真的很喜欢这样的设置所允许的分离。考虑分层如下:

User Interface (GUI controls, CLI, etc)
    |
[syncs with/gets data]
    V
Controller / Presentation Model
    |                    ^
[executes]               |
    V                    |
Commands --------> [gets data by name]
    |
[updates]
    V
Domain Model

如果您“正确”地执行此操作,则可以将相同的命令和演示模型用于任何类型的用户界面。

更进一步,上面的“控制器”非常通用。 UI 控件只需要知道它们将调用的命令的名称——它们(或控制器)不需要知道如何创建该命令或该命令的数据需要。这才是真正的优势。

例如,您可以保存要在 Map 中执行的命令的名称。每当组件被“触发”(通常是 actionPerformed)时,控制器都会查找命令名称、实例化它、调用执行并将其推送到撤消堆栈(如果您使用的话)。

【讨论】:

    【解决方案3】:

    有一些选择:

    您可以通过属性或构造函数传递参数。

    其他选项可能是:

    interface ICommand<T>
    {
        void Execute(T args);
    }
    

    并将所有命令参数封装在一个值对象中。

    【讨论】:

    • 上面代码的问题是不同的命令(例如 CreateSomeThingCommand 和 DeleteSomethingCommand)可能需要不同的参数并且不能再以相同的方式执行(考虑 IEnumerable .Execute() 调用)。命令模式旨在用于将定义与执行分开......如果您在执行时传递参数,您将在执行时而不是定义时更改/控制命令的行为。
    • 顺便说一句:我认为您的意思是 void Execute(T args) 而不是 Execute(T args>,因为 T 已经在 ICommand 中定义,第二个在 function/方法级别没用。您也可以创建类似以下内容: interface ICommand { void Execute(T1 t1, T2 t2); } (这更有意义)或 interface ICommand { void Execute (T2 t2); // 在其他任何地方使用 T1 }
    【解决方案4】:

    我的实现是这样的(使用 Juanma 提出的 ICommand):

    public class DeletePersonCommand: ICommand<Person>
    {
        public DeletePersonCommand(IPersonService personService)
        {  
            this.personService = personService;
        }
    
        public void Execute(Person person)
        {
            this.personService.DeletePerson(person);
        }
    }
    

    IPersonService 可以是 IPersonRepository,这取决于你的命令是什么“层”。

    【讨论】:

    • 似乎是策略模式的更好用例,而不是您示例中的命令模式。
    【解决方案5】:

    在创建命令对象时传递人:

    ICommand command = new DeletePersonCommand(person);
    

    这样当你执行命令时,它就已经知道它需要知道的一切。

    class DeletePersonCommand : ICommand
    {
       private Person person;
       public DeletePersonCommand(Person person)
       {
          this.person = person;
       }
    
       public void Execute()
       {
          RealDelete(person);
       }
    }
    

    【讨论】:

      【解决方案6】:

      在这种情况下,我们对 Command 对象所做的是创建一个 Context 对象,它本质上是一个地图。该映射包含名称值对,其中键是常量,值是命令实现使用的参数。如果您有一个命令链,其中后面的命令依赖于早期命令的上下文更改,则特别有用。

      所以实际的方法变成了

      void execute(Context ctx);
      

      【讨论】:

      • 我在我的设计中使用了这个,但 'Context' 是一个字典
      【解决方案7】:

      在构造函数中并存储为字段。

      您还希望最终使您的 ICommands 可序列化以用于撤消堆栈或文件持久性。

      【讨论】:

        【解决方案8】:

        基于 C#/WPF 中的模式,ICommand 接口 (System.Windows.Input.ICommand) 被定义为将对象作为 Execute 以及 CanExecute 方法的参数。

        interface ICommand
                    {
                        bool CanExecute(object parameter);
                        void Execute(object parameter);
                    }
        

        这允许您将您的命令定义为一个静态公共字段,它是您实现 ICommand 的自定义命令对象的一个​​实例。

        public static ICommand DeleteCommand = new DeleteCommandInstance();
        

        以这种方式,当调用执行时,相关对象(在您的情况下为人)被传入。然后 Execute 方法可以强制转换对象并调用 Delete() 方法。

        public void Execute(object parameter)
                    {
                        person target = (person)parameter;
                        target.Delete();
                    } 
        

        【讨论】:

        • 以这种方式实现“模式”的方式只不过是一个带有验证的“特殊”委托(CanExecute)。我认为,这失去了该模式的真正功能……定义和执行命令的解耦。传入参数将/可能改变执行方式。这样命令的定义取自命令的构造函数到参数创建的时间。 (我知道 M$ 将其用于 GUI 目的,但我认为这不应该是实现命令模式的常用方法。)
        【解决方案9】:

        已经提到 Blair Conrad 的代码(不知道如何标记他)工作得非常好如果你知道在实例化类时要删除什么人并且他的方法就足够了。但是,如果您在按下按钮之前不知道要删除谁,您可以使用返回该人的方法引用来实例化命令。

           class DeletePersonCommand implements ICommand
        {
             private Supplier<Person> personSupplier;
        
             public DeletePersonCommand(Supplier<Person> personSupplier)
             {
                 this.personSupplier = personSupplier;
             }
        
             public void Execute()
             {
                personSupplier.get().delete();
             }
        }
        

        这样,当命令执行时,供应商会在执行时获取您要删除的人。直到那时,该命令还没有关于删除谁的信息。

        对供应商有用的link

        注意:用 java 编写的代码。有 c# 知识的人可以调整它。

        【讨论】:

          【解决方案10】:

          您应该创建一个 CommandArgs 对象来包含您要使用的参数。使用 Command 对象的构造函数注入 CommandArgs 对象。

          【讨论】:

          • 为什么不自己注入所需的参数或通过 Func 委托注入值?
          【解决方案11】:

          DeletePersonCommand 在其构造函数或方法中可以有参数。 DeletePersonCommand 将具有 Execute(),并且在执行中可以检查将由 Getter/Setter 传递的属性,之前调用 Execute()。

          【讨论】:

            【解决方案12】:

            我会向DeletePersonCommand 的构造函数添加任何必要的参数。然后,当Execute()被调用时,会使用那些在构造时传入对象的参数。

            【讨论】:

              【解决方案13】:

              让“Person”实现某种 IDeletable 接口,然后让命令采用您的实体使用的任何基类或接口。这样,您可以创建一个 DeleteCommand,它会尝试将实体强制转换为 IDeletable,如果可行,请调用 .Delete

              public class DeleteCommand : ICommand
              {
                 public void Execute(Entity entity)
                 {
                    IDeletable del = entity as IDeletable;
                    if (del != null) del.Delete();
                 }
              }
              

              【讨论】:

              • 我认为这行不通 - ICommand 的全部意义在于每个子类完全覆盖 Execute()。此解决方案要求 Execute() 的调用者了解有关被调用命令类型的更多详细信息。
              • 我同意马特的观点。无论如何,那个 DeleteCommand 类甚至都不会编译,因为它没有按照 ICommand 接口的要求实现 void Execute()
              • 使用依赖注入,你仍然需要知道关于命令类型的一切,因为你必须更新它!如果您只处理“实体”,至少这样您的代码可以是通用的。原始响应包含有关更改 ICommand 以包含基类/接口的信息。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-05-20
              • 1970-01-01
              • 1970-01-01
              • 2013-10-11
              • 2012-09-01
              • 2020-08-02
              相关资源
              最近更新 更多