設計模式-命令模式(Command)

關注公衆號 JavaStorm 獲取更多成長。java

大約須要6分鐘讀完。建議收藏後閱讀。
命令模式把一個請求或者操做封裝到一個對象中。命令模式容許系統使用不一樣的請求把客戶端參數化,對請求排隊或者記錄請求日誌,能夠提供命令的撤銷和恢復功能。
GitHub地址: https://github.com/UniqueDong/zero-design-stu 中的 headfirst 包下代碼。git

概述

命令模式是對命令的封裝。命令模式把發出命令的責任和執行命令的責任分割開,委派給不一樣的對象。github

每個命令都是一個操做:請求的一方發出請求要求執行一個操做;接收的一方收到請求,並執行操做。命令模式容許請求的一方和接收的一方獨立開來,使得請求的一方沒必要知道接收請求的一方的接口,更沒必要知道請求是怎麼被接收,以及操做是否被執行、什麼時候被執行,以及是怎麼被執行的。編程

  命令容許請求的一方和接收請求的一方可以獨立演化,從而具備如下的優勢:數組

  (1)命令模式使新的命令很容易地被加入到系統裏。ide

  (2)容許接收請求的一方決定是否要否決請求。測試

  (3)能較容易地設計一個命令隊列。this

  (4)能夠容易地實現對請求的撤銷和恢復。線程

  (5)在須要的狀況下,能夠較容易地將命令記入日誌。
類圖設計

角色

  • 客戶端(Client)角色: 建立一個 ConcreteCommand,並設置其接受者。
  • 命令(Command)角色: 爲全部的命令申明一個接口。調用命令對象的 execute 方法就可讓接受者執行相關的動做,同事接口還具有一個 undo() 撤回方法。
  • 具體命令(ConcreteCommand)角色: 定義一個接收者和行爲之間的弱耦合;實現execute()方法,負責調用接收者的相應操做。execute()方法一般叫作執行方法。調用者只須要調用 execute 方法就能夠發出請求,而後由 ConcreteCommand 調用接受者的一個或者多個動做。
  • 調用者(Invoker)角色: 調用者持有一個命令對象,提供一個觸發方法調用命令對象的 execute 方法,將命令執行。
  • 接收者(Receiver)角色: 負責具體實施和執行一個請求。任何一個類均可以成爲接收者,實施和執行請求的方法叫作行動方法。

執行流程

  1. 客戶端建立一個命令對象。
  2. 客戶端在調用者對象上調用 setCommand 方法。
  3. 在將來合適的時間點,調用者調用命令對象的 execute 方法。
  4. 命令經過調用者委託到對應的接受者執行。完成任務。

場景模擬

一個全能遙控器 6個可編程插槽(每一個能夠指定一個不一樣的家電裝置),用來控制家電(電視、空調、冰箱、音響)。每一個插槽有對應的 [開] 和 [關] 按鈕。同時還具有一個總體一鍵撤回按鈕。撤回需求是這樣的,好比電燈是關的,而後按下開啓按鈕電燈就開了。如今假如按下撤銷按鈕,那麼上一個動做將會翻轉。在這裏,電燈將會關閉。
遙控器
插槽鏈接對應的家電,開關是對應的指令。每一個家電對應兩個指令,分別是 【開】和【關】按鍵。

許多家電都有 on() 和 off() 方法,除此以外還有一些 setVolumn()、setTV()、setTemperature() 方法。
咱們總不能 寫 if slot1 == Light then light.on()。

代碼實現

命令接受者角色

首先咱們擁有不少家電。他們其實就是不一樣命令的接受者執行。

package com.zero.headfirst.command.receiver;

public class Light {
    public void on() {
        System.out.println("打開電燈。");
    }
    public void off() {
        System.out.println("關燈。");
    }
}
  • 音響
package com.zero.headfirst.command.receiver;

public class Stereo {
    public void on() {
        System.out.println("打開音響");
    }

    public void off() {
        System.out.println("關閉音響");
    }

    public void setCD() {
        System.out.println("放入CD");
    }

    public void setVolume() {
        System.out.println("音響音量設置爲20");
    }
}

命令角色

首先讓全部的命令對象實現該接口,分別有命令執行與撤回

package com.zero.headfirst.command;

/**
 * 命令(Command)角色
 */
