【问题标题】:Why should I use the command design pattern while I can easily call required methods? [closed]为什么我应该使用命令设计模式,而我可以轻松调用所需的方法? [关闭]
【发布时间】:2015-12-12 09:34:21
【问题描述】:

我正在研究command design pattern,我对它的使用方式感到很困惑。我的示例与用于打开和关闭灯的远程控制类有关。

为什么我不应该使用 Light 类的 switchOn() / switchOff() 方法,而不是拥有最终调用 switchOn / switchOff 方法的单独类和方法?

我知道我的例子很简单,但这就是重点。我在互联网上的任何地方都找不到任何复杂的问题来查看命令设计模式的确切用法。

如果您知道您解决的任何复杂的现实世界问题可以使用此设计模式解决,请与我分享。它可以帮助我和这篇文章的未来读者更好地理解这种设计模式的用法。谢谢

//Command
public interface Command {
  public void execute();
}

//Concrete Command
public class LightOnCommand implements Command {

  //Reference to the light
  Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.switchOn();        //Explicit call of selected class's method
  }
}

//Concrete Command
public class LightOffCommand implements Command {

  //Reference to the light
  Light light;

  public LightOffCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.switchOff();
  }
}

//Receiver
public class Light {
  private boolean on;

  public void switchOn() {
    on = true;
  }

  public void switchOff() {
    on = false;
  }
}

//Invoker
public class RemoteControl {
  private Command command;

  public void setCommand(Command command) {
    this.command = command;
  }

  public void pressButton() {
    command.execute();
  }
}

//Client
public class Client {
  public static void main(String[] args) {
    RemoteControl control = new RemoteControl();
    Light light = new Light();
    Command lightsOn = new LightsOnCommand(light);
    Command lightsOff = new LightsOffCommand(light);

    //Switch on
    control.setCommand(lightsOn);
    control.pressButton();

    //Switch off
    control.setCommand(lightsOff);
    control.pressButton();
  }
}

为什么我不应该轻松地使用如下代码?

 Light light = new Light();
 switch(light.command) {
  case 1:
    light.switchOn();
    break;
  case 2:
    light.switchOff();
    break;
 }

【问题讨论】:

  • 使用Command 界面可以更轻松地将命令插入新按钮、菜单、快捷方式等。
  • @mastov 请给我一个例子。最后,我需要显式调用所选类的方法,有什么区别?
  • 我发现像这样的设计类问题很难用一个小例子来说明;正是当您拥有一个复杂的应用程序时,它们的实用性才开始超过其复杂性,并且需要经验来建立对它的直觉。我建议您将模式留在脑海中,但现在选择更简单的方法。如果您需要更复杂的模式,请重构。另见:YAGNI。
  • 有关命令模式使用的真实示例,请参阅Activiti。 Activiti 是一个非常强大的工作流引擎。它很大程度上基于命令模式。由于它是开源的,您可以下载代码并一目了然。
  • 基本上,你可以用命令对象做更多的事情。例如,您可以将它们存储在列表中;您不能将方法调用存储在列表中。如果你不打算做这些事情,那么你是对的,在这里使用对象是愚蠢的。

标签: java oop design-patterns command-pattern


【解决方案1】:

您不必这样做。设计模式只是指南,过去一些人在编写非常复杂的应用程序时发现它们很有用。

在您的情况下,如果您要做的只是打开和关闭电灯开关,而不是其他任何事情,那么第二个选项是不费吹灰之力的。

更少的代码几乎总是比更多的代码更好。

【讨论】:

  • 是的,但使用设计模式的主要原因是拥有可扩展的代码。我想知道当我想扩展我的代码时会遇到什么样的问题。
  • 我认为您是在向后看:您正在尝试使设计模式适合问题,而应该反过来。正如其他人所指出的,在示例的上下文中很难说明一个用例。
  • 当然,对于这个简单的问题,您是正确的。但问题不同:设计模式的意义何在?总的来说一点,否则人们不会使用它。
  • 这只是一个例子,请阅读问题的粗体部分。
  • @mastov 呃,不,问题是“为什么我不应该使用 Light 类的 switchOn() / switchOff() 方法,而不是拥有最终调用 switchOn / switchOff 方法的单独类和方法”
【解决方案2】:

使用命令模式的主要动机是命令的执行者根本不需要知道命令是什么、它需要什么上下文信息或它做什么。所有这些都封装在命令中。

