Java命令模式以及來自lambda的優化

前言

   設計模式是軟件工程中一些問題的統一解決方案的模型,它的出現是爲了解決一些廣泛存在的,卻不能被語言特性直接解決的問題,隨着軟件工程的發展,設計模式也會不斷的進行更新,本文介紹的是經典設計模式-命令模式以及來自java8的lambda的對它的優化。html

什麼是命令模式

命令模式把一個請求或者操做封裝到一個對象中。命令模式容許系統使用不一樣的請求把客戶端參數化,對請求排
隊或者記錄請求日誌,能夠提供命令的撤銷和恢復功能。 (摘自 <大話設計模式> ) java

  我不想把問題弄的特別複雜,個人理解是,命令模式就是對一段命令的封裝,而命令,就是行爲,行爲在java語言裏能夠理解爲方法,因此說命令模式就是對一些行爲或者說是一些方法的封裝。下面我舉一個簡單的例子來描述這種模式,而後再講解它的特色。git

例子

場景描述

  有一家路邊的小攤,作的小本經營,只有一個作飯的師傅,師傅飯作的還不錯,可來吃飯的人們老是抱怨老闆記性很差,有時候訂單一多就給忘了,路人甲:"老闆!來份牛肉飯!",老闆:"好勒!立刻給您作!",路人乙:"老闆!來份啤酒鴨!",老闆:"沒問題!",路人丙:"老闆!一份西紅柿炒雞蛋!",老闆:"ok!",路人丁:"老闆!兩份啤酒鴨!",老闆:"好的好的!"(心裏:我暈,怎麼有點記不過來了...)github

基礎實現

老闆在這裏同時扮演了作飯的角色 對於一個餐館來講也就是廚房類設計模式

public class Kitchen {
    public void beefRice(){
        System.out.println("一份牛肉飯作好了!");
    }

    public void scrambledEggsWithTomatoes(){
        System.out.println("一份西紅柿炒雞蛋作好了!");
    }

    public void beerDuck(){
        System.out.println("一份啤酒鴨作完啦!");
    }
}

客戶端代碼數據結構

public static void main(String[] args){
    Kitchen boss = new Kitchen();
    boss.beefRice();
    boss.beefRice();
    boss.beerDuck();
    boss.beerDuck();
    boss.beerDuck();
    boss.beefRice();
}

代碼十分的簡單,但是經過觀察發現,客戶和廚房直接交互了,若是一旦請求多了起來,就如同上面老闆說的,怎麼感受頭有點暈那...從代碼的角度來講,這樣的耦合度也過高了,舉個例子,若是如今有一個客戶不想點了,那應該怎麼辦呢?取消訂單代碼寫在Kitchen類裏,顯然不現實,寫在客戶端裏,彷佛又不太靠譜.... 再舉一個例子,雞蛋已經用完了,不能給客戶提供西紅柿炒雞蛋了,那這個拒絕的代碼又寫在哪裏呢?寫在Kitchen?能夠是能夠,可是你把業務邏輯和基礎領域模型混在一塊兒真的好嗎?寫在客戶端裏?全部的邏輯都丟在客戶端第一層顯然是不明智的。ide

廢話了半天,終於有一個顧客受不了了:"老闆你這樣也太累了吧!不如招個服務員給你記咱們要點的飯的訂單,記完了送到廚房給你,你按照這個訂單作就好了,這樣你全身心的投入作飯,這樣能提升作飯的效率,也不會出現先點的顧客您忘記作等了半天嚷嚷着要退錢了!至於取消修改訂單仍是拒絕訂單,都交給服務員去處理,處理好了送過來給您,這樣各司其職,效率變高了,你們不是都開心嘛!"函數

那麼很顯然故事中的這個顧客就頗有軟件工程的天賦 :)優化

使用傳統命令模式實現

命令模式,就是上文中的顧客所提出的建議,下面就是具體的實現類。this

  • 首先要添加服務員類,負責添加,拒絕,修改或者刪除訂單,或者增長訂單的日誌相關信息,其實很是簡單,簡單的增長邏輯便可,這裏爲了演示,只演示拒絕訂單和輸出日誌
  • 其次既然是命令模式,那確定要有命令類,這裏將廚房作飯的三個方法分別構形成三個命令對象,將他們的共同部分抽象成公共的抽象命令類,方便後面向上轉型。

如下爲代碼
抽象命令類,包含一個執行命令的對象和方法,子類繼承該類,對抽象的實現方法進行不一樣的實現

