創業街分店開張啦 — 原型模式

anly_jun發表於2019-02-26

前情提要

上集講到, 小光請來堂哥大龍作為自己的代理與飲品供應商談判, 最終大龍用自己豐富的商場經驗幫小光拿到合適的價格.

小光也是嚐到了代理的甜頭, 開始將店裡的更多工作交給表妹來大理, 自己騰出功夫去選新的分店地址了.

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

新店建設

根據光谷店的經營經驗, 很快, 小光就選好了分店的地址—創業街. 還是為了造福廣大屌絲單身程式猿們啊, 哈哈.

分店的建設相對第一家店的開闢來說也是簡單了很多, 在光谷店的探索, 諸如熱乾麵生產流程, 飲料機機制, 活動策略等都可以複製過來用. 簡單來說, 就是複製成功原型, 如下:

照例, 抽象出一個公司的類:

public class Company implements Cloneable {

    // 此處我們假裝省略了N多, 諸如活動策略, 飲料機, 熱乾麵生產流程等.
    // 再此僅以飲品為例
    private ArrayList<String> drinks = new ArrayList<>();

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addDrink(String drink) {
        drinks.add(drink);
    }

    @Override
    protected Company clone() {

        Company company = null;
        try {
            company = (Company) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return company;
    }

    @Override
    public String toString() {
        return "{" +
                "名字: `" + getName() + ``` +
                ", 飲品: " + drinks  + ``` +
                `}`;
    }
}複製程式碼

光谷店:

public class OpticalValleyCompany extends Company {

    public OpticalValleyCompany() {
        setName("光谷軟體園分店");
        addDrink("橙汁");
        addDrink("可樂");
        addDrink("酸梅湯");
    }
}複製程式碼

看下小光是如何複製光谷店的成功, 建立新的創業街分店的:

public class XiaoGuang {

    public static void main(String[] args) {

        // new 光谷店
        Company ovCompany = new OpticalValleyCompany();
        System.out.println("光谷店: " + ovCompany);

        // 在光谷店的基礎上clone SBI店
        Company sbiCompany = ovCompany.clone();
        sbiCompany.setName("創業街分店");
        System.out.println("SBI店: " + sbiCompany);
    }
}複製程式碼

output:

光谷店: {名字: `光谷軟體園分店`, 飲品: [橙汁, 可樂, 酸梅湯]`}
SBI店: {名字: `創業街分店`, 飲品: [橙汁, 可樂, 酸梅湯]`}複製程式碼

看樣子很成功, 小光開始準備試運營了.

試運營

小光信心滿滿的開始了新店的試運營. 為了慶祝分店開張, 小光新拿了一款飲料XDrink在新店做活動, 買熱乾麵贈送飲料.

// 在光谷店的基礎上clone SBI店
Company sbiCompany = ovCompany.clone();
sbiCompany.setName("創業街分店");

// 給SBI店新增一款飲品
sbiCompany.addDrink("雪碧");

System.out.println("SBI店: " + sbiCompany);複製程式碼

這時, SBI店的飲品列表是:

SBI店: {名字: `創業街分店`, 飲品: [橙汁, 可樂, 酸梅湯, 雪碧]`}複製程式碼

看著很好, Perfect.
然而, 這時, 表妹打來電話了, 說我光谷店這邊的選單系統怎麼無端多出一款雪碧的飲料啊, 我這沒有提供的啊, 怎麼給客戶啊.

小光立馬列印了下光谷店的資訊(基於上面的修改):

// 在光谷店的基礎上clone SBI店
Company sbiCompany = ovCompany.clone();
sbiCompany.setName("創業街分店");

// 給SBI店新增一款飲品
sbiCompany.addDrink("雪碧");

System.out.println("SBI店: " + sbiCompany);

// 列印下光谷店ovCompany
System.out.println("光谷店: " + ovCompany);複製程式碼

果然, 光谷店新增了”雪碧”,

SBI店: {名字: `創業街分店`, 飲品: [橙汁, 可樂, 酸梅湯, 雪碧]`}
光谷店: {名字: `光谷軟體園分店`, 飲品: [橙汁, 可樂, 酸梅湯, 雪碧]`}複製程式碼

這樣當然是不好的咯, 小光只想複製光谷店的基本流程架構過來, 後續兩個店的某些方面還是要分開發展的, 可不能一改俱改啊.

改進之路

小光又開始了clone的改進之路. 先回頭看下, 小光之前是怎麼clone的:

@Override
protected Company clone() {

   Company company = null;
   try {
       company = (Company) super.clone();
   } catch (CloneNotSupportedException e) {
       e.printStackTrace();
   }
   return company;
}複製程式碼

我們注意到, 這個clone只是clone了Company, 並沒有clone Company內部的引用(ArrayList drinks). 也就是說clone出來的物件和之前的物件會使用同一份drinks列表注1, 這顯然不是小光願意看到的.

小光也很快想到了解決方案, 改造了clone過程:

@Override
protected Company clone() {

   Company company = null;
   try {
       company = (Company) super.clone();

       // 對於物件的屬性也加以clone
       company.drinks = (ArrayList<String>) this.drinks.clone();
   } catch (CloneNotSupportedException e) {
       e.printStackTrace();
   }
   return company;
}複製程式碼

這次小光不僅clone了Company, 還clone了其屬性值drinks注2.

讓我們來看下小光的成果:

和之前同樣的使用:

// new 光谷店
Company ovCompany = new OpticalValleyCompany();
System.out.println("光谷店: " + ovCompany);

// 在光谷店的基礎上clone SBI店
Company sbiCompany = ovCompany.clone();
sbiCompany.setName("創業街分店");

// 給SBI店新增一款飲品
sbiCompany.addDrink("雪碧");

System.out.println("SBI店: " + sbiCompany);
System.out.println("光谷店: " + ovCompany);複製程式碼

改造後的結果:

光谷店: {名字: `光谷軟體園分店`, 飲品: [橙汁, 可樂, 酸梅湯]`}
SBI店: {名字: `創業街分店`, 飲品: [橙汁, 可樂, 酸梅湯, 雪碧]`}
光谷店: {名字: `光谷軟體園分店`, 飲品: [橙汁, 可樂, 酸梅湯]`}複製程式碼

我們看到, 光谷店不會因為SBI店的改變而改變了.
小光熱乾麵SBI店試運營正式開始, 歡迎大家光臨咯…

故事之後

我們在故事中多次提到了clone, 原型. 沒錯, 這個就是原型模式. 照例, 我們來梳理下類之間的關係, 相對簡單:

創業街分店開張啦 — 原型模式

原型模式:
通過原型物件例項, 使用clone的方式來快速建立一個新的(與原型物件例項一致的)物件例項.

由於原型模式較為通用, 且相對簡單, Java中的最基類Object已經提供了clone方法, 來方便我們複製出新的物件例項.

擴充套件閱讀一

上述故事中, 我們在某些加了注1, 注2的標籤. 這就是我們今天的擴充套件閱讀一要注意的內容:

注1 淺拷貝
注2 深拷貝

其實, 跟隨故事我們也大致瞭解了淺拷貝和深拷貝的區別:

