統一熱乾麵的製作流程---模板方法

anly_jun發表於2016-12-22

前情提要

上集講到, 小光引入了日報制度, 用來從各個分店店長那兒收集資訊. 如此一來, 小光每天就通過日報系統瞭解到各個分店的銷售情況, 問題所在, 也好根據收集到的使用者反饋來改善系統, 改善經營了.

不多久, 就有一個來自使用者的反饋, 通過店長的日報到了小光這邊:

有使用者反饋不同分店的熱乾麵味道不太一樣

所有示例原始碼已經上傳到Github, 戳這裡

統一熱乾麵的製作流程

小光是想將品牌做大, 做統一的. 當然是需要在任何分店給使用者都是統一的體驗啊. 所以, 小光很重視這個問題, 馬上投入了這個問題的解決中.

上次在弄分店的標準時並沒有統一規劃下熱乾麵的製作方法流程, 現在小光想借這個機會正好弄一套, 於是小光做了一個熱乾麵的製作規範:

public class HotDryNoodlesMaker {

    public void make() {
        // 1, 燙麵
        tangmian();

        // 2, 裝碗
        zhuangwan();

        // 3, 加調料(鹽,雞精,胡椒粉之類)
        jiatiaoliao();

        // 4, 加芝麻醬
        jiazhimajiang();
    }

    // 原諒我, 這些個英文真是不知道怎麼說了, 以下方法名用拼音吧...
    // 非我所願

    private void tangmian() {
        System.out.println("熱乾麵入沸水鍋焯燙十幾秒");
    }

    private void zhuangwan() {
        System.out.println("熱乾麵撈出, 裝入大碗中");
    }

    private void jiatiaoliao() {
        System.out.println("加調料");
    }

    private void jiazhimajiang() {
        System.out.println("加芝麻醬");
    }
}複製程式碼

各分店都用這套流程來製作熱乾麵:

public class XiaoGuang {

    public static void main(String[] args) {

        HotDryNoodlesMaker maker = new HotDryNoodlesMaker();
        maker.make();
    }
}複製程式碼
熱乾麵入沸水鍋焯燙十幾秒
熱乾麵撈出, 裝入大碗中
加調料
加芝麻醬複製程式碼

看起來沒有問題. 小光是個謹慎的人, 在全面啟用這套流程之前, 小光決定現在光谷店試用下.

試用出了問題

果然, 只有經過使用者的檢驗的產品才是好產品, 剛試用第一天, 小光的熱乾麵製作流程就經受考驗, 出了問題.

吃客們, 有的是在店吃的, 有的是打包帶走的.
當初小光為了品牌性, 在店吃都是用的帶有小光熱乾麵專屬標誌的消毒碗的, 外帶是定製的一次性碗具

現在在"裝碗"這個環節出了問題~~

解決問題

解決問題的第一步是描述問題.

小光深入思考, 探索問題的本質, 發現現在的問題是: 熱乾麵製作必須是這個流程, 而中間的某些步驟又必須可以根據實際場景改變. 也就是說, 不能改變熱乾麵的製作流程, 但是又可以改變(重新定義)這個流程中的某一步(裝碗).

如此, 小光想到了一個辦法:

用一個抽象的類來定義操作步驟(當然並不會抽象所有的步驟), 由具體的子類來實現中間需要定製的步驟.

抽象的製作流程:

public abstract class Maker {

    public void make() {
        // 1, 燙麵
        tangmian();

        // 2, 裝碗
        zhuangwan();

        // 3, 加調料(鹽,雞精,胡椒粉之類)
        jiatiaoliao();

        // 4, 加芝麻醬
        jiazhimajiang();
    }

    private void tangmian() {
        System.out.println("熱乾麵入沸水鍋焯燙十幾秒");
    }

    private void jiatiaoliao() {
        System.out.println("加調料");
    }

    private void jiazhimajiang() {
        System.out.println("加芝麻醬");
    }

    // 將裝碗這一步抽象出來, 由具體的子類實現
    abstract void zhuangwan();
}複製程式碼

然後分別弄一個對應的打包的maker和一個堂食的maker:

// 打包
public class PackingMaker extends Maker {

    @Override
    void zhuangwan() {
        System.out.println("熱乾麵撈出, 裝入一次性碗");
    }
}

