从零开始学习Java设计模式 | 创建型模式篇:建造者模式
在本講,我們來(lái)學(xué)習(xí)一下創(chuàng)建型模式里面的最后一個(gè)設(shè)計(jì)模式,即建造者模式。
概述
建造者模式是指將一個(gè)復(fù)雜對(duì)象的構(gòu)建與表示分離,使得同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表示。
讀完這句話之后,我估計(jì)很多人都已經(jīng)懵了,這說(shuō)的是什么意思啊?哈哈哈😊,別急,下面我會(huì)給大家解釋一下,先看一下下面這張圖。
上圖右邊是一個(gè)主機(jī),而左邊則是主機(jī)里面包含的部件(或者組件),有主板、cpu、內(nèi)存條、風(fēng)扇、硬盤以及電源。
結(jié)合這張圖,我來(lái)向大家解釋一下建造者模式的概念。建造者模式的概念中不是有一個(gè)復(fù)雜對(duì)象嗎?你可以將其理解為上圖中的主機(jī)。為什么把它稱為復(fù)雜對(duì)象呢?因?yàn)橹鳈C(jī)里面是包含了很多很多部件的。其實(shí),建造者模式說(shuō)的就是將主機(jī)和主機(jī)里面的部件進(jìn)行了一個(gè)分離,分離之后有什么好處呢?解除耦合唄!另外,你可以把上圖中的那些組件裝配成一個(gè)主機(jī)的過(guò)程理解為建造者模式概念中的構(gòu)建過(guò)程。
而同樣的構(gòu)建過(guò)程,也即不同的裝配方式,最終創(chuàng)建出來(lái)的復(fù)雜對(duì)象(即主機(jī))是不同的。這個(gè)如何去理解呢?例如,我現(xiàn)在主板、cpu、內(nèi)存條都換了一個(gè)品牌,那么這時(shí)同樣的一個(gè)裝配過(guò)程最終出來(lái)的主機(jī)肯定和之前的是不一樣的,你想啊,我現(xiàn)在的內(nèi)存條選擇的是16G,之前有可能選擇的是8G,那現(xiàn)在裝配的主機(jī)肯定和之前的是不一樣的啊!
經(jīng)過(guò)我上面的解釋,相信大家一定理解了建造者模式。下面,我們?cè)賮?lái)看一下有關(guān)建造者模式的具體的描述。
-
建造者模式分離了部件的構(gòu)造(由Builder來(lái)負(fù)責(zé)構(gòu)造,Builder是建造者模式中的一個(gè)角色)和裝配(由Director負(fù)責(zé)裝配,Director也是建造者模式中的一個(gè)角色),從而就可以構(gòu)造出復(fù)雜的對(duì)象了,而且我們使用建造者模式最終要的就是這個(gè)復(fù)雜對(duì)象。這個(gè)模式適用于某個(gè)對(duì)象的構(gòu)建過(guò)程復(fù)雜的情況,若是這種情況,則可以使用建造者模式
-
由于實(shí)現(xiàn)了構(gòu)建和裝配的解耦,不同的構(gòu)建器,相同的裝配,也可以做出不同的對(duì)象,就拿我上面舉的例子來(lái)說(shuō),如果構(gòu)成主機(jī)組件的品牌發(fā)生了變化,那么以同樣的方式進(jìn)行組裝,出來(lái)的肯定是不同的對(duì)象;相同的構(gòu)建器,不同的裝配順序也可以做出不同的對(duì)象。也就是實(shí)現(xiàn)了構(gòu)建算法、裝配算法的解耦,這樣就實(shí)現(xiàn)了更好的復(fù)用
-
建造者模式可以將部件和其組裝過(guò)程分開(kāi),一步一步創(chuàng)建一個(gè)復(fù)雜的對(duì)象。用戶只需要指定復(fù)雜對(duì)象的類型就可以得到該對(duì)象,而無(wú)須知道其內(nèi)部的具體構(gòu)造細(xì)節(jié)。
這就跟我們?nèi)ベI一臺(tái)電腦一樣,我們不關(guān)注這臺(tái)電腦里面用的每個(gè)品牌的組件以及它是如何進(jìn)行裝配的,我們只關(guān)注最終買的是一臺(tái)電腦就行了
相信讀到這,大家對(duì)建造者模式又有了一個(gè)深刻的認(rèn)識(shí)。
結(jié)構(gòu)
建造者(Builder)模式包含有如下角色:
-
抽象建造者類(Builder):這個(gè)接口規(guī)定要實(shí)現(xiàn)復(fù)雜對(duì)象的哪些部分的創(chuàng)建,并不涉及具體的部件對(duì)象的創(chuàng)建,而是由具體建造者類來(lái)實(shí)現(xiàn)。
注意了,這里說(shuō)的接口并不是我們通常在Java里面定義的接口,而是指的是一種規(guī)范,既可以是Java里面的接口,又可以是抽象類,希望大家能明白這點(diǎn)~
-
具體建造者類(ConcreteBuilder):實(shí)現(xiàn)Builder接口,完成復(fù)雜產(chǎn)品的各個(gè)部件的具體創(chuàng)建方法。在構(gòu)造過(guò)程完成后,提供產(chǎn)品的實(shí)例。
注意了,建造者模式的核心思想并不在于復(fù)雜對(duì)象各個(gè)部件的創(chuàng)建,而是強(qiáng)調(diào)的是整個(gè)裝配的過(guò)程,至于對(duì)于這些部件的創(chuàng)建,我們既可以使用工廠方法模式,又可以使用抽象工廠模式
-
產(chǎn)品類(Product):要?jiǎng)?chuàng)建的復(fù)雜對(duì)象
-
指揮者類(Director):調(diào)用具體建造者來(lái)創(chuàng)建復(fù)雜對(duì)象的各個(gè)部分,在指揮者中不涉及具體產(chǎn)品的信息,它只負(fù)責(zé)保證對(duì)象各個(gè)部分完整創(chuàng)建或按某種順序創(chuàng)建。
也就是說(shuō),指揮者所要完成的工作就是裝配復(fù)雜對(duì)象。舉個(gè)例子,小妹需要一臺(tái)電腦來(lái)學(xué)Java,她想要我?guī)退M裝一臺(tái)電腦,在組裝電腦的過(guò)程中,我是不管cpu是什么牌子的,至于這個(gè)cpu是怎么去構(gòu)建的,究竟是小妹自己買的還是自己造的,我也通通不管,我就只負(fù)責(zé)裝配,很顯然這里指揮者類就是指我本人
了解了建造者模式里面包含的角色之后,接下來(lái)我們便來(lái)看一下下面的類圖,根據(jù)以下類圖我會(huì)再為大家講解一下建造者模式。
從以上類圖中可以看到,在抽象建造者類里面構(gòu)建了各個(gè)部分,比如buildPartA方法來(lái)構(gòu)建A部分,buildPartB方法來(lái)構(gòu)建B部分,而且這些方法都是抽象的,需要由具體的建造者去實(shí)現(xiàn),也即構(gòu)建具體的部件;再來(lái)看一下指揮者類,注意看,它是將Builder聚合進(jìn)來(lái)了,而且在construct這個(gè)組裝方法里面,還會(huì)將各個(gè)部件組裝成一個(gè)復(fù)雜對(duì)象喲;最后看一下具體建造者類,它除了要重寫抽象建造者類里面的抽象方法之外,還提供了一個(gè)獲取復(fù)雜對(duì)象的方法,即getResult,調(diào)用該方法我們就可以獲取到復(fù)雜對(duì)象。
以上類圖分析完了之后,接下來(lái)我們就通過(guò)一個(gè)案例再去深入地理解一下建造者模式,而這個(gè)案例就是創(chuàng)建共享單車,也就是生產(chǎn)自行車。
案例
生產(chǎn)自行車是一個(gè)復(fù)雜的過(guò)程,它包含了車架、車座等組件的生產(chǎn)。而車架又有碳纖維、鋁合金等材質(zhì),車座有橡膠、真皮等材質(zhì)。這樣,對(duì)于自行車的生產(chǎn),我們就可以使用建造者模式了,因?yàn)樯a(chǎn)的是自行車這個(gè)復(fù)雜對(duì)象。
很明顯,對(duì)于該案例而言,自行車就是一個(gè)復(fù)雜對(duì)象,因?yàn)樗锩姘泻芏嘟M件,而現(xiàn)在的組件只有車架和車座,但是肯定還有其他的一些組件,例如輪胎、輪轂等等,只不過(guò)在該案例里面我們就不進(jìn)行模擬了,而只是通過(guò)車架和車座這兩個(gè)組件來(lái)簡(jiǎn)單的模擬一下就可以了。
下面我們就來(lái)分析一下該案例里面要涉及到哪些類。
- 產(chǎn)品類:Bike類,包括了車架、車座等組件。注意了,在該案例中,我為了簡(jiǎn)單,就不再將車架和車座單獨(dú)設(shè)計(jì)成對(duì)應(yīng)的類了,而是直接以字符串的形式體現(xiàn)出來(lái)
- 抽象建造者類:Builder類,在該案例中,我是將其設(shè)計(jì)成了抽象類
- 具體建造者類:抽象建造者類的子實(shí)現(xiàn)類,在該案例中,我設(shè)計(jì)出來(lái)了兩個(gè)子實(shí)現(xiàn)類,分別是MobileBuilder(用來(lái)構(gòu)建摩拜單車)和OfoBuilder(用來(lái)構(gòu)建ofo單車)
- 指揮者類:Director類,它是用來(lái)裝配自行車的
明確了該案例里面所涉及到的類之后,你能不能畫出下面這樣一個(gè)類圖呢?要是畫不出來(lái),就看下面這張類圖吧!也不礙事。
先來(lái)看第一個(gè)類吧,即Client(客戶端),它是需要依賴于其他類的,我們先暫時(shí)不去關(guān)注它。
再來(lái)看一下Bike類(即產(chǎn)品類),可以看到它里面聲明了表示車架和車座這倆組件的屬性,不過(guò)這兒我們直接是以字符串的形式體現(xiàn)出來(lái)了,并且還為其提供了對(duì)應(yīng)的setter和getter方法。
看完產(chǎn)品類之后,我們?cè)賮?lái)看一下Builder類,可以看到它里面聲明了一個(gè)Bike類型的變量,這就相當(dāng)于是聚合了Bike類型的對(duì)象,此外,它里面還提供了三個(gè)抽象方法,一個(gè)buildFrame方法用于構(gòu)建車架,一個(gè)buildSeat方法用于構(gòu)建車座,一個(gè)createBike方法用于創(chuàng)建自行車,正是由于該方法是用于創(chuàng)建自行車的,所以該方法的返回值的類型是Bike。大家一定要注意了,由于Builder類是抽象建造者類(這里我將其設(shè)計(jì)成了抽象類),所以它里面提供的三個(gè)方法都是抽象方法。
正是由于Builder類是抽象建造者類,所以我們還要提供對(duì)應(yīng)的具體建造者類,從以上類圖中可以看到,這兒我們提供了兩個(gè)具體建造者類,分別是MobileBuilder和OfoBuilder,它倆肯定是要重寫父類中的三個(gè)抽象方法的。
最后,大家千萬(wàn)不要忘了最重要的一個(gè)類,即Director,它聚合了Builder類,因?yàn)樗猛ㄟ^(guò)Builder類去構(gòu)建具體的自行車,而且通過(guò)該類中的construct方法,我們還可以去控制自行車組裝的一個(gè)過(guò)程,因?yàn)樵摲椒ň褪怯糜诮M裝自行車的。此外,在該類里面我們還提供了一個(gè)有參構(gòu)造,這樣,該類就沒(méi)有無(wú)參構(gòu)造了。
以上類圖我就為大家分析至此了,接下來(lái),我們就要編寫代碼來(lái)實(shí)現(xiàn)以上生產(chǎn)自行車的案例了。
首先,打開(kāi)咱們的maven工程,在com.meimeixia.pattern包下新建一個(gè)子包,即builder.demo1,實(shí)現(xiàn)以上生產(chǎn)自行車案例的代碼我們都放在了該包下。
然后,創(chuàng)建產(chǎn)品類,即Bike。
package com.meimeixia.pattern.builder.demo1;/*** 產(chǎn)品對(duì)象* @author liayun* @create 2021-06-02 21:48*/ public class Bike {private String frame; // 車架private String seat; // 車座public String getFrame() {return frame;}public void setFrame(String frame) {this.frame = frame;}public String getSeat() {return seat;}public void setSeat(String seat) {this.seat = seat;} }接著,創(chuàng)建抽象建造者類,即Builder。
package com.meimeixia.pattern.builder.demo1;/*** @author liayun* @create 2021-06-03 5:16*/ public abstract class Builder {// 聲明Bike類型的變量,并進(jìn)行賦值protected Bike bike = new Bike();public abstract void buildFrame();public abstract void buildSeat();// 構(gòu)建自行車的方法public abstract Bike createBike();}從以上代碼中可以看到,我們是在Builder類中聲明了一個(gè)Bike類型的變量,并為其進(jìn)行了賦值,這是為啥呢?其實(shí)從以上類圖中我們也知道應(yīng)該這么做,它說(shuō)得很明白了,Builder類是要聚合Bike類型的對(duì)象的。
其二,是因?yàn)檫@樣做可以提高代碼的一個(gè)復(fù)用性,為什么這么說(shuō)呢?還是看一下以上類圖,你會(huì)發(fā)現(xiàn)我們得在具體建造者類(比如MobileBuilder和OfoBuilder)里面構(gòu)建自行車,既然這樣,那么它里面就得有一個(gè)自行車了,所以我們就把聲明一個(gè)Bike類型變量這件事放在父類(即抽象建造者類)中來(lái)做了,這樣做的話,就可以提高代碼的一個(gè)復(fù)用性了。
此外,大家還要注意一點(diǎn),在為Bike類型的變量賦值時(shí),我們是new了一個(gè)Bike類型的對(duì)象,而我想要告訴大家的是,該對(duì)象里面的組件(車架和車座)還并未組裝,就跟我們組裝一臺(tái)主機(jī)一樣,現(xiàn)在是只有一個(gè)機(jī)箱。如果要想給該對(duì)象組裝組件,那么這件事就得交給具體的指揮者去做了。
緊接著,創(chuàng)建具體建造者類。第一個(gè)類是MobileBuilder,它得繼承以上Builder類并重寫里面所有的抽象方法。
package com.meimeixia.pattern.builder.demo1;/*** 具體的構(gòu)建者,用來(lái)構(gòu)建摩拜單車對(duì)象* @author liayun* @create 2021-06-03 5:26*/ public class MobileBuilder extends Builder {/*** 構(gòu)建車架*/@Overridepublic void buildFrame() {bike.setFrame("碳釬維");}/*** 構(gòu)建車座*/@Overridepublic void buildSeat() {bike.setSeat("真皮");}@Overridepublic Bike createBike() {return bike;} }第二個(gè)類是OfoBuilder,同樣,它也得繼承以上Builder類并重寫里面所有的抽象方法。
package com.meimeixia.pattern.builder.demo1;/*** ofo單車構(gòu)建者,用來(lái)構(gòu)建ofo單車* @author liayun* @create 2021-06-03 5:31*/ public class OfoBuilder extends Builder {/*** 構(gòu)建車架*/@Overridepublic void buildFrame() {bike.setFrame("鋁合金");}/*** 構(gòu)建車座*/@Overridepublic void buildSeat() {bike.setFrame("橡膠");}@Overridepublic Bike createBike() {return bike;} }具體建造者類創(chuàng)建完畢之后,接下來(lái),我們來(lái)創(chuàng)建指揮者類,即Director。
package com.meimeixia.pattern.builder.demo1;/*** 指揮者類* @author liayun* @create 2021-06-03 5:40*/ public class Director {// 聲明Builder類型的變量private Builder builder; // 在去創(chuàng)建Director對(duì)象的時(shí)候再為其賦值,所以在Director類里面我們得提供一個(gè)有參構(gòu)造public Director(Builder builder) {this.builder = builder;}// 組裝自行車的功能public Bike construct() {builder.buildFrame();builder.buildSeat();return builder.createBike();} }最后,編寫一個(gè)測(cè)試類來(lái)進(jìn)行測(cè)試,測(cè)試類的名稱我們不妨就叫做Client。
package com.meimeixia.pattern.builder.demo1;/*** @author liayun* @create 2021-06-03 5:48*/ public class Client {public static void main(String[] args) {// 創(chuàng)建指揮者對(duì)象,此刻我們要生產(chǎn)的是摩拜單車,所以在Director類的有參構(gòu)造中我們傳入的是MobileBuilder對(duì)象Director director = new Director(new MobileBuilder());// 讓指揮者指揮組裝自行車Bike bike = director.construct();System.out.println(bike.getFrame());System.out.println(bike.getSeat());} }運(yùn)行以上測(cè)試類,打印結(jié)果如下,這說(shuō)明咱們的摩拜單車已經(jīng)組裝好了。
從上可以看到,對(duì)于客戶端來(lái)說(shuō),它并不需要去關(guān)注底層實(shí)現(xiàn),即復(fù)雜對(duì)象(Bike對(duì)象)的構(gòu)建過(guò)程以及構(gòu)建算法,而是直接調(diào)用指揮者類中的功能就可以構(gòu)建出一個(gè)復(fù)雜對(duì)象了,繼而客戶端就能使用該復(fù)雜對(duì)象了。
這里,我還得給大家說(shuō)一個(gè)注意事項(xiàng),就是上面示例是Builder模式的常規(guī)用法,指揮者類 (即Director)在建造者模式中具有很重要的作用,它用于指導(dǎo)具體建造者如何構(gòu)建產(chǎn)品,控制調(diào)用先后次序,并向調(diào)用者返回完整的產(chǎn)品類,但是有些情況下需要簡(jiǎn)化系統(tǒng)結(jié)構(gòu),比如說(shuō),對(duì)于上面示例而言,整個(gè)的系統(tǒng)結(jié)構(gòu)還是比較復(fù)雜的,這樣,對(duì)于程序員的要求是比較高的,所以我們可以去做簡(jiǎn)化,怎么去簡(jiǎn)化呢?這里我們可以這樣去做,即把指揮者類和抽象建造者類進(jìn)行結(jié)合,也就是說(shuō)我們把指揮者類中的功能都給它加到抽象建造者類中,也即我們就不再需要指揮者類了。
來(lái)看下面這段代碼,它就是抽象建造者類,和上面我們創(chuàng)建的幾乎一模一樣,只不過(guò)是給它里面多加了一個(gè)組裝自行車的功能,這樣,就不需要具體的指揮者類了,這也算是建造者模式的一種改進(jìn)吧!對(duì)于這種改進(jìn),我就不編寫具體的代碼來(lái)進(jìn)行演示了,大家如果有興趣的話,不妨自己嘗試著去做一下。
// 抽象建造者類 public abstract class Builder {protected Bike bike = new Bike();public abstract void buildFrame();public abstract void buildSeat();public abstract Bike createBike();public Bike construct() {this.buildFrame();this.BuildSeat();return this.createBike();}}不過(guò),大家還得注意一點(diǎn),就是這樣做確實(shí)是簡(jiǎn)化了系統(tǒng)結(jié)構(gòu),但同時(shí)也加重了抽象建造者類的職責(zé)(不光要構(gòu)建組件,還要進(jìn)行組裝,所以任務(wù)還是比較多的),也不是太符合單一職責(zé)原則,如果construct方法過(guò)于復(fù)雜,那么建議還是封裝到Director類中。
優(yōu)缺點(diǎn)
通過(guò)以上案例,相信大家對(duì)于建造者模式的理解應(yīng)該更加深刻了。接下來(lái),我們便來(lái)探討一下建造者模式的優(yōu)缺點(diǎn)。
優(yōu)點(diǎn)
第一個(gè),建造者模式的封裝性很好。使用建造者模式可以有效的封裝變化,在使用建造者模式的場(chǎng)景中,一般產(chǎn)品類和建造者類是比較穩(wěn)定的,因此,將主要的業(yè)務(wù)邏輯封裝在指揮者類中對(duì)整體而言可以取得比較好的穩(wěn)定性。
這段話怎么去理解呢?產(chǎn)品類和建造者類一般而言是比較穩(wěn)定的,唯一發(fā)生變化的就是指揮者類中組裝的那個(gè)過(guò)程,如果它經(jīng)常發(fā)生變化,那么我們只需要去修改指揮者類里面的代碼就可以了。
第二個(gè),在建造者模式中,客戶端不必知道產(chǎn)品內(nèi)部組成的細(xì)節(jié),直接調(diào)用指揮者類里面的組裝方法就可以組裝一個(gè)產(chǎn)品出來(lái),這樣就將產(chǎn)品本身與產(chǎn)品的創(chuàng)建過(guò)程解耦了,使得相同的創(chuàng)建過(guò)程可以創(chuàng)建不同的產(chǎn)品對(duì)象。其實(shí)這個(gè)我也一直在強(qiáng)調(diào),組裝過(guò)程一樣,如果需要的組件發(fā)生了變化,那么生產(chǎn)出來(lái)的產(chǎn)品肯定是不一樣的。
第三個(gè),可以更加精細(xì)地控制產(chǎn)品的創(chuàng)建過(guò)程。將復(fù)雜產(chǎn)品的創(chuàng)建步驟分解在不同的方法中,使得創(chuàng)建過(guò)程更加清晰,也更方便使用程序來(lái)控制創(chuàng)建過(guò)程。
從以上案例中你就能清晰地知道,我們確實(shí)是將復(fù)雜對(duì)象的創(chuàng)建步驟分解在不同的方法里面了,你要是不信的話,不妨再看一下Builder類的代碼,你會(huì)發(fā)現(xiàn)它里面的buildFrame和buildSeat這倆方法創(chuàng)建的都是不同的組件。最終,客戶端就會(huì)調(diào)用指揮者類中的組裝方法進(jìn)行一個(gè)組裝,便生成了一個(gè)復(fù)雜對(duì)象。
第四個(gè),建造者模式很容易進(jìn)行擴(kuò)展。如果有新的需求,通過(guò)實(shí)現(xiàn)一個(gè)新的建造者類就可以完成(如果以上例為例,我現(xiàn)在要生產(chǎn)另外一個(gè)品牌的單車,那么我只需要再去創(chuàng)建一個(gè)新的建造者類就可以了),基本上不用修改之前已經(jīng)測(cè)試通過(guò)的代碼,因此也就不會(huì)對(duì)原有功能引入風(fēng)險(xiǎn)了,這也符合了開(kāi)閉原則。
缺點(diǎn)
建造者模式所創(chuàng)建的產(chǎn)品一般具有較多的共同點(diǎn),其組成部分相似,若產(chǎn)品之間的差異性很大,則不適合使用建造者模式,因此其使用范圍受到了一定的限制。
就拿上例來(lái)說(shuō),該案例生產(chǎn)的是自行車,如果現(xiàn)在要去生產(chǎn)一臺(tái)電腦的話,那么此情此景下肯定是不適合使用建造者模式的,因?yàn)榻M裝產(chǎn)品的組件都不一樣。當(dāng)然,雖說(shuō)以上案例中生產(chǎn)的是自行車,但是我們可以再對(duì)其進(jìn)行一個(gè)抽象(即抽取共有的),然后再去創(chuàng)建不同的子類,不過(guò),我們?cè)谶@里面并沒(méi)有這樣去做。
使用場(chǎng)景
建造者(Builder)模式創(chuàng)建的是復(fù)雜對(duì)象,其產(chǎn)品的各個(gè)部分經(jīng)常面臨著劇烈的變化,但將它們組合在一起的算法卻相對(duì)穩(wěn)定,所以它通常在以下場(chǎng)合使用。
- 創(chuàng)建的對(duì)象較復(fù)雜,由多個(gè)部件構(gòu)成,各部件面臨著復(fù)雜的變化,但構(gòu)件間的建造順序是穩(wěn)定的
- 創(chuàng)建復(fù)雜對(duì)象的算法獨(dú)立于該對(duì)象的組成部分以及它們的裝配方式,即產(chǎn)品的構(gòu)建過(guò)程和最終的表示(即最終創(chuàng)建出來(lái)的復(fù)雜對(duì)象)是獨(dú)立的,也就是說(shuō)復(fù)雜對(duì)象裝配的算法和其里面的組件是分離的
根據(jù)以上兩條法則,大家就可以去判斷什么樣的場(chǎng)景下適合使用建造者模式了。
擴(kuò)展
在這一小節(jié),我們對(duì)建造者模式進(jìn)行一個(gè)擴(kuò)展。
建造者模式除了上面的用途外,在開(kāi)發(fā)中還有一個(gè)常用的使用方式,就是當(dāng)一個(gè)類構(gòu)造器需要傳入很多參數(shù)時(shí),如果創(chuàng)建這個(gè)類的實(shí)例,代碼可讀性就會(huì)非常差,而且還很容易引入錯(cuò)誤,那么此時(shí)就可以利用建造者模式進(jìn)行重構(gòu)了。
我們先不管利用建造者模式進(jìn)行重構(gòu)這件事,而是先來(lái)看一下最原始的設(shè)計(jì)方式,就拿我們現(xiàn)在要設(shè)計(jì)一個(gè)手機(jī)類來(lái)說(shuō)事,手機(jī)里面是有很多組件的,例如cpu、屏幕、內(nèi)存、主板等等,這樣,當(dāng)我們?nèi)?chuàng)建一個(gè)手機(jī)對(duì)象時(shí),肯定是要設(shè)置這些組件的數(shù)據(jù)的。不難想到在設(shè)計(jì)手機(jī)類時(shí),我們應(yīng)該提供了一個(gè)有參構(gòu)造方法,而且方法里面的參數(shù)應(yīng)有cpu、屏幕、內(nèi)存以及主板等。
public class Phone {private String cpu;private String screen;private String memory;private String mainboard;public Phone(String cpu, String screen, String memory, String mainboard) {this.cpu = cpu;this.screen = screen;this.memory = memory;this.mainboard = mainboard;}public String getCpu() {return cpu;}public void setCpu(String cpu) {this.cpu = cpu;}public String getScreen() {return screen;}public void setScreen(String screen) {this.screen = screen;}public String getMemory() {return memory;}public void setMemory(String memory) {this.memory = memory;}public String getMainboard() {return mainboard;}public void setMainboard(String mainboard) {this.mainboard = mainboard;}@Overridepublic String toString() {return "Phone{" +"cpu='" + cpu + '\'' +", screen='" + screen + '\'' +", memory='" + memory + '\'' +", mainboard='" + mainboard + '\'' +'}';} }以上Phone類的代碼還是比較簡(jiǎn)單的,下面我們主要來(lái)看一下客戶端的代碼。
public class Client {public static void main(String[] args) {// 構(gòu)建Phone對(duì)象Phone phone = new Phone("intel", "三星屏幕", "金士頓", "華碩");System.out.println(phone);} }大家看清楚在構(gòu)建手機(jī)對(duì)象時(shí)所需要傳入的參數(shù)了吧!現(xiàn)在還只是需要傳入四個(gè)參數(shù),如果需要傳遞更多的參數(shù),那么你會(huì)發(fā)現(xiàn)寫出來(lái)的代碼的可讀性將會(huì)非常差,并且使用成本也會(huì)比較高。所以,在這種情況下,我們就可以選擇使用建造者模式對(duì)其進(jìn)行重構(gòu)了。那如何做到這一點(diǎn)呢,下面我就會(huì)講到。
首先,在com.meimeixia.pattern.builder包下新建一個(gè)子包,即demo2,使用建造者模式進(jìn)行重構(gòu)的代碼我們都放在了該包下。
然后,創(chuàng)建一個(gè)手機(jī)類,即Phone。由于該類的創(chuàng)建比較復(fù)雜,所以下面我會(huì)分步驟來(lái)為大家進(jìn)行講解。
第一步,在該手機(jī)類里面提供四個(gè)成員變量,分別用以表示cpu、屏幕、內(nèi)存、主板等手機(jī)組件。
package com.meimeixia.pattern.builder.demo2;/*** 手機(jī)類* @author liayun* @create 2021-06-03 6:41*/ public class Phone {private String cpu;private String screen;private String memory;private String mainboard;// ... }第二步,由于我們現(xiàn)在是使用建造者模式來(lái)進(jìn)行重構(gòu),所以就不能讓外界直接調(diào)用Phone類的構(gòu)造方法了,也就是說(shuō)我們應(yīng)該私有Phone類的構(gòu)造方法。那么構(gòu)造方法里面需不需要傳遞什么參數(shù)呢?需要,需要傳遞一個(gè)構(gòu)建器。
package com.meimeixia.pattern.builder.demo2;/*** 手機(jī)類* @author liayun* @create 2021-06-03 6:41*/ public class Phone {private String cpu;private String screen;private String memory;private String mainboard;// 私有構(gòu)造方法private Phone(Builder builder) { // 構(gòu)造方法里面需要傳入一個(gè)構(gòu)建器// ...} }第三步,定義Builder構(gòu)建器。在哪定義呢?就在Phone類中定義一個(gè)靜態(tài)的內(nèi)部類,Builder內(nèi)部類定義完畢之后,我們還得在它里面提供四個(gè)成員變量,分別用以表示cpu、屏幕、內(nèi)存、主板等手機(jī)組件,并且還要提供對(duì)應(yīng)的方法進(jìn)行這些組件的構(gòu)建喲!
package com.meimeixia.pattern.builder.demo2;/*** 手機(jī)類* @author liayun* @create 2021-06-03 6:41*/ public class Phone {private String cpu;private String screen;private String memory;private String mainboard;// 私有構(gòu)造方法private Phone(Builder builder) { // 構(gòu)造方法里面需要傳入一個(gè)構(gòu)建器// ...}public static final class Builder {private String cpu;private String screen;private String memory;private String mainboard;/*** 大家可以看到,方法名是和以上成員變量(或者屬性)的名稱保持一致的,并且方法最終返回的就是咱們的Builder* @param cpu* @return*/public Builder cpu(String cpu) {this.cpu = cpu; // 把方法中的參數(shù)設(shè)置給成員變量return this; // 因?yàn)榉椒ǖ姆祷刂殿愋褪荁uilder,所以我們得返回一個(gè)Builder對(duì)象,這樣,不妨直接返回當(dāng)前對(duì)象就哦了}public Builder screen(String screen) {this.screen = screen;return this;}public Builder memory(String memory) {this.memory = memory;return this;}public Builder mainboard(String mainboard) {this.mainboard = mainboard;return this;}// ...}}第四步,完善Phone類中的構(gòu)造方法,即把構(gòu)建者對(duì)象中的成員變量直接賦給Phone類中的成員變量。
package com.meimeixia.pattern.builder.demo2;/*** 手機(jī)類* @author liayun* @create 2021-06-03 6:41*/ public class Phone {private String cpu;private String screen;private String memory;private String mainboard;// 私有構(gòu)造方法private Phone(Builder builder) { // 構(gòu)造方法里面需要傳入一個(gè)構(gòu)建器this.cpu = builder.cpu;this.screen = builder.screen;this.memory = builder.memory;this.mainboard = builder.mainboard;}public static final class Builder {private String cpu;private String screen;private String memory;private String mainboard;/*** 大家可以看到,方法名是和以上成員變量(或者屬性)的名稱保持一致的,并且方法最終返回的就是咱們的Builder* @param cpu* @return*/public Builder cpu(String cpu) {this.cpu = cpu; // 把方法中的參數(shù)設(shè)置給成員變量return this; // 因?yàn)榉椒ǖ姆祷刂殿愋褪荁uilder,所以我們得返回一個(gè)Builder對(duì)象,這樣,不妨直接返回當(dāng)前對(duì)象就哦了}public Builder screen(String screen) {this.screen = screen;return this;}public Builder memory(String memory) {this.memory = memory;return this;}public Builder mainboard(String mainboard) {this.mainboard = mainboard;return this;}// ...}}第五步,由于Phone類中的構(gòu)造方法私有了,所以外界是不能直接調(diào)用Phone類的構(gòu)造方法去創(chuàng)建Phone對(duì)象的。既然這樣,那么我們只能使用Builder內(nèi)部類去構(gòu)建Phone對(duì)象了。因此,我們還得在Builder內(nèi)部類里面定義一個(gè)創(chuàng)建Phone對(duì)象的方法。
package com.meimeixia.pattern.builder.demo2;/*** 手機(jī)類* @author liayun* @create 2021-06-03 6:41*/ public class Phone {private String cpu;private String screen;private String memory;private String mainboard;// 私有構(gòu)造方法private Phone(Builder builder) { // 構(gòu)造方法里面需要傳入一個(gè)構(gòu)建器this.cpu = builder.cpu;this.screen = builder.screen;this.memory = builder.memory;this.mainboard = builder.mainboard;}public static final class Builder {private String cpu;private String screen;private String memory;private String mainboard;/*** 大家可以看到,方法名是和以上成員變量(或者屬性)的名稱保持一致的,并且方法最終返回的就是咱們的Builder* @param cpu* @return*/public Builder cpu(String cpu) {this.cpu = cpu; // 把方法中的參數(shù)設(shè)置給成員變量return this; // 因?yàn)榉椒ǖ姆祷刂殿愋褪荁uilder,所以我們得返回一個(gè)Builder對(duì)象,這樣,不妨直接返回當(dāng)前對(duì)象就哦了}public Builder screen(String screen) {this.screen = screen;return this;}public Builder memory(String memory) {this.memory = memory;return this;}public Builder mainboard(String mainboard) {this.mainboard = mainboard;return this;}// 使用構(gòu)建者創(chuàng)建Phone對(duì)象public Phone build() {// 在內(nèi)部類中是可以直接去訪問(wèn)外部類中私有的方法的return new Phone(this); // 記住,我們要把當(dāng)前對(duì)象(即this)給其傳遞過(guò)去,因?yàn)镻hone類// 中只提供了一個(gè)構(gòu)造方法,而且方法參數(shù)要的還是Builder對(duì)象}}@Overridepublic String toString() {return "Phone{" +"cpu='" + cpu + '\'' +", screen='" + screen + '\'' +", memory='" + memory + '\'' +", mainboard='" + mainboard + '\'' +'}';}}注意,在Phone類中千萬(wàn)不要忘了重寫toString方法喲,因?yàn)榇龝?huì)我們測(cè)試的時(shí)候便會(huì)用到。
至此,Phone類才算是創(chuàng)建完畢了。
創(chuàng)建完畢之后,接下來(lái),我們就得創(chuàng)建一個(gè)測(cè)試類來(lái)進(jìn)行測(cè)試了,在該測(cè)試類中我們?nèi)绾稳?chuàng)建一個(gè)Phone對(duì)象呢?現(xiàn)在我們是不能直接去創(chuàng)建Phone對(duì)象的,而只能是通過(guò)Builder內(nèi)部類來(lái)創(chuàng)建,如下所示。
package com.meimeixia.pattern.builder.demo2;/*** @author liayun* @create 2021-06-21 16:47*/ public class Client {public static void main(String[] args) {// 創(chuàng)建手機(jī)對(duì)象 通過(guò)構(gòu)建者對(duì)象獲取手機(jī)對(duì)象Phone phone = new Phone.Builder().cpu("intel") // 由于cpu、screen等這些方法返回的都是當(dāng)前對(duì)象,所以我們就可以使用鏈?zhǔn)骄幊塘?/span>.screen("三星屏幕").memory("金士頓內(nèi)存條").mainboard("華碩主板").build();System.out.println(phone);} }此時(shí),運(yùn)行以上測(cè)試類,可以看到打印結(jié)果如下,Phone對(duì)象確實(shí)是創(chuàng)建成功了,并且還為它里面的組件進(jìn)行了賦值。
以上就是建造者模式的另外一種用法。這種做法和我們之前的原始方式有什么區(qū)別呢?
- 第一個(gè)區(qū)別:構(gòu)建哪個(gè)組件,我們是一目了然的。例如調(diào)用cpu方法,我們就知道要進(jìn)行組裝的就是cpu
- 第二個(gè)區(qū)別:對(duì)于原始的建造者模式來(lái)說(shuō),組件的構(gòu)建順序是由指揮者類來(lái)定奪的,而現(xiàn)在是把組件的構(gòu)建順序交給了客戶,客戶想怎么去構(gòu)建,就按照他的順序去構(gòu)建就行了
最后,我得多說(shuō)一嘴,就是重構(gòu)后的代碼使用起來(lái)更方便,某種程度上也可以提高開(kāi)發(fā)效率,因?yàn)槲覀兛梢枣準(zhǔn)骄幊塘?#xff0c;而且代碼的可讀性比我們之前的程序更高了。此外,從軟件設(shè)計(jì)上來(lái)說(shuō),對(duì)程序員的要求就比較高了,因?yàn)槟隳苊黠@看到我們?cè)谠O(shè)計(jì)以上Phone類時(shí),比我們之前設(shè)計(jì)的要復(fù)雜太多,但是用起來(lái)很方便,嘿嘿!
創(chuàng)建型模式各個(gè)模式的對(duì)比
建造者模式講完了之后,創(chuàng)建型模式我們就已經(jīng)全部講解完畢了。想必大家都知道創(chuàng)建型模式總共包含了五個(gè)設(shè)計(jì)模式,它們分別是:
相信大家對(duì)于單例設(shè)計(jì)模式和原型模式應(yīng)該不會(huì)混淆,但是對(duì)于其他的一些設(shè)計(jì)模式或許就會(huì)搞混了,因?yàn)樗鼈冊(cè)谑褂蒙厦孢€是比較相似的。不知你在學(xué)習(xí)這些設(shè)計(jì)模式時(shí),心中是否有一個(gè)小小的疑問(wèn),那就是它們好像在做同一件事耶!
所以,在這一小節(jié),我有必要專門花些時(shí)間對(duì)工廠方法模式、抽象工廠模式以及建造者模式做一個(gè)對(duì)比說(shuō)明。下面我們先來(lái)說(shuō)一下工廠方法模式和建造者模式的一個(gè)區(qū)別。
工廠方法模式 VS 建造者模式
工廠方法模式注重的是整體對(duì)象的創(chuàng)建方式;而建造者模式注重的是部件構(gòu)建的過(guò)程,意在通過(guò)一步一步地精確構(gòu)造創(chuàng)建出一個(gè)復(fù)雜的對(duì)象。
我們舉個(gè)簡(jiǎn)單例子來(lái)說(shuō)明兩者的差異,現(xiàn)在要制造一個(gè)超人,如果使用工廠方法模式,那么直接生產(chǎn)出來(lái)的就是一個(gè)力大無(wú)窮、能夠飛翔、內(nèi)褲外穿的超人;而若使用建造者模式,則需要組裝手、頭、腳、軀干等部分,然后再把內(nèi)褲外穿,于是一個(gè)超人就誕生了。
所以,工廠方法模式和建造者模式這兩者的側(cè)重點(diǎn)是不一樣的,工廠方法模式側(cè)重的是整體對(duì)象的創(chuàng)建,而建造者模式側(cè)重的是部件構(gòu)建的過(guò)程,你只要牢牢記住這一點(diǎn)就行了。
抽象工廠模式 VS 建造者模式
抽象工廠模式實(shí)現(xiàn)對(duì)產(chǎn)品家族的創(chuàng)建,一個(gè)產(chǎn)品家族是這樣的一系列產(chǎn)品:具有不同分類維度的產(chǎn)品組合,采用抽象工廠模式則是不需要關(guān)心構(gòu)建過(guò)程,只關(guān)心什么產(chǎn)品由什么工廠生產(chǎn)就可以了。
舉個(gè)例子來(lái)說(shuō),現(xiàn)在我要去組裝一臺(tái)計(jì)算機(jī),那么計(jì)算機(jī)里面得有這么一些組件,如主板、硬盤、內(nèi)存條、電源以及cpu等等,這些組件就可以理解成是一個(gè)產(chǎn)品家族,而抽象工廠模式就是用來(lái)生產(chǎn)這些產(chǎn)品家族里面的產(chǎn)品的。
建造者模式則是要求按照指定的藍(lán)圖建造產(chǎn)品,它的主要目的是通過(guò)組裝零配件而產(chǎn)生一個(gè)新產(chǎn)品。
如果將抽象工廠模式看成汽車配件生產(chǎn)工廠,生產(chǎn)一個(gè)產(chǎn)品族的產(chǎn)品,那么建造者模式就是一個(gè)汽車組裝工廠,通過(guò)對(duì)部件的組裝可以返回一輛完整的汽車。這樣的話,你會(huì)發(fā)現(xiàn),我們可以把抽象工廠模式和建造者模式整合到一塊來(lái)設(shè)計(jì)汽車的生產(chǎn)過(guò)程,因?yàn)槠嚺浼梢允褂贸橄蠊S模式來(lái)生產(chǎn),而汽車的組裝可以使用建造者模式來(lái)進(jìn)行組裝,如此就可形成一個(gè)完整的系統(tǒng)了。
所以,大家一定要注意了,我們以后真正去用設(shè)計(jì)模式的話,并不意味著只在某一種情況下只用一種設(shè)計(jì)模式,而更有可能是多種設(shè)計(jì)模式混合到一塊去使用。
總結(jié)
以上是生活随笔為你收集整理的从零开始学习Java设计模式 | 创建型模式篇:建造者模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: matlab2c使用c++实现matla
- 下一篇: [Java实验 5] 异常处理