重學 Java 設計模式:實戰命令模式「模擬高檔餐廳八大菜系,小二點單廚師烹飪場景」

小傅哥發表於2020-06-23


作者:小傅哥
部落格:https://bugstack.cn - 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

持之以恆的重要性

初學程式設計往往都很懵,幾乎在學習的過程中會遇到各種各樣的問題,哪怕別人那執行好好的程式碼,但你照著寫完就報錯。但好在你堅持住了,否則你可能看不到這篇文章。時間和成長就是相互關聯著,你在哪條路上堅持走的久,就能看見那條的終點有多美,但如果你浪費了一次又一次努力的機會,那麼你也會同樣錯過很多機遇,因為你的路換了。堅持學習、努力成長,持以恆的付出一定會有所收穫。

學習方法的重要性

不會學習往往會耽誤很多時間,又沒有可觀的收成。但不會學習有時候是因為造成的,尤其是學習視訊、書籍資料、技術文件等,如果只是看了卻不是實際操作驗證,那麼真的很難把別人的知識讓自己吸收,即使是當時感覺會了也很快就會忘記。時而也經常會有人找到你說;“這個我不知道,你先告訴我,過後我就學。”但過後你學了嗎?

你願意為一個知識盲區付出多長時間

你心裡時而會蹦出這樣的詞嗎;太難了我不會找個人幫一下吧放棄了放棄了,其實誰都可能遇到很不好解決的問題,也是可以去問去諮詢的。但,如果在這之前你沒有在自己的大腦中反覆的尋找答案,那麼你的大腦中就不會形成一個凸點的知識樹,缺少了這個學習過程也就缺少了查閱各種資料給自己大腦填充知識的機會,哪怕是問到了答案最終也會因時間流逝而忘記。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-14-01 使用一坨程式碼實現業務需求
itstack-demo-design-14-02 通過設計模式優化程式碼結構,增加擴充套件性和維護性

三、命令模式介紹

命令模式,圖片來自 refactoringguru.cn

命令模式在我們通常的網際網路開發中相對來說用的比較少,但這樣的模式在我們的日常中卻經常使用到,那就是Ctrl+CCtrl+V。當然如果你開發過一些桌面應用,也會感受到這樣設計模式的應用場景。從這樣的模式感受上,可以想到這是把邏輯實現與操作請求進行分離,降低耦合方便擴充套件。

命令模式是行為模式中的一種,以資料驅動的方式將命令物件,可以使用建構函式的方式傳遞給呼叫者。呼叫者再提供相應的實現為命令執行提供操作方法。可能會感覺這部分有一些饒,可以通過對程式碼的實現進行理解,在通過實操來熟練。

在這個設計模式的實現過程中有如下幾個比較重要的點;

  1. 抽象命令類;宣告執行命令的介面和方法
  2. 具體的命令實現類;介面類的具體實現,可以是一組相似的行為邏輯
  3. 實現者;也就是為命令做實現的具體實現類
  4. 呼叫者;處理命令、實現的具體操作者,負責對外提供命令服務

四、案例場景模擬

場景模擬;大餐廳點餐場景

在這個案例中我們模擬在餐廳中點餐交給廚師?‍?烹飪的場景

命令場景的核心的邏輯是呼叫方與不需要去關心具體的邏輯實現,在這個場景中也就是點餐人員只需要把需要點的各種菜系交個小二就可以,小二再把各項菜品交給各個廚師進行烹飪。也就是點餐人員不需要跟各個廚師交流,只需要在統一的環境裡下達命令就可以。

在這個場景中可以看到有不同的菜品;山東(魯菜)、四川(川菜)、江蘇(蘇菜)、廣東(粵菜)、福建(閩菜)、浙江(浙菜)、湖南(湘菜),每種菜品都會有不同的廚師?‍?進行烹飪。而客戶並不會去關心具體是誰烹飪,廚師也不會去關心誰點的餐。客戶只關心早點上菜,廚師只關心還有多少個菜要做。而這中間的銜接的過程,由小二完成。

那麼在這樣的一個模擬場景下,可以先思考?哪部分是命令模式的拆解,哪部分是命令的呼叫者以及命令的實現邏輯。

五、用一坨坨程式碼實現

不考慮設計模式的情況下,在做這樣一個點單系統,有一個類就夠了

像是這樣一個複雜的場景,如果不知道設計模式直接開發,也是可以達到目的的。但對於後續的各項的菜品擴充套件、廚師實現以及如何呼叫上會變得非常耦合難以擴充套件。

1. 工程結構

itstack-demo-design-14-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── XiaoEr.java
  • 這裡只有一個飯店小二的類,通過這樣的一個類實現整個不同菜品的點單邏輯。

2. 程式碼實現

public class XiaoEr {

    private Logger logger = LoggerFactory.getLogger(XiaoEr.class);

    private Map<Integer, String> cuisineMap = new ConcurrentHashMap<Integer, String>();

