折騰Java設計模式之命令模式

大萌小路發表於2019-01-23

部落格原文地址 折騰Java設計模式之命令模式

命令模式

wiki上的描述 Encapsulate a request as an object, thereby allowing for the parameterization of clients with different requests, and the queuing or logging of requests. It also allows for the support of undoable operations.

翻譯意思,把請求封裝成一個物件,從而允許我們可以對客戶端的不同請求進行引數化,以及對請求進行排隊或記錄。還允許支援撤銷操作。看起來好像很複雜,很難理解。

通俗簡單理解,它就是將請求封裝成一個物件,在這裡就是這個物件就是命令,而這個命令就是將請求方和執行方分離隔開。從而每一個命令其實就是操作,而這樣的流程就是請求方發出請求要求執行某操作,接收方收到請求後並執行對應的操作。這樣下來,請求方和接收方就解耦了,使得請求方完全不知道接受的操作方法,從也不會知道接收方是何時接受到請求的,又是何時執行操作的,又是怎麼執行操作的。

具體的角色

Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中宣告瞭用於執行請求的execute()等方法,通過這些方法可以呼叫請求接收者的相關操作。

ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現execute()方法時,將呼叫接收者物件的相關操作(Action)。

Invoker(請求方):呼叫者即請求傳送者,它通過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的execute()方法,從而實現間接呼叫請求接收者的相關操作。

Receiver(接收方):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

Client(客戶端):建立具體命令的物件並且設定命令物件的接受者。

再來看看UML圖

img

img

從上方的時序圖中可以看出執行的順序,Invoker執行execute方法,呼叫Command1物件,Command1執行action1方法呼叫Receiver1物件。

乾貨程式碼

原始碼在我的GitHub地址

普通的命令模式

現在結合下上回說到的狀態模式一起來實現這個風扇的左轉和右轉功能,這次把他用命令模式來代替之前風扇的轉動,把它當做命令來。

客戶端簡單的定義請求方和接收方以及對於的左轉命令和右轉命令,設定命令後對應的執行命令。

public class Client {

    public static void main(String[] args) {
        Invoker invoker = new Invoker();
        Receiver receiver = new Receiver();
        Command leftCommand = new LeftCommand(receiver);
        Command rightCommand = new RightCommand(receiver);

        invoker.setCommand(rightCommand);
        invoker.execute();
        invoker.execute();
        invoker.execute();

        invoker.setCommand(leftCommand);
        invoker.execute();
        invoker.execute();
    }
}
複製程式碼

image-20190102115207650

請求方

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Invoker {

    private Command command;

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

}
複製程式碼

抽象命令

public interface Command {

    void execute();
}
複製程式碼

開關左轉

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LeftCommand implements Command {

    private Receiver receiver;

    @Override
    public void execute() {
        receiver.left();
    }
}
複製程式碼

開關右轉

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RightCommand implements Command {

    private Receiver receiver;

    @Override
    public void execute() {
        receiver.right();
    }
}
複製程式碼

接收方

public class Receiver {

    private Context context = new Context(new CloseLevelState());

    public void left() {
        context.left();
    }

    public void right() {
        context.right();
    }
}
複製程式碼

通過命令模式把左轉和右轉封裝成命令,以及之前的狀態模式變更風扇的狀態。本次就是通過狀態模式和命令模式實現了一個風扇開關左右轉的功能。

巨集命令或者叫做組合命令

設計一組命令,簡單的處理事情,列印一句話,封裝成一組命令。這次我們用了Java8來寫,可以使用lambda。

@Slf4j
public class Client {

    public static void main(String[] args) {
        Invoker invoker = new Invoker();
        log.info("初始化ABC3個命令");
        Command aCommand = () -> log.info("A處理這個請求");
        invoker.addCommand(aCommand);
        invoker.addCommand(() -> log.info("B處理這個請求"));
        invoker.addCommand(() -> log.info("C處理這個請求"));
        invoker.execute();

        log.info("---------------------------");
        log.info("加入新命令D");
        invoker.addCommand(() -> log.info("D處理這個請求"));
        invoker.execute();

        log.info("---------------------------");
        log.info("加入新命令E");
        invoker.addCommand(() -> log.info("E處理這個請求"));
        invoker.execute();

        log.info("---------------------------");
        log.info("移除命令A");
        invoker.removeCommand(aCommand);
        invoker.execute();
    }
}
複製程式碼

