Command

January 1, 2022

如果你負責撰寫測試用的工具程式,一開始你提供了以下的工具:

public class Assert {
    public static void assertEquals(int expected, int result) {
        if(expected == result) {
            System.out.println("正確!");
        }
        else {
            System.out.printf(
               "失敗,預期為 %d,但是傳回 %d!%n", expected, result);
        }
    }
}

這可以讓同事撰寫以下的測試:

public class CalculatorPlusTest {
    public static void main(String[] args) {
        var calculator = new Calculator();
        var expected = 3;
        var result = calculator.plus(1, 2);
        Assert.assertEquals(expected, result);
    }
}

接著,同事在 Calculator 類別中新增了 minus 方法,並撰寫另一個測試:

public class CalculatorMinusTest {
    public static void main(String[] args) {
        var calculator = new Calculator();
        var expected = 1;
        var result = calculator.minus(3, 2);
        Assert.assertEquals(expected, result);
    }
}

現在要執行測試的話,他得分別運行 CalculatorPlusTestCalculatorMinusTest,同事希望有個執行器,可以收集已建立的測試案例並執行。

定義/執行

同事的測試案例會怎麼寫,你當然一無所知,一無所知表示實作不明,那就定義了抽象的 Test 吧!

public interface Test {
    void test();
}

你只相依在抽象的 Test,現在可以寫個 TestRunner

public class TestRunner {
    private List<Test> tests = new ArrayList<>();

    public void add(Test test) {
        tests.add(test);
    }
    public void run() {
        for(Test test : tests) {
            test.run();
        }
    }
}

同事如果要撰寫測試案例,就實作 Test 介面,例如:

public class CalculatorPlusTest implements Test {
    @Override
    public void run() {
        var calculator = new Calculator();
        var expected = 3;
        var result = calculator.plus(1, 2);
        Assert.assertEquals(expected, result);
    }
}

不管有幾個測試案例,總之就是使用你的 TestRunner 來收集並執行:

public class CalculatorTest {
    public static void main(String[] args) {
        TestRunner runner = new TestRunner();
        runner.add(new CalculatorPlusTest());
        runner.add(new CalculatorMinusTest());
        runner.run();
    }
}

在這個情境中,同事定義測試案例,你撰寫的 TestRunner 執行測試案例,定義與執行是分離的,Gof 將此概念命名為 Command 模式。

好像很常見?

嗯?怎麼覺得有點熟悉?不!不是熟悉,這不是到處都看得到嗎?例如執行緒程式設計中,不是就定義 Runnable 嗎?

// Runnable 是 Funnctional interface,直接寫 lambda 表示式
Runnable runnable = () -> {
	你的流程
};
new Thread(runnable).start();

難道這也實現了 Command?是啊!你定義指令(Runnable),執行指令(Runnable)的是 Thread。那麼事件處理呢?你可以自定義事件處理器,發生事件時元件會執行相對應的方法也是嗎?是啊!…照這樣的說法,那麼不就任何可以自定義指令,由另一個角色執行指令的設計,都算是 Command 的實現?基本上就是如此!

先前的文件中,也早就有過一些例子了,例如,〈Composite〉的範例中,MaterialPlaylist 就組合來看是 Composite,然而就「定義/執行」的行為關係,就是 Command 的實現。

當然,也可以有其他角度的看法,例如,某個設計就「定義/執行」的行為關係是 Command 的實現,然而就「抽換策略」的行為來看,或許就是 Strategy,然後就「XX」的角度來看,可能又是 GGYY 模式…模式有時候想表達的是一種思考角度,有時候甚至想傳達的是某個問題情境。

那那那…Gof 中那個什麼 Invoker、Receiver 就不用理它們了嗎?Gof 中確實是用了一些類別圖之類,來解釋他們使用的範例中具有哪些角色,不過也因此造成很多人誤以為,必須有那些類別圖裡出現的角色,才能稱為是某模式…那些又不是 DIY 組裝說明書…看著圖組裝就能有好的設計…嗯…你是不是搞錯了什麼!?

這也是為何我在漫談模式時,特意不畫圖的原因了,也特意不提供可完整運行的程式碼…有些人容易因此而誤會,也容易因此只看最後的實現成果,而是過程中我聊了些什麼。

你應該擺在觀察、釐清需求,確認想實現什麼目的,在看模式相關的文件或書時,記得!範例只是提供一個情境,讓你可以從中可以怎麼觀察、釐清需求、確認目的,從而知道某語言可以如何實現罷了…