public abstract class BaseCommand {
    protected Kitchen kitchen;

    public BaseCommand(Kitchen kitchen) {
        this.kitchen = kitchen;
    }

    public abstract void executeCommand();
}

下面是三個繼承抽象命令類的具體命令,也就是未來客戶端要添加的具體的獨立的訂單命令,結構徹底同樣

作牛肉飯命令類

public class beefRiceCommand extends BaseCommand {

    public beefRiceCommand(Kitchen kitchen) {
        super(kitchen);
    }

    @Override
    public void executeCommand() {
       kitchen.beefRice();
    }
}

作啤酒鴨命令類

public class beerDuckCommand extends BaseCommand {

    public beerDuckCommand(Kitchen kitchen) {
        super(kitchen);
    }

    @Override
    public void executeCommand() {
       kitchen.beerDuck();
    }
}

作西紅柿炒雞蛋命令類

public class EggsWithTomatoesCommand extends BaseCommand {

    public EggsWithTomatoesCommand(Kitchen kitchen) {
        super(kitchen);
    }

    @Override
    public void executeCommand() {
        kitchen.scrambledEggsWithTomatoes();
    }
}

下面是服務員類,這裏爲了演示使用隊列實現了增長訂單與拒絕訂單和添加日誌,刪除中間訂單也很簡單,數據結構換成linkedList或者arrayList便可,拒絕訂單爲了演示只是簡單的經過類名來判斷

public class Waiter {
    /** 用於存儲訂單命令的隊列 */
    private final Queue<BaseCommand> orders ;

    public  Waiter() {
        orders = new ArrayDeque<>();
    }

    /**
     * 添加訂單
     * @param baseCommand 客戶端傳來的命令類,向上轉型
     */
    public final void setOrders(BaseCommand baseCommand){
        if (baseCommand.getClass().getName().equals(EggsWithTomatoesCommand.class.getName())) {
            System.out.println("啤酒鴨賣完了,換一個點點吧!");
        } else {
            String[] names = baseCommand.getClass().getName().split("\\.");
            System.out.printf("添加訂單: %s 訂單時間: %s \n", names[names.length - 1], LocalDateTime.now());
            orders.add(baseCommand);
        }
    }

    /**
     * 通知廚房開始作飯,遍歷隊列,作完了的訂單移出隊列
     */
    public final void notifyKitchen(){
        while (orders.peek() != null) {
            orders.peek().executeCommand();
            orders.remove();
        }
    }
}

客戶端類

public class Client {
    public static void main(String[] args) {
        //準備廚房,服務員,菜單命令工做
        Kitchen kitchen = new Kitchen();
        Waiter waiter = new Waiter();
        BaseCommand beefRiceCommand = new beefRiceCommand(kitchen);
        BaseCommand beerDuckCommand = new beerDuckCommand(kitchen);
        BaseCommand eggsWithTomatoesCommand = new EggsWithTomatoesCommand(kitchen);

        //開始營業
        System.out.println("=======================添加訂單環節=======================");
        // 顧客:服務員 一份牛肉飯!
        waiter.setOrders(beefRiceCommand);
        // 顧客:服務員 一份啤酒鴨!
        waiter.setOrders(beerDuckCommand);
        // 顧客:服務員 一份西紅柿炒雞蛋!
        waiter.setOrders(eggsWithTomatoesCommand);
        // 顧客:服務員 兩份啤酒鴨!
        waiter.setOrders(beerDuckCommand);
        waiter.setOrders(beerDuckCommand);

        System.out.println("==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========");
        //服務員通知廚房按照訂單順序開始作
        waiter.notifyKitchen();

    }
}

運行結果

=======================添加訂單環節=======================
添加訂單: beefRiceCommand 訂單時間: 2017-10-16T02:44:13.631 
添加訂單: beerDuckCommand 訂單時間: 2017-10-16T02:44:13.650 
啤酒鴨賣完了,換一個點點吧!
添加訂單: beerDuckCommand 訂單時間: 2017-10-16T02:44:13.650 
添加訂單: beerDuckCommand 訂單時間: 2017-10-16T02:44:13.650 
==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========
一份牛肉飯作好了!
一份啤酒鴨作完啦!
一份啤酒鴨作完啦!
一份啤酒鴨作完啦!

Process finished with exit code 0

總結與思考

總結

上面的例子應該並不難理解,這裏列出命令模式的uml圖<來源於《head first》>

