多線程場景設計利器:分離方法的調用和執行——命令模式總結

前言

個人感覺,該模式主要還是在多線程程序的設計中比較常用,尤其是一些異步任務執行的過程。但是本文還是打算先在單線程程序裏總結它的用法,至於多線程環境中命令模式的用法,還是想在多線程的設計模式裏重點總結。

實現思路

其實思路很簡單,就是把方法的請求調用和具體執行過程分開,讓客戶端不知道該請求是如何、何時執行的。那麼如何分開呢?

其實沒什麼複雜的,就是使用 OO 思想,把對方法的請求封裝爲對象即可,然後在設計一個請求的接受者對象,當然還要有一個請求的發送者對象,請求本身也是一個對象。最後,請求要如何執行呢?

故,除了請求對象,請求發送者,請求接受者,還要一個請求執行者——這裏可以看成是客戶端,而請求(其實叫命令、或者請求都是一樣的意思,後文就用請求這個術語)最好設計爲抽象的(或者接口)。

也可得知,命令模式是對象的行爲型的設計模式。

簡單的命令模式

模擬場景:在線教育平臺售賣一些培訓的視頻課程,規定必須付費後才能觀看,故管理員需要有開放課程觀看和關閉課程觀看權限的操作

首先需要一個抽象的命令(請求)接口

public interface ICommand { // 抽象的命令(請求)接口
    void execute();
}

然後設計一個課程類——Lesson,它代表課程本身,也是命令(請求)的接受者,因爲是對課程這個實體下命令

public class Lesson { // 代表課程本身,也是命令(請求)的接受者,因爲是對課程這個實體下命令
    private String name;

    public Lesson(String name) {
        this.name = name;
    }

    public void openLesson() {
        System.out.println("可以觀看課程:" + name);
    }

    public void closeLesson() {
        System.out.println("不可以觀看課程:" + name);
    }
}

下面是兩個具體的命令類,分別實現命令接口,裏面是有聚合關係,把課程 Lesson 的引用聚合到命令類,哪一個命令要對哪一個實體,不能寫錯,比如關閉對關閉。

public class CloseCommand implements ICommand {
    private Lesson lesson;

    public CloseCommand(Lesson lesson) {
        this.lesson = lesson;
    }

    @Override
    public void execute() {
        this.lesson.closeLesson();
    }
}
//////////////////////////////////////////////////////
public class OpenCommand implements ICommand {
    private Lesson lesson;

    public OpenCommand(Lesson lesson) {
        this.lesson = lesson;
    }

    @Override
    public void execute() {
        this.lesson.openLesson();
    }
}

設計一個管理員類,作爲命令(請求)的調用者,用來發出請求(命令),而命令的實際執行,交給了命令(請求)的接受者——Lesson

public class Admin2 {
    private ICommand commond;

    public void setCommond(ICommand commond) {
        this.commond = commond;
    }

    public void executeCommond() {
        this.commond.execute();
    }
}

客戶端

Lesson lesson1 = new Lesson("c++"); // 請求(命令)的接受者
        CloseCommand closeCommand1 = new CloseCommand(lesson1); // 命令封裝爲對象
        OpenCommand openCommand1 = new OpenCommand(lesson1);
        Admin2 admin2 = new Admin2(); // 請求(命令)的調用者:用來發出請求
        admin2.setCommond(openCommand1); // 將命令傳給調用者
        admin2.executeCommond(); // 發出請求(命令),但是admin 並不知道這個請求(命令)發給了誰,是誰在執行這個請求(命令)
        admin2.setCommond(closeCommand1);
        admin2.executeCommond();

如上就實現了請求調用和具體執行的分離(解耦)

一次執行多個命令

下面是一次執行多個命令的寫法,也可以作爲宏命令的實現

命令接口和具體命令都不變,admin 變化如下:

public class Admin {
    private List<ICommand> commondList = new ArrayList<>(); // 使用 ArrayList 還能保證命令的順序執行

    public void addCommond(ICommand commond) {
        commondList.add(commond);
    }

    public void executeCommond() {
        for (ICommand commond : commondList) {
            commond.execute();
        }
        commondList.clear();
    }
}

當然這裏用棧等數據結構去包裝命令也是可以的