列印語句。

image-20190102143032334

抽象命令

@FunctionalInterface
public interface Command {

    void execute();
}
複製程式碼

請求方

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Invoker {

    private List<Command> commandList = Lists.newArrayList();

    public void addCommand(Command command) {
        commandList.add(command);
    }

    public void removeCommand(Command command) {
        commandList.remove(command);
    }

    public void execute() {
        if(CollectionUtils.isEmpty(commandList)) {
            return;
        }
        commandList.stream().forEach(command -> command.execute());
    }

}
複製程式碼

撤銷操作

在普通的命令模式的基礎上,增加了撤銷操作,在這裡的撤銷操作,其實即為左轉時的右轉,右轉時的左轉。

@Slf4j
public class Client {

    public static void main(String[] args) {
        Invoker invoker = new Invoker();
        Receiver receiver = new Receiver();
        Command leftCommand = new LeftCommand(receiver);
        Command rightCommand = new RightCommand(receiver);

        invoker.setCommand(rightCommand);
        invoker.execute();
        invoker.execute();
        invoker.execute();
        invoker.undo();
        invoker.undo();

        invoker.setCommand(leftCommand);
        invoker.execute();
        invoker.undo();
    }
}
複製程式碼

image-20190102145507099

抽象命令增加了撤銷操作

public interface Command {

    void execute();

    void undo();
}
複製程式碼

具體左轉時

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LeftCommand implements Command {

    private Receiver receiver;

    @Override
    public void execute() {
        receiver.left();
    }

    @Override
    public void undo() {
        receiver.right();
    }
}
複製程式碼

右轉時

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RightCommand implements Command {

    private Receiver receiver;

    @Override
    public void execute() {
        receiver.right();
    }

    @Override
    public void undo() {
        receiver.left();
    }
}
複製程式碼

請求方

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Invoker {

    private Command command;

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

    public void undo() {
        command.undo();
    }

}
複製程式碼

接收方

public class Receiver {

    private Context context = new Context(new CloseLevelState());

    public void left() {
        context.left();
    }

    public void right() {
        context.right();
    }
}
複製程式碼

命令模式總結

優點

(1) 降低系統的耦合度。由於請求者與接收者之間不存在直接引用,因此請求者與接收者之間實現完全解耦,相同的請求者可以對應不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨立性。

(2) 新的命令可以很容易地加入到系統中。由於增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統原始碼,甚至客戶類程式碼,滿足“開閉原則”的要求。

(3) 可以比較容易地設計一個命令佇列或巨集命令(組合命令)

(4) 為請求的撤銷(Undo)和恢復(Redo)操作提供了一種設計和實現方案

缺點

**使用命令模式可能會導致某些系統有過多的具體命令類。**因為針對每一個對請求接收者的呼叫操作都需要設計一個具體命令類,因此在某些系統中可能需要提供大量的具體命令類,這將影響命令模式的使用。

適用場景

(1) 系統需要將請求呼叫者和請求接收者解耦,使得呼叫者和接收者不直接互動。請求呼叫者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心何時被呼叫。

(2) 系統需要在不同的時間指定請求、將請求排隊和執行請求。一個命令物件和請求的初始呼叫者可以有不同的生命期,換言之,最初的請求發出者可能已經不在了,而命令物件本身仍然是活動的,可以通過該命令物件去呼叫請求接收者,而無須關心請求呼叫者的存在性,可以通過請求日誌檔案等機制來具體實現。

(3) 系統需要支援命令的撤銷(Undo)操作和恢復(Redo)操作。

(4) 系統需要將一組操作組合在一起形成巨集命令。

(5)執行緒池有一個addTash方法,將任務新增到待完成的佇列中,佇列中的元素就是命令物件,通常的就是一個公共介面,像我們常用的java.lang.Runnable介面。

(6)java8之後,最好在Command介面中@FunctionalInterface修飾,這樣具體的命令就可以使用lambda表示式啦。

Java中的使用

將操作封裝到物件內,以便儲存,傳遞和返回。

java.lang.Runnable

javax.swing.Action

文章參考

java設計模式之命令模式

細數JDK裡的設計模式

wiki的命令模式

相關文章