我可以在轻松调用所需方法的同时使用命令设计模式?

时间:2015-09-16 00:20:09

标签: java oop design-patterns command-pattern

我正在研究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;
 }

8 个答案:

答案 0 :(得分:41)

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

这使您可以执行诸如按顺序执行的命令列表,依赖于其他项目,分配给某些触发事件等的事项。

在您的示例中,您可以拥有其他具有自己命令的类(例如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);   

现在转换为命令的方法甚至不需要知道命令 - 只要它们具有正确的签名,您就可以通过引用该方法安静地创建匿名命令对象。

答案 1 :(得分:23)

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

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

隐藏实施

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

表明行动,而不是如何行动

命令会为您提供此类视图。您将立即了解TurnLightOn命令的作用或StartCar使用命令,您将隐藏有关某些事情的详细信息,同时清楚地指出要执行的操作。

允许更改内部细节

另外,让我们稍后再说你重建整个Light类,或者Car类,这需要你实例化几个不同的对象,并且在实际做你想要的之前你可能还需要其他的东西。操作。在这种情况下,如果您已经在很多地方实施了直接访问方法,则需要在事先对其进行编码的所有地方进行更改。 使用命令,您可以在不更改命令调用的情况下更改内容的内部细节。

可能的命令扩展

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

安全扩展或界面曝光

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

但是,从外部看,您可能希望限制对灯光的访问,以便仅打开或关闭灯光。 使用命令可以将界面限制为类

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

执行内容的通用接口

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

由于您要将实现细节遗漏给命令本身,因此您可以使用常用方法来实例化命令,执行命令并查看结果。这样可以更轻松地编码,而不需要了解如何实例化特定的LightCar类,并确定其结果。

命令排序

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

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

命令序列化

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

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

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

跟踪命令

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

这样可以轻松更改日志记录系统,或添加诸如计时或启用/禁用日志记录之类的内容。并且它很容易在命令模块中维护。

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

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

答案 2 :(得分:12)

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

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

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

例如,在第一个示例中,opts实例知道当它看到命令行选项--on时,它应该打开灯。但它知道这一点,却并不知道“打开灯”意味着什么。事实上,opts实例很可能来自第三方库,因此它无法知道灯光。所有它知道的是如何使用它解析的命令行选项来协调动作(命令)。

答案 3 :(得分:7)

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

在你的情况下,如果你需要做的是打开和关闭灯开关,而不是其他,第二个选项是没有脑子。

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

答案 4 :(得分:5)

您提供的示例ICommand相当有限,仅在没有lambda表达式的编程语言中实际使用。 Sprinter在他的回答中很好地介绍了命令工厂的用法。

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

与大多数设计模式一样,命令模式也会变得更加复杂。它也是众所周知的,因此有助于为大多数程序员清楚地编写代码。

答案 5 :(得分:2)

您可以做任何想法,但最好遵循usability manageability代码的模式。

在现实生活中,Light将是一个界面。您有Light的不同实现,例如LEDLightTubeLight

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

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

现在服务A不应该知道Receiver中的更改。

Invoker提供Sender&之间的松散耦合。接收消息。

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

Using Command Design pattern

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

答案 6 :(得分:2)

我经历过Command模式的好处。但首先,我想提醒一下,作为所有其他模式,这种模式也是为了提高代码的可读性,并为您的(可能)共享代码库带来一些共同的理解。

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

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

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

答案 7 :(得分:1)

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

命令模式基本上将请求封装为对象,从而允许您使用不同的请求参数化其他对象并支持可撤销操作。

当我们看到遥控器是命令模式的最佳示例时。在这种情况下,我们将具体命令与每个按钮相关联,并且该命令具有接收器的信息以对其进行操作。

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

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

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