public interface Command {
    /**
     * 命令執行
     */
    void execute();

    /**
     * 命令撤銷
     */
    void undo();
}

具體命令角色

  • 定義開燈命令,實現 execute 。持有 命令接受者 燈的引用,從而當調用者調用 execute 將委託給對應的 燈執行開燈操做。
package com.zero.headfirst.command.impl;

import com.zero.headfirst.command.Command;
import com.zero.headfirst.command.receiver.Light;

public class LightOnCommand implements Command {

    /**
     * 持有接受者實例,以便當命令execute執行的時候由接受者執行開燈
     */
    private Light light;

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void undo() {
        light.off();
    }

    /**
     * 設置命令的接受者
     * @param light
     */
    public void setLight(Light light) {
        this.light = light;
    }
}
  • 定義關燈命令
package com.zero.headfirst.command.impl;

import com.zero.headfirst.command.Command;
import com.zero.headfirst.command.receiver.Light;

public class LightOffCommand implements Command {

    /**
     * 持有接受者實例,以便當命令execute執行的時候由接受者執行
     */
    private Light light;

    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void undo() {
        light.on();
    }

    public void setLight(Light light) {
        this.light = light;
    }
}
  • 定義打開音響命令
package com.zero.headfirst.command.impl;

import com.zero.headfirst.command.Command;
import com.zero.headfirst.command.receiver.Stereo;

/**
 * 音響開指令
 */
public class StereoOnCommand implements Command {

    private Stereo stereo;

    @Override
    public void execute() {
        stereo.on();
        stereo.setCD();
        stereo.setVolume();
    }

    @Override
    public void undo() {
        stereo.off();
    }

    public void setStereo(Stereo stereo) {
        this.stereo = stereo;
    }
}
  • 定義關閉音響命令
package com.zero.headfirst.command.impl;

import com.zero.headfirst.command.Command;
import com.zero.headfirst.command.receiver.Stereo;

public class StereoOffCommand implements Command {

    private Stereo stereo;

    public void setStereo(Stereo stereo) {
        this.stereo = stereo;
    }

    @Override
    public void execute() {
        stereo.off();
    }

    @Override
    public void undo() {
        stereo.on();
        stereo.setCD();
        stereo.setVolume();
    }
}

剩下的打開電視機、關閉電視機、打開空調、關閉空調的就不一一寫了。都是同樣的模板套路。具體代碼能夠查閱 GitHub地址: https://github.com/UniqueDong/zero-design-stu 中的 headfirst 包下代碼。

調用者角色

其實就是咱們的遙控器。

package com.zero.headfirst.command;

import com.zero.headfirst.command.impl.NoCommand;

import java.util.Arrays;

/**
 * 調用者:遙控器
 */
public class RemoteControl {
    /**
     * 一共4個家電插槽,每一個插槽有 開與關命令。
     */
    private Command[] onCommands;
    private Command[] offCommands;

    //用來保存前一個命令,用來實現撤銷功能
    private Command undoCommand;