    public void order(int cuisine) {
        // 廣東(粵菜)
        if (1 == cuisine) {
            cuisineMap.put(1, "廣東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭");
        }

        // 江蘇(蘇菜)
        if (2 == cuisine) {
            cuisineMap.put(2, "江蘇廚師,烹飪蘇菜,宮廷第二大菜系,古今國宴上最受人歡迎的菜系。");
        }

        // 山東(魯菜)
        if (3 == cuisine) {
            cuisineMap.put(3, "山東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭.");
        }

        // 四川(川菜)
        if (4 == cuisine) {
            cuisineMap.put(4, "四川廚師,烹飪川菜,中國最有特色的菜系,也是民間最大菜系。");
        }

    }

    public void placeOrder() {
        logger.info("選單:{}", JSON.toJSONString(cuisineMap));
    }

}
  • 在這個類的實現中提供了兩個方法,一個方法用於點單新增菜品order(),另外一個方法展示菜品的資訊placeOrder()
  • 從上面可以看到有比較多的if語句判斷型別進行新增菜品,那麼對於這樣的程式碼後續就需要大量的經歷進行維護,同時可能實際的邏輯要比這複雜的多。都寫在這樣一個類裡會變得耦合的非常嚴重。

六、命令模式重構程式碼

接下來使用命令模式來進行程式碼優化,也算是一次很小的重構。

命令模式可以將上述的模式拆解三層大塊,命令、命令實現者、命令的呼叫者,當有新的菜品或者廚師擴充時候就可以在指定的類結構下進行實現新增即可,外部的呼叫也會非常的容易擴充套件。

1. 工程結構

itstack-demo-design-14-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── cook
    │           │    ├── impl
    │           │    │   ├── GuangDongCook.java
    │           │    │   ├── JiangSuCook.java
    │           │    │   ├── ShanDongCook.java
    │           │    │   └── SiChuanCook.java
    │           │    └── ICook.java
    │           ├── cuisine
    │           │    ├── impl
    │           │    │   ├── GuangDoneCuisine.java
    │           │    │   ├── JiangSuCuisine.java
    │           │    │   ├── ShanDongCuisine.java
    │           │    │   └── SiChuanCuisine.java
    │           │    └── ICuisine.java
    │           └── XiaoEr.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

命令模式模型結構

命令模式模型結構

  • 從上圖可以看到整體分為三大塊;命令實現(菜品)、邏輯實現(廚師)、呼叫者(小二),以上這三面的實現就是命令模式的核心內容。
  • 經過這樣的拆解就可以非常方面的擴充套件菜品、廚師,對於呼叫者來說這部分都是鬆耦合的,在整體的框架下可以非常容易加入實現邏輯。

2. 程式碼實現

2.1 抽象命令定義(菜品介面)

/**
 * 部落格:https://bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!
 * 公眾號:bugstack蟲洞棧
 * Create by 小傅哥(fustack) @2020
 *
 * 菜系
 * 01、山東(魯菜)——宮廷最大菜系,以孔府風味為龍頭。
 * 02、四川(川菜)——中國最有特色的菜系,也是民間最大菜系。
 * 03、江蘇(蘇菜)——宮廷第二大菜系,古今國宴上最受人歡迎的菜系。
 * 04、廣東(粵菜)——國內民間第二大菜系,國外最有影響力的中國菜系,可以代表中國。
 * 05、福建(閩菜)——客家菜的代表菜系。
 * 06、浙江(浙菜)——中國最古老的菜系之一,宮廷第三大菜系。
 * 07、湖南(湘菜)——民間第三大菜系。
 * 08、安徽(徽菜)——徽州文化的典型代表。
 */
public interface ICuisine {

    void cook(); // 烹調、製作

}
  • 這是命令介面類的定義,並提供了一個烹飪方法。後面會選四種菜品進行實現。

2.2 具體命令實現(四種菜品)

廣東(粵菜)

public class GuangDoneCuisine implements ICuisine {

    private ICook cook;

    public GuangDoneCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }

}

江蘇(蘇菜)

public class JiangSuCuisine implements ICuisine {

    private ICook cook;

    public JiangSuCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }

}

山東(魯菜)

public class ShanDongCuisine implements ICuisine {

    private ICook cook;

    public ShanDongCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }

}

四川(川菜)

public class SiChuanCuisine implements ICuisine {

    private ICook cook;

    public SiChuanCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }

}
  • 以上是四種菜品的實現,在實現的類中都有新增了一個廚師類(ICook),並通過這個類提供的方法進行操作命令(烹飪菜品)cook.doCooking()
  • 命令的實現過程可以是按照邏輯進行新增補充,目前這裡抽象的比較簡單,只是模擬一個烹飪的過程,相當於同時廚師進行菜品烹飪。

2.3 抽象實現者定義(廚師介面)

public interface ICook {

    void doCooking();

}
  • 這裡定義的是具體的為命令的實現者,這裡也就是菜品對應的廚師烹飪的指令實現。

2.4 實現者具體實現(四類廚師)

