Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計

Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計

Overview of Content

本文將深入介紹命令模式(Command Pattern),該模式是一種行為設計模式,用於將請求封裝為一個對象(物件),從而允許客戶端參數化操作。我們將從命令模式的基本概念入手,探討其使用場景以及如何定義和實現命令模式

此外,我們還將簡單討論命令模式的優缺點,並介紹了一些相關的設計原則和拓展概念,例如迪米特法則和依賴倒置原則

最後,我們會對指令模式進行實際的應用案例分析,以及一些最佳化和拓展方法的討論。 這篇文章旨在幫助讀者全面了解命令模式的概念、用途和實作方式,以及在實際專案中如何應用命令模式來提高程式碼的可維護性和擴展性。


Command 模式 - 概述

Command 命令模式是一種行為模式設計

可以用於封裝一系列的操作,而使用者只須要執行一個方法(調用者),而不必知道細節(執行者)

這有點類似於 Factory 設計 ?

  1. Factory 設計 也會封裝細節不讓使用者知道,但 Command 設計 的封裝自由度更高,會把行為(執行者)拆分,並自由讓使用者調整順序
  2. 兩者個目的不同,Command 模式 目標是修改行為(呼叫者跟執行者解耦),Factory 設計 則是創建一個對象

這有點類似於 Template 設計 ?

  • Template 設計 會按照一個模板,來操作一系列 固定 的行為,自由度較低,Command 設計 則較為自由,可隨意調整順序

Command 使用場景

● Command 命令模式 特性是高內聚,減少使用者使用類的成本

● 對請求排入佇列 (Queue),需要儲存命令的 Log 日誌

● 透過 呼叫者 Invoker執行者 RealControl 進行解偶,對每一個方法封裝給呼叫者使用,使用者不會直接控制到執行者

Command 定義 & Command UML

● Command 模式定義

請求裝成一個 對象(物件),讓用戶使用不同的請求,把客戶端請求參數化;既然參數化,那就可以把請求進行調整,像是排序、撤銷

● Command UML 核心重點類

關係其他
Invoker給使用者使用,需要 注入包裝後的方法,用於包裝命令可記錄方法調用
ICommand(抽象方法)抽象執行方法參數化 RealControl 的行為
ConcreateCommand實做命令(並不是實做,只是命令的實做),會對應 1~ 多個 RealControl 類可做加強,紀錄時間等等
RealControl邏輯的底層程式也稱為接收者 (Receiver)

