命令模式(三)
隊列請求
所謂隊列請求,就是對命令對象進行排隊,組成工作隊列,然後依次取出命令對象來執行。多用多線程或者線程池來進行命令隊列的處理,當然也可以不用多線程,就是一個線程,一個命令一個命令的循環處理,就是慢點。
繼續宏命令的例子,其實在後廚,會收到很多很多的菜單,一般是按照菜單傳遞到後廚的先後順序來進行處理,對每張菜單,假定也是按照菜品的先後順序進行制作,那麽在後廚就自然形成了一個菜品的隊列,也就是很多個用戶的命令對象的隊列。
後廚有很多廚師,每個廚師都從這個命令隊列裏面取出一個命令,然後按照命令做出菜來,就相當於多個線程在同時處理一個隊列請求。
因此後廚就是一個很典型的隊列請求的例子。
下面就一起來看看如何實現隊列請求。
1:如何實現命令模式的隊列請求
(1)先從命令接口開始,除了execute方法外,新加了一個返回發出命令的桌號,就是點菜的桌號,還有一個是為命令對象設置接收者的方法,也把它添加到接口上,這個是為了後面多線程處理的時候方便使用。示例代碼如下:
/** * 命令接口,聲明執行的操作 */ public interface Command { /** * 執行命令對應的操作 */ public void execute(); /** * 設置命令的接收者 * @param cookApi 命令的接收者 */ public void setCookApi(CookApi cookApi); /** * 返回發起請求的桌號,就是點菜的桌號 * @return 發起請求的桌號 */ public int getTableNum(); }
(2)廚師的接口也發生了一點變化,在cook的方法上添加了發出命令的桌號,這樣在多線程輸出信息的時候,才知道到底是在給哪個桌做菜,示例代碼如下:
/** * 廚師的接口 */ public interface CookApi { /** * 示意,做菜的方法 * @param tableNum 點菜的桌號 * @param name 菜名 */ public void cook(int tableNum,String name); }
(3)開始來實現命令接口,為了簡單,這次只有熱菜,因為要做工作都在後廚的命令隊列裏面,因此涼菜就不要了,示例代碼如下:
/** * 命令對象,綠豆排骨煲 */ public class ChopCommand implements Command{ /** * 持有具體做菜的廚師的對象 */ private CookApi cookApi = null; /** * 設置具體做菜的廚師的對象 * @param cookApi 具體做菜的廚師的對象 */ public void setCookApi(CookApi cookApi) { this.cookApi = cookApi; } /** * 點菜的桌號 */ private int tableNum; /** * 構造方法,傳入點菜的桌號 * @param tableNum 點菜的桌號 */ public ChopCommand(int tableNum){ this.tableNum = tableNum; } public int getTableNum(){ return this.tableNum; } public void execute() { this.cookApi.cook(tableNum,"綠豆排骨煲"); } }
還有一個命令對象是“北京烤鴨“,跟上面實現一樣,只是菜名不同而已,所以就不去展示示例代碼了。
(4)接下來構建很重要的命令對象的隊列,其實也不是有多難,多個命令對象嘛,用個集合來存儲就好了,然後按照放入的順序,先進先出即可。
請註意:為了演示的簡單性,這裏沒有使用java.util.Queue,直接使用List來模擬實現了。
示例代碼如下:
/** * 命令隊列類 */ public class CommandQueue { /** * 用來存儲命令對象的隊列 */ private static List<Command> cmds = new ArrayList<Command>(); /** * 服務員傳過來一個新的菜單,需要同步, * 因為同時會有很多的服務員傳入菜單,而同時又有很多廚師在從隊列裏取值 * @param menu 傳入的菜單 */ public synchronized static void addMenu(MenuCommand menu){ //一個菜單對象包含很多命令對象 for(Command cmd : menu.getCommands()){ cmds.add(cmd); } } /** * 廚師從命令隊列裏面獲取命令對象進行處理,也是需要同步的 */ public synchronized static Command getOneCommand(){ Command cmd = null; if(cmds.size() > 0 ){ //取出隊列的第一個,因為是約定的按照加入的先後來處理 cmd = cmds.get(0); //同時從隊列裏面取掉這個命令對象 cmds.remove(0); } return cmd; } }
提示:這裏並沒有考慮一些復雜的情況,比如:如果命令隊列裏面沒有命令,而廚師又來獲取命令怎麽辦?這裏只是做一個基本的示範,並不是完整的實現,所以這裏就沒有去處理這些問題了,當然出現這種問題,就需要使用wait/notify來進行線程調度了。
(5)有了命令隊列,誰來向這個隊列裏面傳入命令呢?
很明顯是服務員,當客戶點菜完成,服務員就會執行菜單,現在執行菜單就相當於把菜單直接傳遞給後廚,也就是要把菜單裏的所有命令對象加入到命令隊列裏面。因此菜單對象的實現需要改變,示例代碼如下:
/** * 菜單對象,是個宏命令對象 */ public class MenuCommand implements Command { /** * 用來記錄組合本菜單的多道菜品,也就是多個命令對象 */ private Collection<Command> col = new ArrayList<Command>(); /** * 點菜,把菜品加入到菜單中 * @param cmd 客戶點的菜 */ public void addCommand(Command cmd){ col.add(cmd); } public void setCookApi(CookApi cookApi){ //什麽都不用做 } public int getTableNum(){ //什麽都不用做 return 0; } /** * 獲取菜單中的多個命令對象 * @return 菜單中的多個命令對象 */ public Collection<Command> getCommands(){ return this.col; } public void execute() { //執行菜單就是把菜單傳遞給後廚 CommandQueue.addMenu(this); } }
(6)現在有了命令隊列,也有人負責向隊列裏面添加命令了,可是誰來執行命令隊列裏面的命令呢?
答案是:由廚師從命令隊列裏面獲取命令,並真正處理命令,而且廚師在處理命令前會把自己設置到命令對象裏面去當接收者,表示這個菜由我來實際做。
廚師對象的實現,大致有如下的改變:
為了更好的體現命令隊列的用法,再說實際情況也是多個廚師,這裏用多線程來模擬多個廚師,他們自己從命令隊列裏面獲取命令,然後處理命令,然後再獲取下一個,如此反復,因此廚師類要實現多線程接口。
還有一個改變,為了在多線程中輸出信息,讓我們知道是哪一個廚師在執行命令,給廚師添加了一個姓名的屬性,通過構造方法傳入。
另外一個改變是為了在多線程中看出效果,在廚師真正做菜的方法裏面使用隨機數模擬了一個做菜的時間。
好了,介紹完了改變的地方,一起看看代碼吧,示例代碼如下:
/** * 廚師對象,做熱菜的廚師 */ public class HotCook implements CookApi,Runnable{ /** * 廚師姓名 */ private String name; /** * 構造方法,傳入廚師姓名 * @param name 廚師姓名 */ public HotCook(String name){ this.name = name; } public void cook(int tableNum,String name) { //每次做菜的時間是不一定的,用個隨機數來模擬一下 int cookTime = (int)(20 * Math.random()); System.out.println(this.name+"廚師正在為"+tableNum +"號桌做:"+name); try { //讓線程休息這麽長時間,表示正在做菜 Thread.sleep(cookTime); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name+"廚師為"+tableNum +"號桌做好了:"+name+",共計耗時="+cookTime+"秒"); } public void run() { while(true){ //到命令隊列裏面獲取命令對象 Command cmd = CommandQueue.getOneCommand(); if(cmd != null){ //說明取到命令對象了,這個命令對象還沒有設置接收者 //因為前面都還不知道到底哪一個廚師來真正執行這個命令 //現在知道了,就是當前廚師實例,設置到命令對象裏面 cmd.setCookApi(this); //然後真正執行這個命令 cmd.execute(); } //休息1秒 try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
(7)該來看看服務員類了,由於現在考慮了後廚的管理,因此從實際來看,這次服務員也不知道到底命令的真正接收者是誰了,也就是說服務員也不知道某個菜到底最後由哪一位廚師完成,所以服務員類就簡單了。
組裝命令對象和接收者的功能後移到廚師類的線程裏面了,當某個廚師從命令隊列裏面獲取一個命令對象的時候,這個廚師就是這個命令的真正接收者。
看看服務員類的示例代碼如下:
/** * 服務員,負責組合菜單,還負責執行調用 */ public class Waiter { /** * 持有一個宏命令對象——菜單 */ private MenuCommand menuCommand = new MenuCommand(); /** * 客戶點菜 * @param cmd 客戶點的菜,每道菜是一個命令對象 */ public void orderDish(Command cmd){ //添加到菜單中 menuCommand.addCommand(cmd); } /** * 客戶點菜完畢,表示要執行命令了,這裏就是執行菜單這個組合命令 */ public void orderOver(){ this.menuCommand.execute(); } }
(8)在見到曙光之前,還有一個問題要解決,就是誰來啟動多線程的廚師呢?
為了實現後廚的管理,為此專門定義一個後廚管理的類,在這個類裏面去啟動多個廚師的線程。而且這種啟動在運行期間應該只有一次。示例代碼如下:
/** * 後廚的管理類,通過此類讓後廚的廚師進行運行狀態 */ public class CookManager { /** * 用來控制是否需要創建廚師,如果已經創建過了就不要再執行了 */ private static boolean runFlag = false; /** * 運行廚師管理,創建廚師對象並啟動他們相應的線程, * 無論運行多少次,創建廚師對象和啟動線程的工作就只做一次 */ public static void runCookManager(){ if(!runFlag){ runFlag = true; //創建三位廚師 HotCook cook1 = new HotCook("張三"); HotCook cook2 = new HotCook("李四"); HotCook cook3 = new HotCook("王五"); //啟動他們的線程 Thread t1 = new Thread(cook1); t1.start(); Thread t2 = new Thread(cook2); t2.start(); Thread t3 = new Thread(cook3); t3.start(); } } }
(9)曙光來臨了,寫個客戶端測試測試,示例代碼如下:
public class Client { public static void main(String[] args) { //先要啟動後臺,讓整個程序運行起來 CookManager.runCookManager(); //為了簡單,直接用循環模擬多個桌號點菜 for(int i = 0;i<5;i++){ //創建服務員 Waiter waiter = new Waiter(); //創建命令對象,就是要點的菜 Command chop = new ChopCommand(i); Command duck = new DuckCommand(i); //點菜,就是把這些菜讓服務員記錄下來 waiter.orderDish(chop); waiter.orderDish(duck); //點菜完畢 waiter.orderOver(); } } }
(10)運行一下,看看效果,可能每次運行的效果不一樣,畢竟是使用多線程在處理請求隊列,某次運行的結果如下:
好好觀察上面的數據,在多線程環境下,雖然保障了命令對象取出的順序是先進先出,但是究竟是哪一個廚師來做,還有具體做多長時間都是不定的。
本文出自 “ciyo技術分享” 博客,請務必保留此出處http://ciyorecord.blog.51cto.com/6010867/1945511
命令模式(三)