  • 淺拷貝對於要克隆的物件, 會複製其基本資料型別(包括String)的屬性(本例中的name屬性)的給新的物件. 而對於非基本資料型別的屬性(本例中的drinks), 僅僅複製一份引用給新產生的物件, 即新產生的物件和原始物件中的非基本資料型別的屬性都指向的是同一個物件.
  • 深拷貝 對於要克隆的物件, clone出的非基本資料型別的屬性(要求屬性也實現了Cloneable介面, ArrayList就已經自帶實現了)不再是和原物件指向同一個物件了, 而是一個新的clone出來的屬性物件例項.

如下:

創業街分店開張啦 — 原型模式

擴充套件閱讀二

如果我們檢視java原始碼, 可以發現, 我們呼叫的clone()方法是Object物件的. 而不是Cloneable介面的. 那麼我們為什麼要實現Cloneable介面呢? 不識閒Cloneable介面可否呼叫Object的clone()方法呢?

我們先來看下Cloneable介面的原始碼:

public interface Cloneable {
}複製程式碼

發現其中並沒有任何方法. 幸運的是Java原始碼的java doc註釋足夠清晰:

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object`s clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 */複製程式碼

大體我們可以理解幾點:

  1. Cloneable可以看著是一個標識, 實現了改介面的類才能合法地呼叫其從Object類中繼承而來的clone()方法.
  2. 如果沒有實現Cloneable介面而呼叫clone()方法, 會觸發CloneNotSupportedException異常.
  3. 實現Cloneable介面的類應當重寫Object的clone()方法.

擴充套件閱讀三

原型模式也是一種建立型的設計模式, 一般會結合工廠模式一起使用, 來構建物件. 本例中就不擴充套件了.


好了, 小光熱乾麵創業街分店開張啦, 吃熱乾麵贈雪碧了, 歡迎大家光臨, 歡迎大家關注.

相關文章