第22次文章:建造者模式+原型模式
- 2019 年 10 月 8 日
- 筆記
這周我們就可以把GOFO23設計模式中的創建型模式全部介紹完了!後面在項目裡面可以試一下啦!
一、建造者模式
1、場景
(1)我們需要建造一個複雜的產品。比如:神州飛船,iPhone。這個複雜的產品的創建。有這樣一個問題需要處理:裝配這些子組件是不是有個步驟問題?
(2)實際開發中,我們所需要的對象構建有時也非常複雜,有許多步驟需要處理時。
2、本質
(1)分離了對象子組件的單獨構造(由Builder來負責)和裝配(由director負責)。從而可以構造出複雜的對象。這個模式適用於:某個對象的構造過程複雜的情況下使用。
(2)由於實現了構建和裝配的解耦。不同的構造器,相同的裝配,可以做出不同的對象;相同的構造器,不同的裝配順序也可以做出不同的對象,也就是實現了構建演算法、裝配演算法的解耦,實現了更好的復用。
3、建造者模式實例化
(1)背景
現在我們假設一個背景,我們需要建造一個一艘飛船,裡面需要用到3個零部件,分別是發動機,軌道艙和逃逸塔。首先我們需要構造一個飛船類,以及相應的發動機,軌道艙和逃逸塔類。對於一艘飛船,我們需要3個零部件。主要的實現思路如下:
public class AirShip { private Engine engine;//發動機 private OrbitalModule orbitalModule;//軌道艙 private EscapeTower escapeTower;//逃逸塔 public Engine getEngine() { return engine; } public void setEngine(Engine engine) { this.engine = engine; } public OrbitalModule getOrbitalModule() { return orbitalModule; } public void setOrbitalModule(OrbitalModule orbitalModule) { this.orbitalModule = orbitalModule; } public EscapeTower getEscapeTower() { return escapeTower; } public void setEscapeTower(EscapeTower escapeTower) { this.escapeTower = escapeTower; }} class Engine{ private String name; public Engine(String name) { super(); this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; }} class OrbitalModule{ ......}class EscapeTower{ ......}
tips:OrbitalModule和EscapeTower類中實現的程式碼與Engine中的程式碼類似,所以在上面的程式碼塊中省略。
(2)builder和director的構建
在建造者模式中,最核心的部分就是將零部件的創造和組裝進行分離,使得整個創建飛船的每一個步驟都相對簡潔。所以我們根據建造飛船的流程,分別構建兩個介面AirShipBuilder(負責零部件的構建)和AirShipDirector(負責零部件的組裝)。
public interface AirShipBuilder { OrbitalModule builderOrbitalModule(); Engine builderEngine(); EscapeTower builderEscapeTower();}
public interface AirShipDirector { AirShip directAirShip();}
(3)實體化構建
經過上述的基本準備,已經將一個飛船類的全部流程節點規劃好了,接下來我們通過上述流程,實現一個具體的飛船對象PengAirShip,依次構建PengAirShipBuilder和PengAirShipDirector。
public class PengAirShipBuilder implements AirShipBuilder{ @Override public OrbitalModule builderOrbitalModule() { System.out.println("建造軌道艙"); return new OrbitalModule("pengAirShip軌道艙"); } @Override public Engine builderEngine() { System.out.println("建造發動機"); return new Engine("pengAirShip發動機"); } @Override public EscapeTower builderEscapeTower() { System.out.println("建造逃逸塔"); return new EscapeTower("pengAirShip逃逸塔"); }}
public class PengAirShipDirector implements AirShipDirector { private PengAirShipBuilder builder; public PengAirShipDirector(PengAirShipBuilder builder) { super(); this.builder = builder; } @Override public AirShip directAirShip() { AirShip airShip = new AirShip(); airShip.setEngine(builder.builderEngine()); airShip.setEscapeTower(builder.builderEscapeTower()); airShip.setOrbitalModule(builder.builderOrbitalModule()); return airShip; }}
(4)完成構建
最後我們通過一個簡單的測試,來實現PengAirShip構建的全部操作。
public class Client { public static void main(String[] args) { PengAirShipDirector a = new PengAirShipDirector(new PengAirShipBuilder()); AirShip airship = a.directAirShip(); System.out.println(airship.getEngine().getName()); System.out.println(airship.getEscapeTower().getName()); System.out.println(airship.getOrbitalModule().getName()); }}
測試結果:

tips:我們可以在結果中看出建造的過程資訊,以及最後的各個零部件的資訊。
4、總結
上周我們一起聊了聊工廠模式,我們現在回過來將兩者進行一個對比。會感到兩者甚至有點相似,都是將在滿足客戶端要求的前提下,儘可能的通過構建一系列的流程方法,簡化客戶端的操作步驟。在工廠模式中,我們通過構建一系列的工廠來實現生產一個實體類對象,相當於一個工廠同時兼任了零部件的構造以及零部件的組裝兩個工作。而建造者模式的優點就在於將構建和組裝進行分離,互相解耦。建造者模式的核心思想在於將工廠的作用再次進行細化。一個工廠僅僅負責一個職能,使得對每個工廠的管理更加方便。相互的關聯也更少。
二、原型模式prototype
1、場景
通過new產生一個對象需要非常繁瑣的數據準備或訪問許可權,則可以使用原型模式。
2、原型模式
(1)就是java中的克隆技術,以某個對象為原型,複製出新的對象。顯然,新的對象具備原型對象的特點。
(2)優勢:效率高(直接克隆,避免了重新執行構造過程步驟)。
(3)克隆類似於new,但是不同於new。new創建新的對象屬性採用的是默認值。克隆出的對象的屬性值完全和原型對象相同。並且克隆出的新對象改變不會影響原型對象。然後,再修改克隆對象的值。
3、原型模式實現
在原型模式中,我們需要在需要實現克隆的類上實現Cloneable介面,並且重寫clone方法。所以我們首先構建一個Sheep類。
public class Sheep implements Cloneable{ private String name; private Date birthday; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public Sheep(String name, Date birthday) { super(); this.name = name; this.birthday = birthday; }.....}
tips:省略號的地方是兩個屬性name 和 birthday的set與get方法。需要注意的是,方法clone()屬於類Object的方法,而不是Cloneable介面中的方法。我們打開Cloneable介面的源碼可以發現,此介面是一個空介面,裡面沒有任何方法。這也就代表著Cloneable介面僅僅屬於一個標識符。
(1)淺複製
使用clone方法的時候,在JVM的內部,使用的並不是值的複製,而是將原生對象的屬性地址傳遞給了新對象。稍微思考,這樣的機制就會產生一個問題:假如我們將一個原生對象屬性值的地址傳遞給一個新對象,然後該屬性的值假如有所變化,那麼緊接著,新對象該屬性的值也會跟著變化,這樣就無法和原對象的值保持一致了。這就是所謂的淺複製,我們結合下面的例子來說明。
public class Client { public static void main(String[] args) throws CloneNotSupportedException { Date date = new Date(351454652132654312L); Sheep s1 = new Sheep("多利",date); Sheep s2 = (Sheep) s1.clone(); System.out.println(s1); System.out.println(s1.getName()); System.out.println(s1.getBirthday()); date.setTime(546565454654635L); System.out.println("多利羊的新出生日期:"+s1.getBirthday()); System.out.println("n"+"###########################################"+"n"); System.out.println(s2); System.out.println(s2.getName()); System.out.println(s2.getBirthday()); System.out.println("n"+"可以修改s2中的值"); s2.setName("邵莉"); System.out.println(s2.getName()); }}
我們查看一下結果:

tips:
1.首先我們看一下兩個紅色方框,我們在程式中列印出了兩個類s1和s2,s2是使用clone()方法得到新對象。可以在紅色方框中可以看出,兩個對象s1和s2屬於兩個不同的對象,由此證明,clone()方法產生的是新對象,而不是對原對象的引用。
2.我們關注一下藍色方框中的日期,在程式的最開始,我們隨機設置了一個日期date,列印出來的日期為藍色方框中的日期。並將其設置為原對象s1的birthday屬性值。在完成克隆操作之後,我們對date值進行更改,可以發現,s2的birthday也同時改變為新的date值,與s1對象最開始的birthday值不同(橘色方框)。這種結果的原因就是克隆方法的基本原理是基於地址的複製,而並非真實值的複製。所以這就是淺複製。
(2)深複製
面對上面的淺複製,在很多情況下是不滿足我們的需求的,既然是克隆,那麼我們的需求應該是克隆出的對象,與原對象的所有值都是相同的,而不會隨著外界屬性值的改變而有所改變,這樣才更加符合我們的需求。面對這樣需求,我們需要在源頭上進行克服。主要是在clone()方法的內部,對birthday進行再次克隆。所以我們在Sheep的基礎上加以修改clone()方法,對Sheep的birthday屬性進行克隆,由此就可以解決淺克隆問題。修改如下:
public class Sheep2 implements Cloneable,Serializable{ private String name; private Date birthday; @Override protected Object clone() throws CloneNotSupportedException { Object obj = super.clone(); Sheep2 s = (Sheep2) obj; s.birthday = (Date) this.birthday.clone();//把屬性也進行克隆 return s; } ......}
tips:Sheep2與Sheep的主要區別就在於clone()方法的修改,從Sheep僅僅克隆對象,到Sheep2中克隆屬性,從而避免了淺克隆問題。
然後我們繼續使用上面測試程式碼,檢查一下修正過後的Sheep2類型。結果如下:

tips:我們觀察上圖中的紅色方框,第一個紅色方框表示s1最開始的birthday日期,s2表示date經過修改過後,s2的birthday日期。橘色方框表示birthday修改後的日期。可以發現,雖然date經過修改,但是s2的birthday並沒有改變。這就是由於在date修改之前,s2克隆了s1中birthday的屬性值,此時s2的birthday屬性值就不再依賴於date中的值了。這就是深克隆的思想。
4、使用序列化和反序列化技術實現克隆
在前面的多次文章中,我們提到過序列化和反序列化的內容。就屬於將二進位值的輸入與輸出。通過這種機制,我們也可以完成一個對象的克隆。具體的實現方案如下;
public class Client3 { public static void main(String[] args) throws Exception { Date date = new Date(351454652132654312L); Sheep2 s1 = new Sheep2("多利",date); System.out.println(s1); System.out.println(s1.getName()); System.out.println(s1.getBirthday()); // 利用序列化和反序列化進行深複製 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(s1); byte[] bytes = bos.toByteArray(); ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis); Sheep2 s2 = (Sheep2) ois.readObject();//通過序列化把s1的屬性值都進行複製 // 重新設置時間 date.setTime(5465654546635L); System.out.println("修改後的日期:"+s1.getBirthday()); System.out.println("n"+"###########################################"+"n"); System.out.println(s2); System.out.println(s2.getName()); System.out.println(s2.getBirthday()); System.out.println("n"+"可以修改s2中的值"); s2.setName("邵莉"); System.out.println(s2.getName()); }}
tips:序列化與反序列化實現的克隆,屬於深克隆,所得結果與上面的深克隆結果一致,此處不再粘貼結果。僅僅將實現方法粘貼出來,作為一種對於克隆模式的思考和拓展。
5、原型模式中幾個注意點
(1)原型模式產生新對象的方法的效率大大高於new方法產生新對象的效率,所以當需要產生大量類似對象時,優先考慮原型模式。
(2)原型模式主要是用來產生新對象,克隆之後得到的新對象,只是獲得了原始對象全部特性,我們依舊可以根據自己的需求更改新對象的一些屬性值。
(3)原型模式很少單獨出現,一般是和工廠方法模式一起出現,通過clone的方法創建一個對象,然後由工廠方法提供給調用者。
三、創建型模式的總結
創建型模式:都是用來幫助我們創建對象的
1、單例模式
保證一個類只有一個實例,並且提供一個訪問該實例的全局訪問點
2、工廠模式
(1)簡單工廠模式:用來生產同一等級結構中的任意產品。(對於增加新的產品,需要修改已有程式碼)
(2)工廠方法模式:用來生產同一等級結構中的固定產品。(支援增加任意產品)
(3)抽象工廠模式:用來生產不同產品族的全部產品。(對於增加新的產品,無能為力;支援增加產品族)
3、建造者模式
分離了對象子組件的單獨構造(由Builder來負責)和裝配(由Director負責)。從而可以構造出複雜的對象。
4、原型模式
通過new產生一個對象需要非常繁瑣的數據準備或訪問許可權,則可以使用原型模式。