这允许你做一些事情,例如有一个按顺序执行的命令列表,这些命令依赖于其他项目,分配给某个触发事件等。

在您的示例中,您可以有其他类(例如Air Conditioner),它们有自己的命令(例如Turn Thermostat UpTurn Thermostat Down)。这些命令中的任何一个都可以分配给按钮或在满足某些条件时触发,而无需任何命令知识。

因此,总而言之,该模式封装了采取行动所需的一切,并允许行动的执行完全独立于任何上下文而发生。如果这不是您的要求,那么该模式可能对您的问题空间没有帮助。

这是一个简单的用例:

interface Command {
    void execute();
}

class Light {
    public Command turnOn();
    public Command turnOff();
}

class AirConditioner {
    public Command setThermostat(Temperature temperature);
}

class Button {
    public Button(String text, Command onPush);
}

class Scheduler {
    public void addScheduledCommand(Time timeToExecute, Command command);
}

然后您可以执行以下操作:

new Button("Turn on light", light.turnOn());
scheduler.addScheduledCommand(new Time("15:12:07"), airCon.setThermostat(27));
scheduler.addScheduledCommand(new Time("15:13:02"), light.turnOff());

如您所见,ButtonScheduler 根本不需要知道任何有关命令的信息。 Scheduler 是一个可能包含命令集合的类的示例。

还要注意,在 Java 8 中,函数式接口和方法引用使这种类型的代码更加整洁:

@FunctionalInterface
interface Command {
    void execute();
}

public Light {
    public void turnOn();
}

new Button("Turn On Light", light::turnOn);   

现在变成命令的方法甚至不需要知道命令——只要它们有正确的签名,你就可以通过引用方法悄悄地创建一个匿名命令对象。

【讨论】:

  • 我正在研究它,还没有任何具体的问题空间。我想了解有关 Command DP 及其用法的更多信息。
  • 要添加空调,我可以做类似于我对照明所做的事情。为什么我应该有单独的命令类来为我做这件事?我有点困惑。
  • @DanielNewtown 如果您将命令分配给按钮(例如),则按钮必须知道该命令是用于灯还是空调。命令模式确保它不需要知道任何关于命令的信息来执行它。
  • @sprinter:这正是 OP 的示例将命令分配给按钮的原因。为了说明按钮点击的正确功能只需要能够调用没有参数的方法。 (即可以访问 Command 对象 - 不再具体 - 并调用其 execute 方法)
【解决方案3】:

可能性很多,但通常是这样的:

  • 构建一个命令行框架,从操作中抽象出选项解析。然后,您可以使用 opts.register("--on", new LightOnCommand()) 之类的内容注册操作。
  • 让用户拖放一系列动作以作为宏执行
  • 在触发某些事件时注册回调,例如on(Event.ENTER_ROOM, new LightOnCommand())

这里的一般模式是,您有一段代码负责在不知道该操作是什么的情况下确定需要采取一些操作,而另一段代码知道如何做一个动作,而不是什么时候做。

例如,在第一个示例中,opts 实例知道当它看到命令行选项 --on 时,它应该打开灯。但它知道这一点,但实际上并不知道“打开灯”是什么意思。事实上,opts 实例很可能来自第三方库,因此它无法了解灯光。它所知道的只是如何将操作(命令)与它解析的命令行选项相关联。

【讨论】:

  • 很好,你能详细说明你的第一个例子吗?
  • @DanielNewtown 因为调用回调的人并不(静态地)知道要调用哪个函数。他只是引用了一些Command 对象并调用它的execute 方法而不知道它做了什么。这样,其他人就可以决定在那种情况下应该发生什么,只需注册Command 的特定子类的对象即可。
  • 这也可能有用,但它是正交的。要记住的是,命令行解析器、宏框架等可能甚至不知道任何灯泡类型的东西甚至存在。 所有它所知道的是可以执行命令。它甚至可能是与了解灯光的事物不同的代码库。
  • 对。你基本上有三段代码:一段定义命令和事件;另一个知道灯光之类的。这两块互不了解,可能在不同的项目中。然后第三部分,即“主要”组件,知道这两部分并将它们缝合在一起。
  • 此模式有用的另一种情况是实现多级撤消。如果Command 接口具有undo() 方法和execute() 方法,并且具体类以完全反转execute() 效果的方式实现undo(),那么您可以将命令推送到执行时的堆栈,然后 pop 和 undo() 撤消任意次数。
