3.5 隊列請求##
所謂隊列請求涵紊,就是對命令對象進行排隊傍妒,組成工作隊列,然后依次取出命令對象來執(zhí)行
栖袋。多用多線程或者線程池來進行命令隊列的處理拍顷,當(dāng)然也可以不用多線程,就是一個線程塘幅,一個命令一個命令的循環(huán)處理昔案,就是慢點。
繼續(xù)宏命令的例子电媳,其實在后廚踏揣,會收到很多很多的菜單,一般是按照菜單傳遞到后廚的先后順序來進行處理匾乓,對每張菜單捞稿,假定也是按照菜品的先后順序進行制作,那么在后廚就自然形成了一個菜品的隊列拼缝,也就是很多個用戶的命令對象的隊列娱局。
后廚有很多廚師郎笆,每個廚師都從這個命令隊列里面取出一個命令红氯,然后按照命令做出菜來这难,就相當(dāng)于多個線程在同時處理一個隊列請求行嗤。
因此后廚就是一個很典型的隊列請求的例子。
提示一點:后廚的廚師與命令隊列之間是沒有任何關(guān)聯(lián)的匀泊,也就是說是完全解耦的
烘绽。命令隊列是客戶發(fā)出的命令敌卓,廚師只是負責(zé)從隊列里面取出一個,處理抹缕,然后再取下一個澈蟆,再處理,僅此而已卓研,廚師不知道也不管客戶是誰趴俘。
- 如何實現(xiàn)命令模式的隊列請求
(1)先從命令接口開始,除了execute方法外鉴分,新加了一個返回發(fā)出命令的桌號哮幢,就是點菜的桌號,還有一個是為命令對象設(shè)置接收者的方法志珍,也把它添加到接口上,這個是為了后面多線程處理的時候方便使用垛叨。示例代碼如下:
/**
* 命令接口伦糯,聲明執(zhí)行的操作
*/
public interface Command {
/**
* 執(zhí)行命令對應(yīng)的操作
*/
public void execute();
/**
* 設(shè)置命令的接收者
* @param cookApi 命令的接收者
*/
public void setCookApi(CookApi cookApi);
/**
* 返回發(fā)起請求的桌號,就是點菜的桌號
* @return 發(fā)起請求的桌號
*/
public int getTableNum();
}
(2)廚師的接口也發(fā)生了一點變化嗽元,在cook的方法上添加了發(fā)出命令的桌號敛纲,這樣在多線程輸出信息的時候,才知道到底是在給哪個桌做菜剂癌,示例代碼如下:
/**
* 廚師的接口
*/
public interface CookApi {
/**
* 示意淤翔,做菜的方法
* @param tableNum 點菜的桌號
* @param name 菜名
*/
public void cook(int tableNum,String name);
}
** (3)開始來實現(xiàn)命令接口,為了簡單佩谷,這次只有熱菜旁壮,因為要做工作都在后廚的命令隊列里面,因此涼菜就不要了谐檀,示例代碼如下:**
/**
* 命令對象抡谐,綠豆排骨煲
*/
public class ChopCommand implements Command{
/**
* 持有具體做菜的廚師的對象
*/
private CookApi cookApi = null;
/**
* 設(shè)置具體做菜的廚師的對象
* @param cookApi 具體做菜的廚師的對象
*/
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
/**
* 點菜的桌號
*/
private int tableNum;
/**
* 構(gòu)造方法,傳入點菜的桌號
* @param tableNum 點菜的桌號
*/
public ChopCommand(int tableNum){
this.tableNum = tableNum;
}
public int getTableNum(){
return this.tableNum;
}
public void execute() {
this.cookApi.cook(tableNum,"綠豆排骨煲");
}
}
(4)接下來構(gòu)建很重要的命令對象的隊列桐猬,其實也不是有多難麦撵,多個命令對象嘛,用個集合來存儲就好了溃肪,然后按照放入的順序免胃,先進先出即可。
/**
* 命令隊列類
*/
public class CommandQueue {
/**
* 用來存儲命令對象的隊列
*/
private static List<Command> cmds = new ArrayList<Command>();
/**
* 服務(wù)員傳過來一個新的菜單惫撰,需要同步羔沙,
* 因為同時會有很多的服務(wù)員傳入菜單,而同時又有很多廚師在從隊列里取值
* @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;
}
}
提示:這里并沒有考慮一些復(fù)雜的情況诞挨,比如:如果命令隊列里面沒有命令,而廚師又來獲取命令怎么辦呢蛤?
這里只是做一個基本的示范惶傻,并不是完整的實現(xiàn),所以這里就沒有去處理這些問題了其障,當(dāng)然出現(xiàn)這種問題银室,就需要使用wait/notify來進行線程調(diào)度了
。
(5)有了命令隊列励翼,誰來向這個隊列里面?zhèn)魅朊钅兀?/strong>
很明顯是服務(wù)員蜈敢,當(dāng)客戶點菜完成,服務(wù)員就會執(zhí)行菜單汽抚,現(xiàn)在執(zhí)行菜單就相當(dāng)于把菜單直接傳遞給后廚抓狭,也就是要把菜單里的所有命令對象加入到命令隊列里面。因此菜單對象的實現(xiàn)需要改變造烁,示例代碼如下:
/**
* 菜單對象否过,是個宏命令對象
*/
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() {
//執(zhí)行菜單就是把菜單傳遞給后廚
CommandQueue.addMenu(this);
}
}
(6)現(xiàn)在有了命令隊列苗桂,也有人負責(zé)向隊列里面添加命令了,可是誰來執(zhí)行命令隊列里面的命令呢告组?
答案是:由廚師從命令隊列里面獲取命令煤伟,并真正處理命令,而且廚師在處理命令前會把自己設(shè)置到命令對象里面去當(dāng)接收者木缝,表示這個菜由我來實際做便锨。
廚師對象的實現(xiàn),大致有如下的改變:
為了更好的體現(xiàn)命令隊列的用法氨肌,再說實際情況也是多個廚師鸿秆,這里用多線程來模擬多個廚師,他們自己從命令隊列里面獲取命令怎囚,然后處理命令卿叽,然后再獲取下一個,如此反復(fù)恳守,因此廚師類要實現(xiàn)多線程接口考婴。
還有一個改變,為了在多線程中輸出信息催烘,讓我們知道是哪一個廚師在執(zhí)行命令沥阱,給廚師添加了一個姓名的屬性,通過構(gòu)造方法傳入伊群。
另外一個改變是為了在多線程中看出效果考杉,在廚師真正做菜的方法里面使用隨機數(shù)模擬了一個做菜的時間策精。
好了,介紹完了改變的地方崇棠,一起看看代碼吧咽袜,示例代碼如下:
/**
* 廚師對象,做熱菜的廚師
*/
public class HotCook implements CookApi,Runnable{
/**
* 廚師姓名
*/
private String name;
/**
* 構(gòu)造方法枕稀,傳入廚師姓名
* @param name 廚師姓名
*/
public HotCook(String name){
this.name = name;
}
public void cook(int tableNum,String name) {
//每次做菜的時間是不一定的询刹,用個隨機數(shù)來模擬一下
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){
//說明取到命令對象了萎坷,這個命令對象還沒有設(shè)置接收者
//因為前面都還不知道到底哪一個廚師來真正執(zhí)行這個命令
//現(xiàn)在知道了凹联,就是當(dāng)前廚師實例,設(shè)置到命令對象里面
cmd.setCookApi(this);
//然后真正執(zhí)行這個命令
cmd.execute();
}
//休息1秒
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(7)該來看看服務(wù)員類了哆档,由于現(xiàn)在考慮了后廚的管理蔽挠,因此從實際來看,這次服務(wù)員也不知道到底命令的真正接收者是誰了瓜浸,也就是說服務(wù)員也不知道某個菜到底最后由哪一位廚師完成象泵,所以服務(wù)員類就簡單了。
組裝命令對象和接收者的功能后移到廚師類的線程里面了斟叼,當(dāng)某個廚師從命令隊列里面獲取一個命令對象的時候,這個廚師就是這個命令的真正接收者春寿。
/**
* 服務(wù)員朗涩,負責(zé)組合菜單,還負責(zé)執(zhí)行調(diào)用
*/
public class Waiter {
/**
* 持有一個宏命令對象——菜單
*/
private MenuCommand menuCommand = new MenuCommand();
/**
* 客戶點菜
* @param cmd 客戶點的菜,每道菜是一個命令對象
*/
public void orderDish(Command cmd){
//添加到菜單中
menuCommand.addCommand(cmd);
}
/**
* 客戶點菜完畢绑改,表示要執(zhí)行命令了谢床,這里就是執(zhí)行菜單這個組合命令
*/
public void orderOver(){
this.menuCommand.execute();
}
}
(8)在見到曙光之前,還有一個問題要解決厘线,就是誰來啟動多線程的廚師呢识腿?
為了實現(xiàn)后廚的管理,為此專門定義一個后廚管理的類造壮,在這個類里面去啟動多個廚師的線程
渡讼。而且這種啟動在運行期間應(yīng)該只有一次。示例代碼如下:
/**
* 后廚的管理類耳璧,通過此類讓后廚的廚師進行運行狀態(tài)
*/
public class CookManager {
/**
* 用來控制是否需要創(chuàng)建廚師成箫,如果已經(jīng)創(chuàng)建過了就不要再執(zhí)行了
*/
private static boolean runFlag = false;
/**
* 運行廚師管理,創(chuàng)建廚師對象并啟動他們相應(yīng)的線程旨枯,
* 無論運行多少次蹬昌,創(chuàng)建廚師對象和啟動線程的工作就只做一次
*/
public static void runCookManager(){
if(!runFlag){
runFlag = true;
//創(chuàng)建三位廚師
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();
//為了簡單栖榨,直接用循環(huán)模擬多個桌號點菜
for(int i = 0;i<5;i++){
//創(chuàng)建服務(wù)員
Waiter waiter = new Waiter();
//創(chuàng)建命令對象,就是要點的菜
Command chop = new ChopCommand(i);
Command duck = new DuckCommand(i);
//點菜明刷,就是把這些菜讓服務(wù)員記錄下來
waiter.orderDish(chop);
waiter.orderDish(duck);
//點菜完畢
waiter.orderOver();
}
}
}
(10)運行一下婴栽,看看效果,可能每次運行的效果不一樣遮精,畢竟是使用多線程在處理請求隊列居夹,某次運行的結(jié)果如下:
好好觀察上面的數(shù)據(jù),在多線程環(huán)境下本冲,雖然保障了命令對象取出的順序是先進先出准脂,但是究竟是哪一個廚師來做,還有具體做多長時間都是不定的
檬洞。
3.7 命令模式的優(yōu)缺點##
- 更松散的耦合
命令模式使得發(fā)起命令的對象——客戶端狸膏,和具體實現(xiàn)命令的對象——接收者對象完全解耦,也就是說發(fā)起命令的對象添怔,完全不知道具體實現(xiàn)對象是誰湾戳,也不知道如何實現(xiàn)。
- 更動態(tài)的控制
命令模式把請求封裝起來广料,可以動態(tài)對它進行參數(shù)化砾脑、隊列化和日志化等操作,從而使得系統(tǒng)更靈活艾杏。
- 能很自然的復(fù)合命令
命令模式中的命令對象韧衣,能夠很容易的組合成為復(fù)合命令,就是前面講的宏命令购桑,從而使系統(tǒng)操作更簡單畅铭,功能更強大。
- 更好的擴展性
由于發(fā)起命令的對象和具體的實現(xiàn)完全解耦勃蜘,因此擴展新的命令就很容易硕噩,只需要實現(xiàn)新的命令對象,然后在裝配的時候缭贡,把具體的實現(xiàn)對象設(shè)置到命令對象里面炉擅,然后就可以使用這個命令對象,已有的實現(xiàn)完全不用變化匀归。
3.8 思考命令模式##
- 命令模式的本質(zhì)
命令模式的本質(zhì):封裝請求坑资。
前面講了,命令模式的關(guān)鍵就是把請求封裝成為命令對象穆端,然后就可以對這個對象進行一系列的處理了
袱贮,比如上面講到的參數(shù)化配置、可撤銷操作、宏命令攒巍、隊列請求嗽仪、日志請求等功能處理。
- 何時選用命令模式
建議在如下情況中柒莉,選用命令模式:
如果需要抽象出需要執(zhí)行的動作闻坚,并參數(shù)化這些對象,可以選用命令模式兢孝,把這些需要執(zhí)行的動作抽象成為命令窿凤,然后實現(xiàn)命令的參數(shù)化配置
如果需要在不同的時刻指定、排列和執(zhí)行請求跨蟹,可以選用命令模式雳殊,把這些請求封裝成為命令對象,然后實現(xiàn)把請求隊列化
如果需要支持取消操作窗轩,可以選用命令模式夯秃,通過管理命令對象,能很容易的實現(xiàn)命令的恢復(fù)和重做的功能
如果需要支持當(dāng)系統(tǒng)崩潰時痢艺,能把對系統(tǒng)的操作功能重新執(zhí)行一遍仓洼,可以選用命令模式,把這些操作功能的請求封裝成命令對象堤舒,然后實現(xiàn)日志命令色建,就可以在系統(tǒng)恢復(fù)回來后,通過日志獲取命令列表舌缤,從而重新執(zhí)行一遍功能
在需要事務(wù)的系統(tǒng)中镀岛,可以選用命令模式,命令模式提供了對事務(wù)進行建模的方法友驮,命令模式有一個別名就是Transaction。
3.9 退化的命令模式##
在領(lǐng)會了命令模式本質(zhì)后驾锰,來思考一個命令模式退化的情況卸留。
前面講到了智能命令,如果命令的實現(xiàn)對象超級智能椭豫,實現(xiàn)了命令所要求的功能耻瑟,那么就不需要接收者了,既然沒有了接收者赏酥,那么也就不需要組裝者了
喳整。
- 舉個最簡單的示例來說明
比如現(xiàn)在要實現(xiàn)一個打印服務(wù),由于非常簡單裸扶,所以基本上就沒有什么講述框都,依次來看,命令接口定義如下:
public interface Command {
public void execute();
}
命令的實現(xiàn)示例代碼如下:
public class PrintService implements Command{
/**
* 要輸出的內(nèi)容
*/
private String str = "";
/**
* 構(gòu)造方法呵晨,傳入要輸出的內(nèi)容
* @param s 要輸出的內(nèi)容
*/
public PrintService(String s){
str = s;
}
public void execute() {
//智能的體現(xiàn)魏保,自己知道怎么實現(xiàn)命令所要求的功能熬尺,并真的實現(xiàn)了相應(yīng)的功能,不再轉(zhuǎn)調(diào)接收者了
System.out.println("打印的內(nèi)容為="+str);
}
}
此時的Invoker示例代碼如下:
public class Invoker {
/**
* 持有命令對象
*/
private Command cmd = null;
/**
* 設(shè)置命令對象
* @param cmd 命令對象
*/
public void setCmd(Command cmd){
this.cmd = cmd;
}
/**
* 開始打印
*/
public void startPrint(){
//執(zhí)行命令的功能
this.cmd.execute();
}
}
最后看看客戶端的代碼谓罗,示例如下:
public class Client {
public static void main(String[] args) {
//準備要發(fā)出的命令
Command cmd = new PrintService("退化的命令模式示例");
//設(shè)置命令給持有者
Invoker invoker = new Invoker();
invoker.setCmd(cmd);
//按下按鈕粱哼,真正啟動執(zhí)行命令
invoker.startPrint();
}
}
測試結(jié)果如下:
打印的內(nèi)容為=退化的命令模式示例
- 繼續(xù)變化
如果此時繼續(xù)變化,Invoker也開始變得智能化檩咱,在Invoker的startPrint方法里面揭措,Invoker加入了一些實現(xiàn),同時Invoker對持有命令也有意見刻蚯,覺得自己是個傀儡绊含,要求改變一下,直接在調(diào)用方法的時候傳遞命令對象進來
芦倒,示例代碼如下:
public class Invoker {
public void startPrint(Command cmd){
System.out.println("在Invoker中艺挪,輸出服務(wù)前");
cmd.execute();
System.out.println("輸出服務(wù)結(jié)束");
}
}
看起來Invoker退化成一個方法了。
這個時候Invoker很高興兵扬,宣稱自己是一個智能的服務(wù)麻裳,不再是一個傻傻的轉(zhuǎn)調(diào)者,而是有自己功能的服務(wù)了器钟。這個時候Invoker調(diào)用命令對象的執(zhí)行方法津坑,也不叫轉(zhuǎn)調(diào),改名叫“回調(diào)”傲霸,意思是在我Invoker需要的時候疆瑰,會回調(diào)你命令對象,命令對象你就乖乖的寫好實現(xiàn)昙啄,等我“回調(diào)”你就可以了
穆役。
事實上這個時候的命令模式的實現(xiàn),基本上就等同于Java回調(diào)機制的實現(xiàn)梳凛,可能有些朋友看起來感覺還不是佷像耿币,那是因為在Java回調(diào)機制的常見實現(xiàn)上,經(jīng)常沒有單獨的接口實現(xiàn)類韧拒,而是采用匿名內(nèi)部類的方式來實現(xiàn)的
淹接。
- 再進一步
把單獨實現(xiàn)命令接口的類改成用匿名內(nèi)部類實現(xiàn),這個時候就只剩下命令的接口叛溢、Invoker類塑悼,還有客戶端了
。
為了使用匿名內(nèi)部類楷掉,還要設(shè)置要輸出的值厢蒜,對命令接口做點小改動,增加一個設(shè)置輸出值的方法,示例代碼如下:
public interface Command {
public void execute();
/**
* 設(shè)置要輸出的內(nèi)容
* @param s 要輸出的內(nèi)容
*/
public void setStr(String s);
}
此時Invoker就是上面那個郭怪,而客戶端會有些改變支示,客戶端的示例代碼如下:
public class Client {
public static void main(String[] args) {
//準備要發(fā)出的命令,沒有具體實現(xiàn)類了
//匿名內(nèi)部類來實現(xiàn)命令
Command cmd = new Command() {
private String str = "";
public void setStr(String s) {
str = s;
}
public void execute() {
System.out.println("打印的內(nèi)容為="+str);
}
};
cmd.setStr("退化的命令模式類似于Java回調(diào)的示例");
//這個時候的Invoker或許該稱為服務(wù)了
Invoker invoker = new Invoker();
//按下按鈕鄙才,真正啟動執(zhí)行命令
invoker.startPrint(cmd);
}
}
運行測試一下颂鸿,結(jié)果如下:
在Invoker中,輸出服務(wù)前
打印的內(nèi)容為=退化的命令模式類似于Java回調(diào)的示例
輸出服務(wù)結(jié)束
- 現(xiàn)在是不是看出來了攒庵,
這個時候的命令模式的實現(xiàn)嘴纺,基本上就等同于Java回調(diào)機制的實現(xiàn)
。這也是很多人大談特談命令模式可以實現(xiàn)Java回調(diào)的意思浓冒。
當(dāng)然更狠的是連Invoker也不要了栽渴,直接把那個方法搬到Client中,那樣測試起來就更方便了稳懒。在實際開發(fā)中闲擦,應(yīng)用命令模式來實現(xiàn)回調(diào)機制的時候,Invoker通常還是有的场梆,但可以智能化實現(xiàn)墅冷,更準確的說Invoker充當(dāng)客戶調(diào)用的服務(wù)實現(xiàn),而回調(diào)的方法只是實現(xiàn)服務(wù)功能中的一個或者幾個步驟或油。
3.10 相關(guān)模式##
- 命令模式和組合模式
這兩個模式可以組合使用寞忿。
在命令模式中,實現(xiàn)宏命令的功能顶岸,就可以使用組合模式來實現(xiàn)
腔彰。前面的示例并沒有按照組合模式來做,那是為了保持示例的簡單辖佣,還有突出命令模式的實現(xiàn)霹抛,這點請注意。
- 命令模式和備忘錄模式
這兩個模式可以組合使用卷谈。
在命令模式中上炎,實現(xiàn)可撤銷操作功能時,前面講了有兩種實現(xiàn)方式雏搂,其中有一種就是保存命令執(zhí)行前的狀態(tài),撤銷的時候就把狀態(tài)恢復(fù)回去
寇损。如果采用這種方式實現(xiàn)凸郑,就可以考慮使用備忘錄模式。
如果狀態(tài)存儲在命令對象里面矛市,那么還可以使用原型模式芙沥,把命令對象當(dāng)作原型來克隆一個新的對象,然后把克隆出來的對象通過備忘錄模式存放。
- 命令模式和模板方法模式
這兩個模式從某種意義上有相似的功能而昨,命令模式可以作為模板方法的一種替代模式救氯,也就是說命令模式可以模仿實現(xiàn)模板方法模式的功能
。
如同前面講述的退化的命令模式可以實現(xiàn)Java的回調(diào)歌憨,而Invoker智能化后向服務(wù)進化着憨,如果Invoker的方法就是一個算法骨架,其中有兩步在這個骨架里面沒有具體實現(xiàn)务嫡,需要外部來實現(xiàn)甲抖,這個時候就可以通過回調(diào)命令接口來實現(xiàn)。
而類似的功能在模板方法里面心铃,一個算法骨架准谚,其中有兩步在這個骨架里面沒有具體實現(xiàn),是先調(diào)用抽象方法去扣,然后等待子類來實現(xiàn)柱衔。
可以看出雖然實現(xiàn)方式不一樣,但是可以實現(xiàn)相同的功能愉棱。