命令模式涉及到五個角色,它們分別是:

  • 客戶端(Client)角色:具體執行程序的地方,建立一個具體命令(ConcreteCommand)對象並肯定其接收者。
  • 命令(Command)角色 : 聲明瞭一個給全部具體命令類的抽象接口,在上面的例子中是BaseCommand類,這裏的抽象接口只是一個概念,我使用抽象類來代替也是能夠的。
  • 具體命令(ConcreteCommand)角色 : 定義一個接收者和行爲之間的弱耦合;實現execute()方法,負責調用接收者的相應操做。execute()方法一般叫作執行方法。在這裏就是牛肉飯,啤酒鴨,番茄炒雞蛋等命令....
  • 請求者(Invoker)角色:負責調用命令對象執行請求,相關的方法叫作行動方法。在這裏就是服務員,負責收集這些命令,從uml圖也能夠看出來他和command角色的關係是聚合關係
  • 接收者(Receiver)角色:負責具體實施和執行一個請求。任何一個類均可以成爲接收者,實施和執行請求的方法叫作行動方法。在這裏就是廚房了,其實我更喜歡稱之爲執行者,由於他是最終負責執行這些命令的人,固然反過來講,對於命令來講,他也是這些命令的接收者。

下面談一談命令模式的優缺點

優勢

  • 最大的做用是將請求者與執行者分割開,在上面的例子中就是將顧客和廚房給分開了,顧客負責向服務員點菜,服務員負責將訂單交給廚房,廚房安心作飯,顧客安心點菜,這樣分工明確。
  • 在有須要的狀況下,能夠很方便的進行日誌的記錄,如上面的例子所示
  • 對於客戶端的請求,能夠選擇撤銷請求與從新記錄請求,也是十分方便的
  • 能夠十分容易的實現一個命令隊列,例如上文所示
  • 每個命令類互不關聯,添加新的命令類或者修改舊有的命令類十分容易
  • 總結幾點就是 解耦,複合命令,動態的控制,易於擴展

缺點

  • 增長的類太多了,事實上不少設計模式都有這樣的毛病,與其說是缺點,不如說是不可避免,由於程序語言特性的缺陷不得不用更多資源來變着法子完成,就拿上文的例子來講,最初的基本實現雖然問題不少,可是隻有兩個類,廚房類和客戶端類,可以使用命令模式完成了擴展以後,除開原先的兩個類,新增了服務員類,抽象命令類,以及它下面具體的三個命令,那麼假設這個廚房類能夠作100道菜,難道我就要加100個子命令類???

思考

命令模式的優勢是顯而易見的,解耦複合易擴展動態控制,簡直棒!可他的這個缺點彷佛有時候也挺頭疼的,那麼具體的進行分析與思考,命令模式的優勢幾乎所有集中於這個服務員類,也就是invoke角色裏,而剩下的1個抽象接口與它下面的n個子類只是爲了將廚房類(receiver角色)裏的每個行爲(方法)給抽象出來。問題來了,爲何要用類去包裝這個行爲才能抽象呢?事實上,行爲抽象是函數式語言的特性之一,java此前並無這個語言特性,因此沒辦法,只能用單獨新增一個類來包裹這個行爲來代替,但是這一點自從Java8出來以後就不同了,Java8的函數式特性徹底能夠將命令模式的這一缺點給優化掉!所以,咱們只須要保留服務員類,剩餘的行爲命令包裝類使用嘗試使用函數接口來代替,這樣既保持了優勢,又規避了缺點!

使用函數抽象行爲進行優化

將這些作飯的命令抽象成函數接口,而後指定一個執行者,這樣的接口在Java8 4大函數接口中屬於Consumer接口,也就是消費者接口,下面就用Consumer接口進行行爲抽象
廚房類依舊不變

public class Kitchen {
    public void beefRice(){
        System.out.println("一份牛肉飯作好了!");
    }

    public void scrambledEggsWithTomatoes(){
        System.out.println("一份西紅柿炒雞蛋作好了!");
    }

    public void beerDuck(){
        System.out.println("一份啤酒鴨作完啦!");
    }
}

服務員類也很容易,原先裝的是一個個命令對象,如今直接一點,直接將行爲放進去

public class Waiter {
    /**
     * 此時隊列裝載的再也不是命令對象了,而是更直接的廚房類的行爲
     */
    private final Queue<Consumer<Kitchen>> orders;

    public Waiter() {
        orders = new ArrayDeque<>();
    }