在標準的 Command 模式下 User 會有兩種行為

  1. 手動創建實際命令 RealControl 類 的實體(instance
  2. 再將命令注入到 Invoker 類

Command 優缺點

Command 設計的優點

● 對於方法的調用屬於弱耦合、擴充性、靈活性更強,可以依據需求隨意調整調動

Command 設計的缺點

● 很明顯的類的膨脹(基本上是所有設計模式的通病)

● 它會加重程式的複雜度,需要思考一下業務需求是否有需要這樣設計,避免設計過度的問題


Command 實現

Command 標準實現

抽象調用 ICommand 接口:將執行(調用)動作抽象化,最終每個行為會成為一個參數,並讓 Invoker 聚合、操作這些行為 (參數)

命令參數化的基礎上再將其抽象化

// ICommand.java

public interface ICommand {

    void execute();

}

調用者 ConcreteCommand1. 實作執行這個行為 Function,2. 內部存有實際邏輯(執行者)的對象 RealControl,但並不是實際邏輯(不是實際執行者,只是實際調用者)

這個類代表了 命令參數化 的實做!

這一層把真正的命令包裝起來,可以控制命令真正執行的時間、執行方式、運行環境… 等等!

public class ConcreteCommand implements ICommand {

    private final RealControl realControl;

    public ConcreteCommand(RealControl realControl) {
        this.realControl = realControl;
    }

    @Override
    public void execute() {
        realControl.action();
    }

}

執行者 RealControl:實際邏輯 (具體操作行為)

public class RealControl {

    public void action() {
        System.out.println("Real Controller");
    }
}

Invoker:聚合 1 ~ 多個 ICommand 對象(物件),控制多個行為 (操作順序),可以有預設行為,也可以由使用者來加載自己所需的行為(ICommand

public class Invoker {

    private final List<ICommand> commandList = new ArrayList<>();

    public void addCommand(ICommand iCommand) {
        commandList.add(iCommand);
    }

    public void start() {
        for(ICommand cmd : commandList) {
            cmd.execute();
        }
    }

}

Command 優化:迪米特、依賴倒置

Command 設計模式優化的目標有如下

迪米特:減少使用者對於RealControl類的認知

依賴倒置AbstactCommand也就是抽象類,讓其依賴於抽象RealControl而不是實體ConcreteRealControl

這裡依照最基本的 Command 設計實作一個 RPG Game 的上、下、左、右操作

A. RealControl 抽象:符合依賴倒置原則,抽象化實做指令(抽象化執行者)

// RealControl.java

public abstract class GameControl {
    
    public abstract void action();

}

B. ConcreteRealControl:實際指令 實作細節

public class TurnRight extends GameControl {

    @Override
    public void action() {
        System.out.println("Right");
    }

}

// -------------------------------------------------------
public class TurnLeft extends GameControl {

    @Override
    public void action() {
        System.out.println("Lift");
    }

}

// -------------------------------------------------------
public class TurnUp extends GameControl {

    @Override
    public void action() {
        System.out.println("Up");
    }

}

// -------------------------------------------------------
public class TurnDown extends GameControl {

    @Override
    public void action() {
        System.out.println("Down");
    }

}

C. AbstractCommand 抽象類:簡單的抽象化一個行為(抽象化調用者),準備 參數化 GameControl 的行為(也就是 RealControl 角色)

// ICommand.java

public abstract class AbstractCommand {

    protected GameControl gameControl;

    public AbstractCommand(GameControl gameControl) {
        this.gameControl = gameControl;
    }

    // 走的步數
    public void execute(long step) {
        System.out.println("----------- Start command");

        while (step-- > 0) {    // 閉區間,0 ~ (step -1)
            exec(gameControl);
        }
    }

    // 抽象化調用者
    abstract void exec(GameControl control);

}

D. ConcreteCommand:針對每個指令 (上、下、左、右) 實作,每個 指令的參數化(實際的調用者),並直接寫入實做類

// ConcreteCommand.java

// 左轉
public class LeftCommand extends AbstractCommand {

    public LeftCommand() {
        super(new TurnLeft());
    }

    @Override
    public void exec(GameControl control) {
        control.turnLeft();
    }

}

// ------------------------------------------------------------
// 右轉
public class RightCommand extends AbstractCommand {

    public RightCommand() {
        super(new TurnRight());
    }

    @Override
    public void exec(GameControl control) {
        control.turnRight();
    }

}

// ------------------------------------------------------------
// 向上
public class UpCommand extends AbstractCommand {

    public UpCommand() {
        super(new TurnUp());
    }

    @Override
    public void exec(GameControl control) {
        control.turnUp();
    }

}

// ------------------------------------------------------------
// 向下
public class DownCommand extends AbstractCommand {

    public DownCommand() {
        super(new TurnDown());
    }

    @Override
    public void exec(GameControl control) {
        control.turnDown();
    }

}

E. Invoker聚合 所有要操作的指令,透過 Invoker 操作實際命令

  • 符合迪米特原則(最少知識):不用讓使用者知道真正個實做類,只須知道某個指令
  • 這裡使用外部 DI 指令,當然 我們也可以把指令直接創建在 Invoker 之內
public class Invoker {

    private final Map<String, AbstractCommand> map = new HashMap<>();

    public Invoker(AbstractCommand right, AbstractCommand left, AbstractCommand up, AbstractCommand down) {
        map.put("Right", right);
        map.put("Left", left);
        map.put("Up", up);
        map.put("Down", down);
    }

    // 指令化
    public void turnRight(long step) {
        AbstractCommand right = map.get("Right");

        if(right == null) {
            return;
        }

        right.execute(step);
    }

    // 指令化
    public void turnLeft(long step) {
        AbstractCommand left = map.get("Left");

        if(left == null) {
            return;
        }

        left.execute(step);
    }

    // 指令化
    public void turnUp(long step) {
        AbstractCommand up = map.get("Up");

        if(up == null) {
            return;
        }

        up.execute(step);
    }

    // 指令化
    public void turnDown(long step) {
        AbstractCommand down = map.get("Down");

        if(down == null) {
            return;
        }

        down.execute(step);
    }

}

作為 User 使用 Command 設計模式:創建實作類,再創建 Invoker,將實作類注入 Invoker

public class MainCommand {

    public static void main(String[] args) {

        // 將實作類注入 Invoker
        Invoker invoker = new Invoker(
                new RightCommand(),
                new LeftCommand(),
                new UpCommand(),
                new DownCommand());

        invoker.turnDown(3);
        invoker.turnUp(1);
        invoker.turnLeft(2);
        invoker.turnRight(5);
    }

}

Command 拓展:添加紀錄

● 若要紀錄 Command 也十分簡單,由於我們參數化具體命令,所以只需要在 User 操做的 Invoker 類添加 LinkedList 做紀錄即可

A. 首先先修改 ICommandAbstractCommand)抽象類,多出一個共用 Function,用來記錄上次移動的 Step

// AbstractCommand.java

public abstract class AbstractCommand {

    private long step;

    protected GameControl gameControl;

    public AbstractCommand(GameControl gameControl) {
        this.gameControl = gameControl;
    }

    public void execute(long step) {
        // 紀錄最後一次移動的步數
        this.step = step;

        System.out.println("----------- Start command");

        while (step-- > 0) {
            exec(gameControl);
        }
    }

    abstract void exec(GameControl control);

    // 新增 Function
    public void doLastAgain() {
        execute(step);
    }

}

B. 在 Invoker 內添加 LinkedList 做紀錄(也可以用來排隊、儲存)

使用 LinkedList 可以按照順序做 Step 移動紀錄(這個數據結構對於動態操作元素的行為較為友善,效率較高),也可以作為 Log 紀錄

// Invoker.java

public class Invoker {

    private final LinkedList<AbstractCommand> record = new LinkedList<>();

    private final Map<String, AbstractCommand> map = new HashMap<>();

    public Invoker(AbstractCommand right, AbstractCommand left, AbstractCommand up, AbstractCommand down) {
        map.put("Right", right);
        map.put("Left", left);
        map.put("Up", up);
        map.put("Down", down);
    }

    ... 省略上、下、左、右操作

    public void showRecordStep() {
        AbstractCommand tmp = record.getFirst();
        while (tmp != null) {
            tmp.doLastAgain();
            tmp = record.poll();
        }
    }

Servant 模式 - 概述

Servant 雇工(僕人)模式其實就是 命令模式的簡化版本,它會為使用者提供一組通用的功能


Servant UML

● Servant UML

Servant 類Command 設計類說明
ServantInvoker聚合所有的實做類
IServicedICommand提供共用的功能界面
ConcreteServicedRealControl真正個實做類
-ConcreateCommandServant 設計會省略這個類,讓 Servant(Invoker)直接調用真正實做

● 比起命令模式,雇工模式的 Servant 可以調用到真正的實做類,命令模式中 Invoker 則是調用到 ConcreateCommand 的實做


Servant 實現

Servant 標準實現

A. IServiced 界面

該界面代表對外提供統一的合約界面,類似於策略模式的抽象界面,它代表了一個原子單元(函數)


interface IServiced {

    fun serviced()

}

B. ConcreteServiced

實做類,會依據不同方式來達成業務需求


class FanServiced: IServiced {

    override fun serviced() {
        println("Open Fan ~ ")
    }

}

class AirConditionServiced: IServiced {

    override fun serviced() {
        println("Open Air Condition ~ ")
    }

}

C. Servant

該類就是命令模式的調用者類(Invoker),它會會聚多個 IServiced,對外提供使用者一組通用的功能


class CoolDownServant {

    enum class CoolDownWay constructor(val serviced: IServiced) {
        FAN(FanServiced()),
        AIR_CONDITION(AirConditionServiced())
    }

    fun serviced(way: CoolDownWay) {
        way.serviced.serviced()
    }
}

● 使用 Servant 類:

使用者只須面向 Servant 類就可以使用所有的功能


fun main() {
    CoolDownServant().apply {

        serviced(CoolDownServant.CoolDownWay.FAN)

        serviced(CoolDownServant.CoolDownWay.AIR_CONDITION)

    }
}

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響