    /**
     * 經過構造器初始化開關數組
     */
    public RemoteControl() {
        onCommands = new Command[4];
        offCommands = new Command[4];
        //初始化全部插槽爲空指令
        Command noCommand = new NoCommand();
        for (int i = 0; i < 4; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        //一開始沒有所謂的前一個命令,因此默認無指令
        undoCommand = noCommand;
    }

    /**
     * 設置指定插槽對應的按鈕指令
     * @param slot 插槽位置
     * @param onCommand 開指令
     * @param offCaommand 關指令
     */
    public void setCommand(int slot,Command onCommand, Command offCaommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCaommand;
    }

    /**
     * 模擬按下指定插槽對應的【開】按鍵
     */
    public void pressOnButton(int slot) {
        onCommands[slot].execute();
        //將當前指令記錄下來,用於在撤銷的時候能執行命令對應的 undo 方法從而實現撤銷功能
        undoCommand = onCommands[slot];
    }

    /**
     * 模擬按下指定插槽對應的【關】按鍵
     */
    public void pressOffButton(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    /**
     * 撤銷功能
     */
    public void pressUndoButton() {
        undoCommand.undo();
    }

    @Override
    public String toString() {
        return "RemoteControl{" +
                "onCommands=" + Arrays.toString(onCommands) +
                ", offCommands=" + Arrays.toString(offCommands) +
                '}';
    }
}

客戶端角色

獲取遙控器,而且拿到燈、空調等命令接受者。分別建立對應的 【開】,【關】指令。
連接到對應的插槽。當按下按鈕的時候觸發指定的指令。

package com.zero.headfirst.command;

import com.zero.headfirst.command.impl.*;
import com.zero.headfirst.command.receiver.AirConditioning;
import com.zero.headfirst.command.receiver.Light;
import com.zero.headfirst.command.receiver.Stereo;
import com.zero.headfirst.command.receiver.TV;

/**
 * 客戶端角色
 */
public class CommandClient {
    public static void main(String[] args) {
        //建立一個遙控器-調用者角色
        RemoteControl remoteControl = new RemoteControl();
        //1. 建立電燈-接受者角色
        Light light = new Light();
        //建立開燈、關燈命令-命令具體角色
        LightOnCommand lightOnCommand = new LightOnCommand();
        lightOnCommand.setLight(light);
        LightOffCommand lightOffCommand = new LightOffCommand();
        lightOffCommand.setLight(light);

        //調用者設置電燈插槽以及對應的開關按鍵指令-調用者角色
        remoteControl.setCommand(0, lightOnCommand, lightOffCommand);

        // 2. 設置音響插槽與對應按鍵指令
        Stereo stereo = new Stereo();
        StereoOnCommand stereoOnCommand = new StereoOnCommand();
        stereoOnCommand.setStereo(stereo);
        StereoOffCommand stereoOffCommand = new StereoOffCommand();
        stereoOffCommand.setStereo(stereo);

        remoteControl.setCommand(1, stereoOnCommand, stereoOffCommand);

        //3. 空調
        AirConditioning airConditioning = new AirConditioning();
        AirConditioningOnCommand airConditioningOnCommand = new AirConditioningOnCommand();
        airConditioningOnCommand.setAirConditioning(airConditioning);
        AirConditioningOffCommand airConditioningOffCommand = new AirConditioningOffCommand();
        airConditioningOffCommand.setAirConditioning(airConditioning);

        remoteControl.setCommand(2, airConditioningOnCommand, airConditioningOffCommand);

        //4. 電視
        TV tv = new TV();
        TVOnCommand tvOnCommand = new TVOnCommand();
        tvOnCommand.setTv(tv);
        TVOffCommand tvOffCommand = new TVOffCommand();
        tvOffCommand.setTv(tv);

        remoteControl.setCommand(3, tvOnCommand, tvOffCommand);

        //模擬按鍵
        System.out.println("-------碼農回家了,使用遙控開啓電燈、音響、空調、電視----");
        remoteControl.pressOnButton(0);
        remoteControl.pressOnButton(1);
        remoteControl.pressOnButton(2);
        remoteControl.pressOnButton(3);

        System.out.println("------碼農睡覺了,使用遙控關閉電燈、音響、電視。不關空調--------");
        remoteControl.pressOffButton(0);
        remoteControl.pressOffButton(1);
        remoteControl.pressOffButton(3);

        System.out.println("----撤銷測試,先打開電燈。再關閉電燈。而後按撤銷----");
        remoteControl.pressOnButton(0);
        remoteControl.pressOffButton(0);
        //一鍵撤銷
        remoteControl.pressUndoButton();
    }
}

測試結果

-------碼農回家了,使用遙控開啓電燈、音響、空調、電視----
打開電燈。
打開音響
放入CD
音響音量設置爲20
打開空調
空調溫度設置28°
打開電視
設置頻道爲宇宙電視臺
電視音量設置爲20
------碼農睡覺了,使用遙控關閉電燈、音響、電視。不關空調--------
關燈。
關閉音響
關閉電視
----撤銷測試,先打開電燈。再關閉電燈。而後按撤銷----
打開電燈。
關燈。
打開電燈。

總結

使用場景:

  1. 工做隊列:在某一端添加指令,只要是實現命令模式的對象均可以放到隊列裏。另一端是線程。線程進項下面的工做:從隊列取出一個命令,而後調用execute 方法,調用完後將該命令丟棄,再繼續取下一個命令。
  2. 線程池。

關注公衆號 JavaStorm 獲取更多模式

相關文章
相關標籤/搜索