設計模式是軟件工程中一些問題的統一解決方案的模型,它的出現是爲了解決一些廣泛存在的,卻不能被語言特性直接解決的問題,隨着軟件工程的發展,設計模式也會不斷的進行更新,本文介紹的是經典設計模式-命令模式以及來自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》>
命令模式涉及到五個角色,它們分別是:
下面談一談命令模式的優缺點
命令模式的優勢是顯而易見的,解耦複合易擴展動態控制,簡直棒!可他的這個缺點彷佛有時候也挺頭疼的,那麼具體的進行分析與思考,命令模式的優勢幾乎所有集中於這個服務員類,也就是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以後,依舊只須要三個類!而且原先的優勢徹底保留了下來。
在整理關於方法引用轉換函數接口資料的時候突然以爲命令模式優化這裏彷佛能夠再稍微變更一下使得代碼呈現鏈式(以前也有過相似的想法,可是沒想到門路,今天想到了,就補充一下)
上文的代碼其實已經成型了,這裏作的優化是將屢次調用設置成鏈式調用法。
只須要將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方法鏈式的講訂單存入訂單隊列中。
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 ^_^