賣熱乾麵的啟發 ---Builder 模式

anly_jun發表於2016-11-14

看個故事

網際網路寒冬來襲, 小光越來越覺得碼農這個行當不太好混了. 年關將至, 思鄉之情也是倍切. 心底一橫, 要不直接回老家做點小買賣得了~

說做就做, 小光辭了工作, 回到老家武漢, 做起了賣熱乾麵的行當.
小光秉著科學開店, 合理經營的心思, 走訪老店, 探索人流, 最終把店開在了軟體園旁邊.

經過仔細他發現, 每個人點熱乾麵時, 要的配料還各不相同, 有的要蔥花, 有的不要蔥花要香菜, 有的不要辣, 有的要加酸菜......
小光心想, 這可難不倒我, 你要什麼不要什麼一次性告訴我, 不就得了. 很快, 熱乾麵程式出爐:

public class HotDryNoodles {

    private boolean addShallot;
    private boolean addParsley;
    private boolean addChili;
    private boolean addSauerkraut;

    public HotDryNoodles(boolean shallot, boolean parsley, boolean chili, boolean sauerkraut) {
        this.addShallot = shallot;
        this.addParsley = parsley;
        this.addChili = chili;
        this.addSauerkraut = sauerkraut;
    }

    @Override
    public String toString() {

        StringBuilder builder = new StringBuilder("A bowl of hot-dry noodles has:");

        if (this.addShallot) {
            builder.append("蔥花.");
        }

        if (this.addParsley) {
            builder.append("香菜.");
        }

        if (this.addChili) {
            builder.append("辣椒.");
        }

        if (this.addSauerkraut) {
            builder.append("酸菜.");
        }

        return builder.toString();
    }
}複製程式碼

注意: 建構函式的引數是有順序的(蔥花.香菜.辣椒.酸菜).

你還別說, 大武漢的碼農們還真挺愛吃熱乾麵, 一會生意就上門了:

A: 老闆, 熱乾麵一份, 加蔥, 加香菜, 放點酸菜.
B: 老闆, 熱乾麵, 加蔥, 放點酸菜.

小光, 一聲"好嘞", 立馬開工:

public class XiaoGuang {

    public static void main(String[] args) {

        // A
        HotDryNoodles noodlesA = new HotDryNoodles(true, true, false, true);
        System.out.println("Customer A wants: " + noodlesA);

        // B
        HotDryNoodles noodlesB = new HotDryNoodles(true, false, false, true);
        System.out.println("Customer B wants: " + noodlesB);
    }
}複製程式碼

很快出爐:

Customer A wants: A bowl of hot-dry noodles has:蔥花.香菜.酸菜.
Customer B wants: A bowl of hot-dry noodles has:蔥花.酸菜.複製程式碼

完美~

這時, 來了一哥們兒: 老闆, 一碗熱乾麵, 加辣椒, 不放蔥.
小光吭哧吭哧忙活上了, 結果一端出來, 人哥們兒不樂意了, 怎麼就放蔥了啊, 還沒有放辣椒~~
原來是小光搞錯了順序(引數順序)~ (也怪這哥們不按套路出牌啊)

沒有辦法, 重做一份咯~ (為了使用者體驗)

收攤之後, 小光是痛定思痛啊, 現在的工序的確很容易出問題啊:

  • 目前的(引數)順序是固定的, 客戶要求各異, 根本不按套路來, 就很容易出錯.
  • 目前的(引數)個數是固定的, 大多數客戶都選擇預設, 只額外提一兩個要求.

小光思考著怎麼改進, 突然靈光一現:

這個不就是構建物件時有大量可選引數的問題嗎?
我可以使用Builder模式來解決這個問題啊. 原來程式設計無處不在, 小光暗思.

然後就開始動手改造工序了:

  • 他買了張桌子, 用來作為配料臺(Builder)
  • 把配料(蔥花.香菜.辣椒.酸菜)從他的熱乾麵(HotDryNoodlesWithBuilder)構造中移到配料臺放著了.
  • 讓客戶自己選擇新增什麼配料, 自己從配料臺上選擇新增.

那麼, 新增配料的過程程式設計自取了:

賣熱乾麵的啟發 ---Builder 模式

如此一來, 他不僅僅釋放了自己的勞動力, 也不用費力去記客戶到底要哪些不要哪些的, 把基本的面做好, 放在調料臺, 讓使用者自己新增吧~~

改造後的熱乾麵工序:

public class HotDryNoodlesWithBuilder {

    private boolean addShallot;
    private boolean addParsley;
    private boolean addChili;
    private boolean addSauerkraut;

    public HotDryNoodlesWithBuilder(Builder builder) {
        this.addShallot = builder.addShallot;
        this.addParsley = builder.addParsley;
        this.addChili = builder.addChili;
        this.addSauerkraut = builder.addSauerkraut;
    }