// 堂食
public class EatInMaker extends Maker {

    @Override
    void zhuangwan() {
        System.out.println("熱乾麵撈出, 裝入店內消毒碗");
    }
}複製程式碼

如此這般, 製作的流程有maker控制, 但是也給了不同實現(打包, 堂食)一些自主化空間:

Maker packingMaker = new PackingMaker();
packingMaker.make();

// output
熱乾麵入沸水鍋焯燙十幾秒
熱乾麵撈出, 裝入一次性碗
加調料
加芝麻醬

Maker eatInMaker = new EatInMaker();
eatInMaker.make();

// output
熱乾麵入沸水鍋焯燙十幾秒
熱乾麵撈出, 裝入店內消毒碗
加調料
加芝麻醬複製程式碼

達成要求.

故事之後

照例我們先縷縷類的關係:

統一熱乾麵的製作流程---模板方法

在故事過程中, 我們描述了問題及其解決之道. 實際上這個解決之道就是我們今天要說的模板方法模式.

模板方法模式
定義一個操作中的演算法骨架(熱乾麵的製作流程), 而將某些步驟實現延遲到子類中. 使得子類可以根據實際情況不改變演算法骨架(熱乾麵的製作流程), 但是可以重新定義或改變該演算法中的某些特定步驟(例如裝碗).

擴充套件閱讀一

模板方法是程式碼複用的基本技術, 基本上隨處可見. 我們平常編碼上, 如果想複用一些程式碼, 基本上第一個想到的就是提取父類, 抽象可變, 然後子類實現抽象部分的方式來複用. 沒錯, 這種用法往往你就用到模板方法模式.

所以說, 設計模式並不高深, 它實際上就是一種良好程式設計習慣的提煉. 我們可能隨處再用, 只是有的時候我們並沒有意識到.

這種程式碼複用的方式在一些底層庫, SDK中更是被頻繁使用. 例如Android中最為複雜之一的View, 就用到木板方法模式.

其draw()方法就是模板方法, onDraw()是抽象的可定製的原語操作:

// 程式碼節選自View.java

// draw()方法相當於定義了一個View的繪製演算法結構, 其中的onDraw的可變的原語操作.
public void draw(Canvas canvas) {

   // Step 1, draw the background, if needed
   int saveCount;

   if (!dirtyOpaque) {
       drawBackground(canvas);
   }

   // skip step 2 & 5 if possible (common case)
   final int viewFlags = mViewFlags;
   boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
   boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

   if (!verticalEdges && !horizontalEdges) {
       // Step 3, draw the content
       if (!dirtyOpaque) onDraw(canvas);

       // Step 4, draw the children
       dispatchDraw(canvas);

       // Step 6, draw decorations (scrollbars)
       onDrawScrollBars(canvas);

       if (mOverlay != null && !mOverlay.isEmpty()) {
           mOverlay.getOverlayView().dispatchDraw(canvas);
       }

       // we're done...
       return;
   }
}  

/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
// 原語操作, 由子類實現, 來定製
protected void onDraw(Canvas canvas) {
}複製程式碼

擴充套件閱讀二

很多繼承關係中, 我們都能找到模板方法的影子, 但是並非所有的繼承就是用到了模板方法模式. 有的同學可能會回想起我們之前提到的策略模式. 二者看起來很像啊, 貌似都是跟替換演算法有關的.

在這裡簡單區別下二者:

二者都是通過用繼承關係來達到"演算法替換"的目的.
但是模板方法是不改變演算法結構, 只替換/改變其中的步驟.
而策略模式是改變了整個演算法.

打個比方, 我從北京到武漢的過程, 定義一個回家演算法 --- 做火車回家, 分成幾步:

  1. 上火車站買票
  2. 檢票上車
  3. 一路順風回家

如果我改變了買票的方式, 例如我不想去火車站買票了, 在網上買, 或是電話買. 但是演算法結構不變, 還是做火車, 還是這三步, 那麼這個就是模板方法模式.

但是如果我不想做火車了, 我開車, 我坐飛機回家, 那麼就相當於替換了整個演算法, 這個就是策略模式.


總之, 小光又解決了一個問題, 解決使用者痛點是產品的生存之道, 小光知道自己的熱乾麵正在變得越來越好...哈哈.

相關文章