粵菜,廚師

public class GuangDongCook implements ICook {

    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("廣東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭");
    }

}

蘇菜,廚師

public class JiangSuCook implements ICook {

    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("江蘇廚師,烹飪蘇菜,宮廷第二大菜系,古今國宴上最受人歡迎的菜系。");
    }

}

魯菜,廚師

public class ShanDongCook implements ICook {

    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("山東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭");
    }

}

蘇菜,廚師

public class SiChuanCook implements ICook {

    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("四川廚師,烹飪川菜,中國最有特色的菜系,也是民間最大菜系。");
    }

}
  • 這裡是四類不同菜品的廚師?‍?,在這個實現的過程是模擬打了日誌,相當於通知了廚房裡具體的廚師進行菜品烹飪。
  • 從以上可以看到,當我們需要進行擴從的時候是可以非常方便的進行新增的,每一個類都具備了單一職責原則。

2.5 呼叫者(小二)

public class XiaoEr {

    private Logger logger = LoggerFactory.getLogger(XiaoEr.class);

    private List<ICuisine> cuisineList = new ArrayList<ICuisine>();

    public void order(ICuisine cuisine) {
        cuisineList.add(cuisine);
    }

    public synchronized void placeOrder() {
        for (ICuisine cuisine : cuisineList) {
            cuisine.cook();
        }
        cuisineList.clear();
    }

}
  • 在呼叫者的具體實現中,提供了菜品的新增和選單執行烹飪。這個過程是命令模式的具體呼叫,通過外部將菜品和廚師傳遞進來而進行具體的呼叫。

3. 測試驗證

3.1 編寫測試類

@Test
public void test(){

    // 菜系 + 廚師;廣東(粵菜)、江蘇(蘇菜)、山東(魯菜)、四川(川菜)
    ICuisine guangDoneCuisine = new GuangDoneCuisine(new GuangDongCook());
    JiangSuCuisine jiangSuCuisine = new JiangSuCuisine(new JiangSuCook());
    ShanDongCuisine shanDongCuisine = new ShanDongCuisine(new ShanDongCook());
    SiChuanCuisine siChuanCuisine = new SiChuanCuisine(new SiChuanCook());

    // 點單
    XiaoEr xiaoEr = new XiaoEr();
    xiaoEr.order(guangDoneCuisine);
    xiaoEr.order(jiangSuCuisine);
    xiaoEr.order(shanDongCuisine);
    xiaoEr.order(siChuanCuisine);

    // 下單
    xiaoEr.placeOrder();
}
  • 這裡可以主要觀察菜品廚師的組合;new GuangDoneCuisine(new GuangDongCook());,每一個具體的命令都擁有一個對應的實現類,可以進行組合。
  • 當菜品和具體的實現定義完成後,由小二進行操作點單,xiaoEr.order(guangDoneCuisine);,這裡分別新增了四種菜品,給小二。
  • 最後是下單,這個是具體命令實現的操作,相當於把小二手裡的選單傳遞給廚師。當然這裡也可以提供刪除和撤銷,也就是客戶取消了自己的某個菜品。

3.2 測試結果

22:12:13.056 [main] INFO  org.itstack.demo.design.cook.ICook - 廣東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭
22:12:13.059 [main] INFO  org.itstack.demo.design.cook.ICook - 江蘇廚師,烹飪蘇菜,宮廷第二大菜系,古今國宴上最受人歡迎的菜系。
22:12:13.059 [main] INFO  org.itstack.demo.design.cook.ICook - 山東廚師,烹飪魯菜,宮廷最大菜系,以孔府風味為龍頭
22:12:13.059 [main] INFO  org.itstack.demo.design.cook.ICook - 四川廚師,烹飪川菜,中國最有特色的菜系,也是民間最大菜系。

Process finished with exit code 0
  • 從上面的測試結果可以看到,我們已經交給呼叫者(小二)的點單,由不同的廚師具體實現(烹飪)。
  • 此外當我們需要不同的菜品時候或者修改時候都可以非常方便的新增和修改,在具備單一職責的類下,都可以非常方便的擴充套件。

七、總結

  • 從以上的內容和例子可以感受到,命令模式的使用場景需要分為三個比較大的塊;命令實現呼叫者,而這三塊內容的拆分也是選擇適合場景的關鍵因素,經過這樣的拆分可以讓邏輯具備單一職責的性質,便於擴充套件。
  • 通過這樣的實現方式與if語句相比,降低了耦合性也方便其他的命令和實現的擴充套件。但同時這樣的設計模式也帶來了一點問題,就是在各種命令與實現的組合下,會擴充套件出很多的實現類,需要進行管理。
  • 設計模式的學習一定要勤加練習,哪怕最開始是模仿實現也是可以的,多次的練習後再去找到一些可以優化的場景,並逐步運用到自己的開發中。提升自己對程式碼的設計感覺,讓程式碼結構更加清晰易擴充套件。

八、推薦閱讀

相關文章