【问题标题】:How does the Command pattern decouple the sender from the receiver?命令模式如何将发送者与接收者解耦?
【发布时间】:2016-05-18 13:06:47
【问题描述】:

Command 模式有一个带有少量方法的 IReceiver 接口,并且对应于每个方法都有具体的 Command 对象(使用 execute() 方法实现接口ICommand)。

我读到客户端知道具体的接收者和具体的命令,通常是客户端在具体的命令对象中设置接收者对象。那为什么说它解耦了发送者和接收者呢?

当客户端已经知道具体的接收者时,我觉得这不是松耦合,而且在这种情况下客户端可以直接调用接收者对象上的 API(方法)。

【问题讨论】:

    标签: java design-patterns command-pattern


    【解决方案1】:

    您可以将命令模式的工作流程想象如下。

    1. Command 为所有命令声明了一个接口,提供了一个简单的 execute() 方法,该方法要求命令的接收者执行操作。

    2. Receiver 知道如何执行请求。

    3. Invoker持有命令,可以通过调用execute方法获取Command执行请求。

    4. Client 创建ConcreteCommands 并为命令设置Receiver

    5. ConcreteCommand 定义了动作和接收者之间的绑定。

    6. Invoker 调用执行时,ConcreteCommand 将在接收器上运行一个或多个操作。

    查看示例代码以更好地理解事物。

    public class CommandDemoEx{
        public static void main(String args[]){
    
            // On command for TV with same invoker 
            Receiver r = new TV();
            Command onCommand = new OnCommand(r);
            Invoker invoker = new Invoker(onCommand);
            invoker.execute();
    
            // On command for DVDPlayer with same invoker 
            r = new DVDPlayer();
            onCommand = new OnCommand(r);
            invoker = new Invoker(onCommand);
            invoker.execute();
    
        }
    }
    interface Command {
        public void execute();
    }
    
    class Receiver {
        public void switchOn(){
            System.out.println("Switch on from:"+this.getClass().getSimpleName());
        }
    }
    
    class OnCommand implements Command{
    
        private Receiver receiver;
    
        public OnCommand(Receiver receiver){
            this.receiver = receiver;
        }
        public void execute(){
            receiver.switchOn();
        }
    }
    
    class Invoker {
        public Command command;
    
        public Invoker(Command c){
            this.command=c;
        }
        public void execute(){
            this.command.execute();
        }
    }
    
    class TV extends Receiver{
        public TV(){
    
        }
        public String toString(){
            return this.getClass().getSimpleName();
        }
    }
    class DVDPlayer extends Receiver{
        public DVDPlayer(){
    
        }
        public String toString(){
            return this.getClass().getSimpleName();
        }
    }
    

    输出:

    java CommandDemoEx
    Switch on from:TV
    Switch on from:DVDPlayer
    

    回答你的问题:

    我读过客户端知道具体的接收器和具体的命令,通常是客户端在具体的命令对象中设置接收器对象。那为什么说它使发送者和接收者解耦

    为了标准化单词,将“sender”替换为“invoker”。现在通过代码。

    1. Invoker simply executes the ConcreteCommand(在本例中为 OnCommand)通过传递 ConcreteReceiver。
    2. ConcreteCommand executes Command 通过 ConcreteReceiver 即ConcreteCommand defines binding between Action and Receiver.
    3. 如果您看到工作流,Invoker 不会随着附加命令而改变,您可以在 Invoker 的 execute() 方法中添加业务逻辑,如 java.lang.Thread,解释如下。
    4. 这样Client (sender) and Receiver are loosely couple through Invoker, which has knowledge of what command to be executed

    线程示例来自link

    您可以通过实现 Runnable 对象来创建线程。

    Thread t = new Thread (new MyRunnable()).start();
    

    =>

     Invoker invoker = new Invoker(new ConcreteCommand());
     invoker.start() 
    

    你在 start() 中有逻辑调用 ConcreteCommand.execute(),在上述情况下它是 run()。

    start() 方法会在 Thread 中调用 run() 方法。如果直接调用 run() 方法会发生什么?它不会被视为线程

    像这个线程的 start() 方法一样,你可以在 Invoker 中添加一些业务逻辑。

    public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
            group.add(this);
            start0();
            if (stopBeforeStart) {
                stop0(throwableFromStop);
            }
        }
    
    private native void start0(); // Native code is not here but this method will call run() method
    
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    

    编辑:

    关于您的最后一个查询

    这里我们创建命令对象、接收者对象和调用者对象。然后在命令对象中传递接收者对象,然后在调用者对象中传递命令对象。我们对每个接收器执行此操作,就像我们在此处对 TV 和 DVDPlayer 执行的操作一样。同样在方法中,TV 和 DVDPlayer 的“主要”对象是已知的并且实际上是创建的。我们可以简单地执行 tvObject.switchOn() 和 dvdPlayer.switchOn()。命令模式有什么帮助

    客户不必担心Receiver 类的变化。 Invoker 直接作用于具有Receiver 对象的ConcreteCommandReceiver 对象将来可能会将 siwtchOn() 更改为 switchOnDevice()。但是客户端交互不会改变。

    如果你有两个不同的命令,比如switchOn() 和switchOff(),你仍然可以使用相同的Invoker

    【讨论】:

    • 感谢 Ravindra 的回答,但我仍然不清楚。我想澄清我对命令模式如何有用的理解。这里我们创建命令对象、接收者对象和调用者对象。然后在命令对象中传递接收者对象,然后在调用者对象中传递命令对象。我们对每个接收器执行此操作,就像我们在此处对 TV 和 DVDPlayer 执行的操作一样。同样在方法中,TV 和 DVDPlayer 的“主要”对象是已知的并且实际上是创建的。我们可以简单地执行 tvObject.switchOn() 和 dvdPlayer.switchOn()。命令模式有什么帮助。
    • 如果 switchOn 稍后更改为 switchDevice,如果从 ConcreteCommand 执行该命令,客户端不需要任何更改。与 switchOn 一样,您可以使用 switchOff 命令,并且可以对 on 和 off 命令使用相同的调用程序。
    • 请参考codereview.stackexchange.com/questions/120029/… 在学习了Command模式和这里的cmets之后,我已经按照我的理解实现了Command模式。很高兴有你的评论 cmets 在那里......
    • 在那个问题中更新了我的 cmets。把这个问题链接在那里:)
    • ' 客户端无需担心 Receiver 类的变化。 ..... Receiver 对象将来可能会将siwtchOn() 更改为switchOnDevice()。但是客户端交互不会改变。 ' - 如果接收者对象将 siwtchOn() 更改为 switchOnDevice() ,则客户端无需担心更改。但是Concretecommand 需要知道它,对吧?如果是,那么您在这里指出什么好处?
    【解决方案2】:

    直接来自Wikipedia

    命令模式是一种行为设计模式,其中对象用于封装执行操作或稍后触发事件所需的所有信息。

    编辑

    在重新阅读了Gang of Four 的命令模式部分后,我想出了一个更好的方案。假设您有一个 GUI 库,它定义了以下内容:

    public interface Command {
        public void execute();
    }
    
    public class Button {
        private Command command;
    
        public Button(Command command) {
            this.command = command;
        }
    
        public void click() {
            command.execute();
        }
    }
    

    在这种情况下,Button 是命令的接收者,而您创建 Buttons 实际实例的代码是客户端。当然,当你创建一个按钮时,你必须定义Command接口的一些具体实现。但是 GUI 库不需要知道这些类;它所需要的只是界面。这就是 GUI 代码与您的代码分离的方式。

    【讨论】:

    • 感谢安德鲁的回答。对不起,但我仍然不太清楚这一点。您能否举一个小例子,如果不使用命令模式,如果不使用命令模式,事情会变得难以管理或复杂性增加或代码重复或任何其他不良影响......
    • @nits.kk 查看编辑,我希望它更有意义。主要思想是实现隐藏在 Command 接口后面,因此当您只需要一个类时,您不会得到多个类。例如。一个RefreshButton 和一个UndoButton 可能只是一个普通的Button,执行不同的Commands。
    • 令人印象深刻的解释 Andrew.. n 感谢您的努力。但我觉得上面更像是观察者模式。在上面的解释中,它更像是回调某种观察者模式的微缩形式。在此示例中,Command 就像在按钮类中注册的观察者。单击后,回调方法“execute”被调用,在“execute()”方法的具体实现中,诸如刷新之类的实际操作可以执行撤消。如果我的理解有误,请纠正我...
    • 是的,设计模式经常有很多重叠。这也显示了 Strategy 的许多特性,其中实际实现隐藏在接口后面。 Command 的特别之处在于每个命令对象都包含执行操作所需的所有信息。这个例子并没有真正展示那么多,你想看看它是如何解耦代码的。这是我能展示的最简单的方式。
    【解决方案3】:

    松散耦合不是 Command 的主要目标

    这是来自原始Design Patterns book 的命令模式的类图:

    正如你所说,Client 知道ConcreteCommandReceiver,所以那里没有解耦。

    为什么说它使发送者和接收者解耦

    我的这本书并没有说这是命令模式的目标:

    将请求封装为对象,从而使您可以对具有不同请求、队列或日志请求的客户端进行参数化,并支持可撤消的操作。

    Andrew 的回答涉及到 逻辑线程 与命令分离的事实。当您参考设计模式中描述的模式的序列图时,您可能会更好地看到InvokerCommand 之间的松散耦合:

    许多设计模式定义了一个与变体(例如,访问者、策略、观察者、迭代器等)松散耦合的客户端。松散耦合有利于可维护性,即所谓的变更设计。命令是特殊的,因为不受更改保护的客户端是Invoker——它与ConcreteCommmand 类分离。我认为这就是您正在寻找的经典解耦。添加新命令需要更改Client,但不应该破坏Invoker,谁只知道Command抽象。

    我一直认为命令模式是独一无二的,因为它的主要目标似乎是提供功能需求:撤消、重做、日志记录、宏命令操作、事务等。


    编辑

    关于IReceiver 抽象和与Client 和具体Receiver 类的解耦:这可能只是与Command 一起使用的策略模式。我引用了原书。存在许多模式变体(因此,维基百科并不总是很好的模式参考)。

    【讨论】:

    • 感谢您的回复。如果有多个接收器具有不同的动作,如电视:switchON(), setFirstChannel(); AC:switchOn(),setMinTemp(); MusicPlayer:switchOn(),setMaxVolume()。如果我们有所有这些请求入队。如果需要包含像 Microwave:switchON()、set30SecTimer() 这样的任何新设备,则可以将 Microwave 的简单对象封装在 TimerCommand 的对象中,并且可以简单地将其添加到队列中。这种方式命令模式可以提供很好的帮助。如果我错了,请纠正。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-03
    • 1970-01-01
    • 2011-02-20
    • 1970-01-01
    • 2015-07-28
    • 1970-01-01
    相关资源
    最近更新 更多