    /**
     * 添加訂單
     * @param kitchenAction 廚房的具體行爲
     */
    public final void setOrders(Consumer<Kitchen> kitchenAction) {
        System.out.printf("添加訂單成功! 訂單時間: %s \n", LocalDateTime.now());
        orders.add(kitchenAction);
    }

    /**
     * 這裏增長一個執行者參數,來對隊列中的行爲進行操做
     * @param kitchen 執行者,用於執行隊列中的行爲
     */
    public final void notifyKitchen(Kitchen kitchen) {
        while (orders.peek() != null) {
            orders.peek().accept(kitchen);
            orders.remove();
        }
    }
}

客戶端代碼,簡單、清晰的驚人,代碼自帶註釋效果,不管是簡短性仍是可閱讀性,都比以前的要好上不少,中間的營業代碼很直觀,直接閱讀代碼就很清楚的看到添加了哪些行爲到訂單隊裏中

public class Client {
    public static void main(String[] args) {
        //準備廚房,服務員,菜單命令工做
        Kitchen kitchen = new Kitchen();
        Waiter waiter = new Waiter();

        //開始營業
        System.out.println("=======================添加訂單環節=======================");
        // 顧客:服務員 一份牛肉飯!
        waiter.setOrders(Kitchen::beefRice);
        // 顧客:服務員 一份啤酒鴨!
        waiter.setOrders(Kitchen::beerDuck);
        // 顧客:服務員 一份西紅柿炒雞蛋!
        waiter.setOrders(Kitchen::scrambledEggsWithTomatoes);
        // 顧客:服務員 兩份啤酒鴨!
        waiter.setOrders(Kitchen::beerDuck);
        waiter.setOrders(Kitchen::beerDuck);

        System.out.println("==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========");
        //服務員通知廚房按照訂單順序開始作
        waiter.notifyKitchen(kitchen);

    }
}

輸出結果

=======================添加訂單環節=======================
添加訂單成功! 訂單時間: 2017-10-16T03:19:40.003 
添加訂單成功! 訂單時間: 2017-10-16T03:19:40.019 
添加訂單成功! 訂單時間: 2017-10-16T03:19:40.020 
添加訂單成功! 訂單時間: 2017-10-16T03:19:40.020 
添加訂單成功! 訂單時間: 2017-10-16T03:19:40.021 
==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========
一份牛肉飯作好了!
一份啤酒鴨作完啦!
一份西紅柿炒雞蛋作好了!
一份啤酒鴨作完啦!
一份啤酒鴨作完啦!

Process finished with exit code 0

到這裏,已經沒有其餘類的代碼了!類的數量由原先的7個變成了3個,而且因爲服務員類(Invoke角色)依然存在,原先的解耦複合控制擴展等優勢,一個都沒少,與此同時客戶端的代碼也清爽了很多。試想一下,假如如今廚房有100道作菜的方法,按照原先的方法實現的類的數量應該是3(客戶端+廚房+服務員) + 1(抽象命令接口) + 100(具體命令接口) = 104個類,而採用lambda以後,依舊只須要三個類!而且原先的優勢徹底保留了下來。

函數補充優化(update 17.10.24)

在整理關於方法引用轉換函數接口資料的時候突然以爲命令模式優化這裏彷佛能夠再稍微變更一下使得代碼呈現鏈式(以前也有過相似的想法,可是沒想到門路,今天想到了,就補充一下)
上文的代碼其實已經成型了,這裏作的優化是將屢次調用設置成鏈式調用法。

鏈式調用一

只須要將waiter類的setOrders方法的返回值設置爲this自己便可。

public final Waiter setOrders(Consumer<Kitchen> kitchenAction) {
        System.out.printf("添加訂單成功! 訂單時間: %s \n", LocalDateTime.now());
        orders.add(kitchenAction);
        return this;
    }

客戶端鏈式調用

public class Client {
    public static void main(String[] args) {
        //準備廚房,服務員,菜單命令工做
        Kitchen kitchen = new Kitchen();
        Waiter waiter = new Waiter();
        //開始營業
        System.out.println("=======================添加訂單環節=======================");
        // 顧客:服務員 一份牛肉飯!
        // 顧客:服務員 一份啤酒鴨!
        // 顧客:服務員 一份西紅柿炒雞蛋!
        // 顧客:服務員 兩份啤酒鴨!
        // 顧客:服務員 兩份啤酒鴨!
        //這裏經過setOrders完成鏈式調用,和以前的效果徹底同樣,可是提升了可讀性與間接性
        waiter.setOrders(asConsumer(Kitchen::beefRice))
                .setOrders(Kitchen::beerDuck)
                .setOrders(Kitchen::scrambledEggsWithTomatoes)
                .setOrders(Kitchen::beerDuck)
                .setOrders(Kitchen::beerDuck);
        
        System.out.println("==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========");
        //服務員通知廚房按照訂單順序開始作
        waiter.notifyKitchen(kitchen);
    }
}

