1. 程式人生 > >命令模式(三)

命令模式(三)

設計模式 命令模式

隊列請求

所謂隊列請求,就是對命令對象進行排隊,組成工作隊列,然後依次取出命令對象來執行。多用多線程或者線程池來進行命令隊列的處理,當然也可以不用多線程,就是一個線程,一個命令一個命令的循環處理,就是慢點。
繼續宏命令的例子,其實在後廚,會收到很多很多的菜單,一般是按照菜單傳遞到後廚的先後順序來進行處理,對每張菜單,假定也是按照菜品的先後順序進行制作,那麽在後廚就自然形成了一個菜品的隊列,也就是很多個用戶的命令對象的隊列。
後廚有很多廚師,每個廚師都從這個命令隊列裏面取出一個命令,然後按照命令做出菜來,就相當於多個線程在同時處理一個隊列請求。
因此後廚就是一個很典型的隊列請求的例子。

提示一點:後廚的廚師與命令隊列之間是沒有任何關聯的,也就是說是完全解耦的。命令隊列是客戶發出的命令,廚師只是負責從隊列裏面取出一個,處理,然後再取下一個,再處理,僅此而已,廚師不知道也不管客戶是誰。
下面就一起來看看如何實現隊列請求。
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

命令模式(三)