    @Override
    public String toString() {

        StringBuilder builder = new StringBuilder("A bowl of hot-dry noodles has:");

        if (this.addShallot) {
            builder.append("蔥花.");
        }

        if (this.addParsley) {
            builder.append("香菜.");
        }

        if (this.addChili) {
            builder.append("辣椒.");
        }

        if (this.addSauerkraut) {
            builder.append("酸菜.");
        }

        return builder.toString();
    }


    public static class Builder {

        private boolean addShallot;
        private boolean addParsley;
        private boolean addChili;
        private boolean addSauerkraut;

        public Builder() {

        }

        public Builder withShallot() {
            this.addShallot = true;
            return this;
        }

        public Builder withParsley() {
            this.addParsley = true;
            return this;
        }

        public Builder withChili() {
            this.addChili = true;
            return this;
        }

        public Builder withSauerkraut() {
            this.addSauerkraut = true;
            return this;
        }

        public HotDryNoodlesWithBuilder build() {
            return new HotDryNoodlesWithBuilder(this);
        }
    }

}複製程式碼

來看看使用者怎麼用的:

public class XiaoGuang {

    public static void main(String[] args) {

        // with builder
        HotDryNoodlesWithBuilder noodlesC = new HotDryNoodlesWithBuilder.Builder()
                .withChili()
                .withParsley()
                .build();
        System.out.println("Customer C wants: " + noodlesC);

        HotDryNoodlesWithBuilder noodlesD = new HotDryNoodlesWithBuilder.Builder()
                .withChili()
                .withParsley()
                .withSauerkraut()
                .withShallot()
                .build();
        System.out.println("Customer D wants: " + noodlesD);
    }
}複製程式碼

結果不能太滿意:

Customer C wants: A bowl of hot-dry noodles has:香菜.辣椒.
Customer D wants: A bowl of hot-dry noodles has:蔥花.香菜.辣椒.酸菜.複製程式碼

再也沒有使用者抱怨說小光搞錯了, 想要什麼自己加, 順序也無所謂了. 小光也有更多的時間服務更多的客戶了...
看著來來往往的吃客, 小光憧憬著: 眼看就可以開分店, 連鎖店, 上市, 當董事長, 迎娶白富美, 走上人生巔峰了, 哈哈哈哈哈.

故事之後

讓我們重溫下GoF設計模式中的Builder模式:

賣熱乾麵的啟發 ---Builder 模式
builde

Buidler模式, 是一種建立型的設計模式.
通常用來將一個複雜的物件的構造過程分離, 讓使用者可以根據需要選擇建立過程.
另外, 當這個複雜的物件的構造包含很多可選引數時, 那Builder模式可以說是不二之選了.

從這個故事中, 我們可以看到, 當熱乾麵的配料引數太多時, 我們很難去控制, 一旦弄錯了引數順序, 客戶就無法接受~

再來看下本故事中改造後的工序對應Builder模式的關係:

賣熱乾麵的啟發 ---Builder 模式
builder-2

一般來說, Builder常常作為實際產品的靜態內部類來實現(提高內聚性).
故而Product,Director, Builder常常是在一個類檔案中, 例如本例中的HotDryNoodlesWithBuilder.java.
這裡為了更好的對應Builder模式的類圖關係, 將HotDryNoodlesWithBuilder畫了兩個~.

擴充套件閱讀

作為Java/Android開發者, 如果你想設計一個通用的庫, Builder模式幾乎肯定是會用到的, 我們需要提供良好的入口/介面來使用者可以彈性地構造他們需要的物件.

像一些優秀的開源庫, 例如Glide, OkHttp等都在構造Glide, OkHttpClient時用到Builder模式, 例如OkHttpClient的建立, 如下節選OkHttpClient.java的程式碼:

public class OkHttpClient implements Cloneable, Call.Factory {
  public OkHttpClient() {
    this(new Builder());
  }

  private OkHttpClient(Builder builder) {
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
    this.connectionSpecs = builder.connectionSpecs;
    this.interceptors = Util.immutableList(builder.interceptors);
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    this.proxySelector = builder.proxySelector;
    this.cookieJar = builder.cookieJar;
    this.cache = builder.cache;
    this.internalCache = builder.internalCache;
    this.socketFactory = builder.socketFactory;

    // more code...
  }

  public static final class Builder {

    public Builder() {
      ...
    }


    public Builder cache(Cache cache) {
      ...
    }

    public Builder dispatcher(Dispatcher dispatcher) {
      ...
    }

    public Builder protocols(List protocols) {
       ...
    }

    public List networkInterceptors() {
      ...
    }

    public Builder addNetworkInterceptor(Interceptor interceptor) {
      ...
    }

    public OkHttpClient build() {
      return new OkHttpClient(this);
    }
  }
}複製程式碼

是不是和小光設計的HotDryNoodlesWithBuilder很像...

生活還將繼續, 小光繼續奮鬥著, 憧憬自己的人生巔峰夢~

相關文章