Lesson lesson = new Lesson("java"); // 請求(命令)的接受者
        CloseCommand closeCommand = new CloseCommand(lesson); // 命令
        OpenCommand openCommand = new OpenCommand(lesson);
        Admin admin = new Admin(); // 請求(命令)的調用者:用來發出請求
        admin.addCommond(openCommand); // 將命令傳給調用者
        admin.addCommond(closeCommand);
        admin.executeCommond();

引申:空類型模式

再比如,使用靜態數組去包裝命令,這裏引申一個空類型模式,就是說有一個類,這個類什麼都不做,就是佔位或者初始化用的,代替 null 類型。

下面舉一個例子,設計一個控制器,控制電燈的開關,閃爍,變暗,變亮等操作

public interface ICommand2 {
    void execute(); // 命令接口
}
//////////////////////////////////
public class LightOffCommand implements ICommand2 {
    private Light light;

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

    @Override
    public void execute() {
        this.light.off();
    }
}
//////////////////////////////////
public class LightOnCommand implements ICommand2 {
    private Light light;

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

    @Override
    public void execute() {
        this.light.on();
        this.light.zoomin();
        this.light.blink();
    }
}
//////////////////////////////////
public class EmptyCommand implements ICommand2 { // 空類型模式的體現
    @Override
    public void execute() {
        System.out.println("什麼都不做");
    }
}
//////////////////////////////////
public class Light {
    public Light() {
    }

    public void on() {
        System.out.println("電燈打開");
    }

    public void off() {
        System.out.println("電燈關閉");
    }

    public void zoomin() {
        System.out.println("燈光變強");
    }

    public void zoomout() {
        System.out.println("燈光變弱");
    }

    public void blink() {
        System.out.println("燈光閃爍");
    }

    public void noBlink() {
        System.out.println("燈光停止閃爍");
    }
}

下面是一個控制器類,setCommand 方法可以設置某個命令和某個操作的對應關係,初始化時,使用空類型模式

public class MainController {
    private ICommand2[] onCommands;
    private ICommand2[] offCommands;

    public MainController() {
        this.onCommands = new ICommand2[3];
        this.offCommands = new ICommand2[2];
        ICommand2 emptyCommand = new EmptyCommand();
        for (int i = 0; i < 3; i++) {
            this.onCommands[i] = emptyCommand;
        }
        for (int i = 0; i < 2; i++) {
            this.offCommands[i] = emptyCommand;
        }
    }

    public void setCommand(int idx, ICommand2 onCommand, ICommand2 offCommand) {
        this.onCommands[idx] = onCommand;
        this.offCommands[idx] = offCommand;
    }

    public void executeOnCommand(int idx) {
        this.onCommands[idx].execute();
    }

    public void executeOffCommand(int idx) {
        this.offCommands[idx].execute();
    }
}

客戶端

MainController mainController = new MainController();
        Light roomLight = new Light();
        Light doorLight = new Light();
        LightOnCommand roomLightOnCommand = new LightOnCommand(roomLight);
        LightOffCommand roomLightOffCommand = new LightOffCommand(roomLight);
        LightOnCommand doorLightOnCommand = new LightOnCommand(doorLight);
        LightOffCommand doorLightOffCommand = new LightOffCommand(doorLight);

        mainController.setCommand(0, roomLightOnCommand, roomLightOffCommand);
        mainController.setCommand(1, doorLightOnCommand, doorLightOffCommand);

        mainController.executeOnCommand(0);
        mainController.executeOffCommand(0);
        mainController.executeOnCommand(1);
        mainController.executeOffCommand(1);
        mainController.executeOnCommand(2);

命令模式在單線程環境下的優點(使用場景)

通過封裝對方法的請求調用和方法執行過程,並將其分離,也就是所謂的完全解耦了。

故可以對方法的調用執行實現一些額外操作,比如記錄日誌,撤銷某個方法的請求調用,或者實現一次請求,N 次執行某個方法等。

在架構上,可以讓程序易於擴展新的請求(命令)。

命令模式在多線程程序中的優點

這樣做,在多線程環境下的好處是:

1、避免算法(策略)模塊執行緩慢拖累調用方——抽象了需要等待的操作