輸出結果和上面同樣,這裏再也不顯示

鏈式調用二

這裏是經過一個方法引用的包裝器,而後經過function接口的andThen方法鏈式的講訂單存入訂單隊列中。

  • 這裏與上文的鏈式優化一的是有區別的,區別在於,上文中不管是普通的setOrders仍是return this 鏈式的setOrders的 日誌信息是會輸出5條的(進行了5次的setOrders動做),而這裏只會輸出一條,緣由是這裏的一條動做裏andThen了4個動做,這一條動做鏈做爲一個總體傳入了Setorders,所以只會有一條日誌信息輸出。
  • 至於何時使用,根據實際狀況,例如通知廚房作一道便當快餐,而便當快餐假設是由一份牛肉飯+一份啤酒鴨構成的,那麼在這裏這一個訂單就可使用這樣的andThen來進行構造,這樣的鏈式組合實際上是有序的,其實這就是建造者模式的lambda表示形式。
  • 使用這樣的方法,你的設計裏就是包含了命令模式+建造者模式,以lambda形式表現出來,固然若是不按照順序的話,這樣的模式你還能夠理解成組合的形式。

lambda的方法引用當然使得代碼清晰可見,可是壞處是一旦使用了方法引用,就沒法進行lambda的鏈式調用,也就是沒法使用andThen這樣的鏈式方法,可是咱們經過一箇中轉站,先將方法引用轉換成函數接口,再鏈式調用,由於方法引用本質上是lambda的語法糖,下面是轉換方法,十分簡易。

public class FunctionCastUtil {
    public static <T> Consumer<T> asConsumer(Consumer<T> consumer) {
        return consumer;
    }
}

客戶端代碼

import static com.lambda.functionutils.FunctionCastUtil.*;

public class Client {
    public static void main(String[] args) {
        //準備廚房,服務員,菜單命令工做
        Kitchen kitchen = new Kitchen();
        Waiter waiter = new Waiter();
        //開始營業
        System.out.println("=======================添加訂單環節=======================");
        // 顧客:服務員 一份牛肉飯!
        // 顧客:服務員 一份啤酒鴨!
        // 顧客:服務員 一份西紅柿炒雞蛋!
        // 顧客:服務員 兩份啤酒鴨!
        // 顧客:服務員 兩份啤酒鴨!
        //這裏使用anThen完成鏈式調用,注意這裏是只有一條訂單,因此要注意使用的場合
        waiter.setOrders(asConsumer(Kitchen::beefRice)
                        .andThen(Kitchen::beerDuck)
                        .andThen(Kitchen::scrambledEggsWithTomatoes)
                        .andThen(Kitchen::beerDuck)
                        .andThen(Kitchen::beerDuck));

        System.out.println("==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========");
        //服務員通知廚房按照訂單順序開始作
        waiter.notifyKitchen(kitchen);
    }
}

由於輸出結果不一樣,因此這裏貼一下輸出結果,根據輸出結果就明白這裏的訂單隻記錄了一條,這一條裏面包含了要作多種菜,在廚房那裏實際完成的結果是同樣的,你們能夠根據不一樣的須要使用不一樣的鏈式優化

=======================添加訂單環節=======================
添加訂單成功! 訂單時間: 2017-10-24T07:01:59.444 
==========服務員將訂單送至廚房,廚房按照訂單順序開始作飯=========
一份牛肉飯作好了!
一份啤酒鴨作完啦!
一份西紅柿炒雞蛋作好了!
一份啤酒鴨作完啦!
一份啤酒鴨作完啦!

Process finished with exit code 0

結尾

   使用lambda優化以後的命令模式在保證優勢的同時極大的減小了代碼量,簡直完美。這就是語言特性所帶來的力量,簡單的幾處修改就能得到如此多的受益,這也是我爲何這麼喜歡lambda的緣由。

關於本文代碼

本文的代碼與md文章同步更新在github中的command-mode模塊下,歡迎fork ^_^

下一篇:java策略模式以及來自lambda的優化

相關文章
相關標籤/搜索