【解决方案4】:

您提供的示例ICommand 相当有限,仅在没有 lambda 表达式的编程语言中真正有用。 Sprinter 在他的回答中很好地涵盖了这一点,展示了命令工厂的使用。

命令模式的大多数情况包括其他方法,例如CanRun 和/或Undo。这些允许按钮根据命令状态更新其启用状态,或应用程序实现撤消堆栈。

像大多数设计模式一样,命令模式在事情变得有点复杂的情况下独立出来。它也是众所周知的,因此有助于让大多数程序员清楚地了解您的代码。

【讨论】:

    【解决方案5】:

    让我们关注命令设计的非实现方面,以及使用分为两大类的命令设计模式的一些主要原因:

    • 隐藏命令执行方式的实际实现
    • 允许围绕命令构建方法,也就是命令扩展

    隐藏实现

    在大多数编程中,您会希望隐藏实现,以便在查看最顶层问题时,它包含可理解的命令/代码子集。 IE。您不需要/不想知道如何打开灯或启动汽车的血腥细节。如果你的重点是让汽车启动,你不需要了解发动机是如何工作的,以及它如何需要燃料进入发动机,阀门是如何工作的,......

    指示动作,而不是如何完成

    一个命令给你这种视图。您将立即了解TurnLightOn 命令或StartCar 的作用。 使用命令,您将隐藏如何完成某事的详细信息,同时清楚地指示要执行的操作。

    允许更改内部细节

    此外,假设您稍后重建整个 Light 类或 Car 类,这需要您实例化几个不同的对象,并且在实际执行所需操作之前可能需要其他东西。在这种情况下,如果您已经实现了直接访问方法,那么在很多地方,您将需要在您事先编写过代码的所有地方对其进行更改。 使用命令,你可以在不改变命令调用的情况下改变如何做事的内部细节。

    可能的命令扩展

    使用命令接口在使用命令的代码和执行命令的实际操作的代码之间提供了一个额外的层。这可以允许多种好的场景。

    安全扩展,或者接口暴露

    使用命令界面,您还可以限制对对象的访问,允许您定义另一个安全级别。拥有一个具有相当开放访问权限的模块/库可能是有意义的,这样您就可以轻松处理内部特殊情况。

    但是,从外部来看,您可能希望限制对灯的访问,使其只能打开或关闭。 使用命令可以将接口限制为一个类

    此外,如果您愿意,您可以围绕命令构建一个专用的用户访问系统。这将使您的所有业务逻辑保持开放、可访问且不受限制,但您仍然可以轻松地在命令级别限制访问以强制执行适当的访问。

    执行东西的通用接口

    在构建足够大的系统时,命令提供了一种在不同模块/库之间架桥的巧妙方法。您无需检查任何给定类的每个实现细节,您可以查看哪些命令正在访问该类。

    由于您将执行细节留给命令本身,您可以使用通用方法来实例化命令、执行它并查看结果。这样可以更轻松地进行编码,而无需阅读如何实例化特定的 LightCar 类并确定其结果。

    命令排序

    使用命令,您还可以执行命令排序等操作。那是因为您是否在执行TurnOnLightStartCar 命令并不重要,您可以执行这些序列,因为它们以相同的方式执行。这反过来可以允许执行命令链,这在多种情况下都很有用。

    您可以构建宏,执行您认为组合在一起的命令集。 IE。命令序列:UnlockDoorEnterHouseTurnOnLight。命令的自然序列,但由于它使用不同的对象和动作,因此不太可能被制成方法。

    命令序列化

    命令本质上相当小,也可以很好地进行序列化。这在服务器-客户端上下文或程序-微服务上下文中很有用。

    服务器(或程序)可以触发命令,然后将命令序列化,通过某种通信协议(即事件队列、消息队列、http……)将其发送给实际处理命令的人。无需先在服务器上实例化对象,即 Light 可能是轻量级的(双关语),或者 Car 可能是一个非常大的结构。您只需要命令,可能还需要一些参数。

    这可能是向CQRS - Command Query Responsibility Separation pattern 介绍进一步研究的好地方。

    命令跟踪

    在您的系统中使用额外的命令层还可以允许记录或跟踪命令,如果这是业务需要的话。您可以在命令模块中收集跟踪/日志记录,而不是到处执行此操作。

    这允许轻松更改日志系统,或添加时间等内容,或启用/禁用日志记录。而且这一切都可以在命令模块中轻松维护。

    命令跟踪还允许撤消操作,因为您可以选择从给定状态重新迭代命令。需要一些额外的设置,但很容易做到。


    简而言之,命令非常有用,因为它们允许连接即将成为大型程序的不同部分,因为它们是轻量级的,易于记忆/记录并在需要时隐藏实现细节。此外,这些命令还允许一些有用的扩展,在构建更大的系统时会派上用场:即通用接口、排序、序列化、跟踪、日志记录、安全性。

    【讨论】:

      【解决方案6】:

      您可以做任何您想做的事,但最好遵循代码的 usabilitymanageability 的模式。

      在现实生活中,Light 将是一个接口。 Light 有不同的实现方式,例如 LEDLightTubeLight

      如果通过InovkerRemoteControl)执行具体命令,则不用担心Receiver中方法名的变化。 switchOn() in Light can be changed to switchOnDevice() in future。但它不会影响 Invoker,因为 ConcreteCommand (LightsOnCommand) 将进行相关更改。

      假设您将接口发布到一个服务(服务 A)并在其他服务(服务 B)中实现。

      现在服务 A 不应该知道 Receiver 的变化。

      Invoker 提供消息的发送者和接收者之间的松散耦合。

      看看一些更相关的 SE 问题:

      Using Command Design pattern

      Command Pattern seems needlessly complex (what am I failing to understand?)

      【讨论】:

        【解决方案7】:

        我体验过命令模式的几个好处。但首先,我想提醒一下,与所有其他模式一样,此模式也是为了提高代码的可读性,并为您(可能)共享的代码库带来一些共同的理解。

        我使用这种模式从面向方法的界面过渡到面向命令的界面。这意味着,我将方法调用与必要的数据一起封装到具体命令中。是的,它使代码更具可读性,但更重要的是,我可以将方法视为对象,这样您就可以轻松地添加/删除/修改命令,而不会增加代码的复杂性。因此,它既易于管理又易于阅读。

        其次,由于您将方法作为对象,您可以存储/排队它们以便稍后执行它们。或者在你执行它们之后取消它们。这就是此模式帮助您实现“撤消”的地方。

        最后,命令模式也用于解耦命令执行和命令本身。这就像一个服务员不知道如何烹饪他收到的订单。他不在乎。他不必知道。如果他知道这一点,他也会做厨师。如果餐馆老板想解雇服务员,他/她最终也没有厨师。当然,这个定义并不特殊,也不只与命令模式有关。这就解释了为什么我们通常需要解耦或依赖管理。

        【讨论】:

          【解决方案8】:

          设计模式基本上是为解决复杂问题而开发的,或者换句话说,我们可以说它们用于防止您的解决方案变得复杂。它们为我们提供了在未来添加新功能的灵活性,而无需对现有代码进行太多更改。 对扩展开放,对修改关闭。

          命令模式基本上将请求封装为对象,从而让您可以参数化具有不同请求的其他对象并支持可撤消的操作。

          当我们看到遥控器是命令模式的最佳示例时。在这种情况下,我们为每个按钮关联了一个具体的命令,并且该命令具有要执行的接收器的信息。

          在您的开关案例示例中,假设您要将遥控器上的按钮关联到风扇而不是灯,那么您将不得不再次更改现有代码,因为您需要将按钮与命令关联。这是命令模式的本质,它为您提供了将任何命令与按钮相关联的灵活性,以便在运行时您可以更改功能。尽管在一般电视遥控器中,您没有这种可行性,即您不能将音量增大按钮设置为音量减小。但是,如果您使用任何应用程序来控制您的电视,那么您可以为任何按钮分配任何可用命令。

          此外,使用命令模式,您可以使用一组命令来实现特定功能,即宏命令。因此,如果您从更广泛的层面思考,那么这种模式可以让您灵活地扩展功能。

          命令模式帮助我们在命令对象(LightOnCommand、FanOffCommand 等)的帮助下解耦调用者(远程控制)和接收者(Light、Fan 等)。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-01-06
            • 2012-04-16
            • 2010-10-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-09-23
            相关资源
            最近更新 更多