2、控制執行順序,因爲請求調用和具體執行分離,故執行順序和調用順序沒有關係

3、可以輕鬆實現請求的取消,或者反覆執行某個請求

4、請求調用和具體執行分離後,進一步把負責調用的機器和負責執行的機器分開,可以基於網絡,實現分佈式程序

命令的撤銷實現

前面,無論在什麼環境下,都提到了能撤銷命令(請求),故命令模式經常和備忘錄模式搭配使用。參考:保存快照和撤銷功能的實現方案——備忘錄模式總結

這裏舉一個很簡單的例子,還是電燈開關的例子

public interface ICommand3 {
    void execute();
    void undo(); // 和 execute 執行相反的操作
}
//////////////////////////////////
public class EmptyCommand implements ICommand3 {
    @Override
    public void execute() {
        System.out.println("什麼都不做");
    }

    @Override
    public void undo() {
        System.out.println("什麼都不做");
    }
}
/////////////////////////////////
public class LightOnCommand implements ICommand3 {
    private Light light;

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

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

    @Override
    public void undo() {
        this.light.noBlink();
        this.light.zoomout();
        this.light.off();
    }
}
///////////////////////////////////
public class LightOffCommand implements ICommand3 {
    private Light light;

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

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

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

控制器也要變化,初始化命令的同時,也要初始化 undo 命令

public class MainController {
    private ICommand3[] onCommands;
    private ICommand3[] offCommands;
    private ICommand3 undoCommand; // 記錄上一個命令

    public MainController() {
        this.onCommands = new ICommand3[3];
        this.offCommands = new ICommand3[2];
        ICommand3 emptyCommand = new EmptyCommand();
        for (int i = 0; i < 3; i++) {
            this.onCommands[i] = emptyCommand;
        }
        for (int i = 0; i < 2; i++) {
            this.offCommands[i] = emptyCommand;
        }
        this.undoCommand = emptyCommand; // 初始化 undo 命令
    }

    public void setCommand(int idx, ICommand3 onCommand, ICommand3 offCommand) {
        this.onCommands[idx] = onCommand;
        this.offCommands[idx] = offCommand;
    }

    public void executeOnCommand(int idx) {
        this.onCommands[idx].execute();
        this.undoCommand = this.onCommands[idx];
    }

    public void executeOffCommand(int idx) {
        this.offCommands[idx].execute();
        this.undoCommand = this.offCommands[idx];
    }

    public void undoCommand() {
        this.undoCommand.undo();
    }
}

客戶端

MainController mainController = new MainController();
        Light roomLight = new Light();
        LightOffCommand offCommand = new LightOffCommand(roomLight);
        LightOnCommand onCommand = new LightOnCommand(roomLight);
        mainController.setCommand(0, onCommand, offCommand);
        mainController.executeOnCommand(0);
        System.out.println();
        mainController.executeOffCommand(0);
        System.out.println();
        mainController.undoCommand();
        System.out.println();
        mainController.executeOffCommand(0);
        System.out.println();
        mainController.executeOnCommand(0);
        System.out.println();
        mainController.undoCommand();

打印如下

電燈打開
燈光變強
燈光閃爍

電燈關閉

電燈打開

電燈關閉

電燈打開
燈光變強
燈光閃爍

燈光停止閃爍
燈光變弱
電燈關閉

命令模式的缺陷

個人覺得,唯一的缺點就是會使得程序複雜性提高,但是我認爲微不足道,基礎紮實的 RD 應該無壓力閱讀和使用纔對,因爲在多線程程序裏,該模式大量出現,比如 Netty 等框架就大量使用了該思想。 

命令模式和策略模式的區別 

策略是不同的算法做同一件事情。不同的策略之間可以相互替換。比如實現一個支付功能,有微信支付,支付寶支付,各自渠道的支付。。。

命令是不同的命令做不同的事情。對外隱藏了具體的執行細節。比如菜單中的複製,移動和壓縮

JDK 中的命令模式

最最常見的就是 lang 包裏的 Runnable 接口,這就是一個命令接口,將對線程啓動的請求和具體的執行分離了。實現該接口,也是啓動線程推薦的寫法

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 

原文出處:https://www.cnblogs.com/kubixuesheng/p/10353809.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章