在前面幾篇文章中,已經(jīng)講解了單例模式、工廠方法模式、抽象工廠模式,創(chuàng)建型還剩下一個比較重要的模式-建造者模式。在理解該模式之前,我還是希望重申設(shè)計模式的初衷,即為解決一些問題而提供的優(yōu)良方案。學(xué)習(xí)設(shè)計模式遺忘其初衷,注定無法理解其真正的深刻內(nèi)涵。從創(chuàng)建型模式的名稱上來看,這些都是為了解決創(chuàng)建對象相關(guān)的問題。單例模式解決了如何創(chuàng)建唯一對象的問題,工廠方法模式解決了對象創(chuàng)建過程的封裝問題,抽象工廠模式解決了創(chuàng)建多個相關(guān)聯(lián)對象的問題,那么不知道你之前是否有思考過,建造者模式是要解決什么問題嗎?我相信很多人可能沒有思考而直接用老一套去學(xué)習(xí)該模式,最終就是不理解、記不住、用不會!
一、建造者模式概念理解
建造者模式,又稱生成器模式,在大部分參考資料中都公認(rèn)的定義為:
將一個復(fù)雜對象的構(gòu)建與它的表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。
我不知道你們能不能理解這個很抽象的定義,起初我是不太能理解。我似乎從“分離”上能看出是要解耦,至于“構(gòu)建”、“表示”這兩個詞本身就有模糊的感覺。實際上,這里的“構(gòu)建”可以理解為“里子”,“表示”可以理解為“面子”,“同樣的構(gòu)建過程可以創(chuàng)建不同的表示”就是里子和多個面子需要解耦,如同一個人可以擁有很多面具以示人。意即,你還是你,但是外人看到的可以有多個。
“構(gòu)建”指的就是里子。每一個可由外界創(chuàng)建對象的類都會提供一個或多個構(gòu)造函數(shù),或為有參構(gòu)造,亦或為無參構(gòu)造。對象的正規(guī)創(chuàng)建最常用的方式通過new關(guān)鍵字及構(gòu)造方法Constructor。然而實際上,對象的構(gòu)建過程并不止步于此。對象的本質(zhì)是類信息(包括屬性、方法)+數(shù)據(jù),前者是編譯后就固定不變的,后者數(shù)據(jù)是運(yùn)行時改變的,因此對象的構(gòu)建過程應(yīng)從分配內(nèi)存開始直到對象數(shù)據(jù)初始化結(jié)束。對象的初始化除了調(diào)用Constructor方法之外,還有Setter方法也會經(jīng)常用于初始化對象數(shù)據(jù)(注,這里Setter方法不僅僅指的是get\set方法)。因此,對象的構(gòu)建過程本質(zhì)上是類構(gòu)造函數(shù)-Constructor+Setter方法。我們通常會使用這二者協(xié)同初始化對象數(shù)據(jù)并獲得對象,但是這里面會不會存在問題呢?
可實例化類會提供多個構(gòu)造函數(shù)提供給外界用于創(chuàng)建對象。構(gòu)造函數(shù)可能的復(fù)雜性包括兩個部分,一個是入?yún)?,一個是具體邏輯。后者可以通過工廠方法模式進(jìn)行封裝,那前者怎么辦?即,若對象的創(chuàng)建需要很多外界輸入?yún)?shù),其中包括必要參數(shù)或非必要參數(shù),這種情況下我們在一些源碼或業(yè)務(wù)代碼中會看到類中會提供很多個構(gòu)造方法,不同構(gòu)造方法通過重載的方式處理參數(shù)的差異性,使用方根據(jù)需要選擇不同的構(gòu)造方法。那,如果參數(shù)再多一些呢?你無法判斷使用方想傳入哪些參數(shù)或者不傳入哪些參數(shù)來構(gòu)建對象。還有一種情況即使類的構(gòu)建過程需要通過對應(yīng)的內(nèi)部配置類進(jìn)行構(gòu)建。你只提供內(nèi)部相關(guān)復(fù)雜對象作為入?yún)⒌挠袇?gòu)造,我根本不了解這個入?yún)⑷绾无k?舉例,你提供了Configure內(nèi)部類作為創(chuàng)建會話對象的有參構(gòu)造,保證了迪米特原則,但是我這邊數(shù)據(jù)可能是XML類型配置、YAML類型配置文件等,是否我需要解決這些文件解析為Configure內(nèi)部類對象才能使用你的構(gòu)造方法呢?這些都是問題,問題的根因就是你的類創(chuàng)建對象過程里子和面子耦合太嚴(yán)重(一個構(gòu)造對象提供一個面子)。
對于構(gòu)造方法參數(shù)數(shù)量問題,有的同學(xué)會說那我就把必選參數(shù)留在構(gòu)造方法,可選參數(shù)使用Setter方法。似乎是能夠解決一部分問題,但是Setter方法可能會使得對象的構(gòu)建邏輯分散在各處,增加了使用不完整對象的風(fēng)險?!疽虼?,建造者模式是不建議調(diào)用端直接使用對象的Setter方法,必須封裝起來】
如何解耦呢?能否將構(gòu)建對象所需數(shù)據(jù)(面子)和構(gòu)造函數(shù)中的邏輯(里子)拆分。我們講過,解決耦合問題的究極好辦法就是加一層,即Builder層。讓Builder來充當(dāng)這個面子,Builder負(fù)責(zé)提供給外界并預(yù)處理外界數(shù)據(jù),轉(zhuǎn)換為內(nèi)部Constructor所需要的數(shù)據(jù),然后由Builder來調(diào)用Constructor來返回對象。這樣的解耦使得Constructor僅專注于內(nèi)部構(gòu)建邏輯,而外界所需要的面子均由Builder來負(fù)責(zé),如此設(shè)計既滿足業(yè)務(wù)需要,也滿足單一職責(zé)原則。
概念總結(jié):
- 建造者模式主要解決創(chuàng)建對象時對象的創(chuàng)建過程與創(chuàng)建的所需數(shù)據(jù)表示的嚴(yán)重耦合性問題
- Constructor構(gòu)造方法在非必要參數(shù)多時無法滿足調(diào)用方的需求,且在復(fù)雜構(gòu)造參數(shù)(內(nèi)部配置類)時增加調(diào)用方創(chuàng)建難度。
- Setter方法也是對象構(gòu)建過程的一部分,但是可能會誤使用不成熟對象
- 通過職責(zé)拆分的方法解耦對象構(gòu)建過程及其表示。Builder負(fù)責(zé)承接調(diào)用方可能賦值的數(shù)據(jù),并轉(zhuǎn)換為類對象創(chuàng)建的內(nèi)部參數(shù)需要。目標(biāo)對象的類僅關(guān)注自身功能的實現(xiàn)?!炯碆uilder出去跑業(yè)務(wù),伺候甲方的。Construstor做好自己本職工作】
二、應(yīng)用實踐
上一章節(jié)限于個人水平有限,理解角度與主流理解不完全契合,也會存在讓大家誤解的地方。因此,這個章節(jié)就通過具體的示例來說說我的想法,比較不同方案的優(yōu)劣達(dá)到理解建造者模式的目的。在其他參考資料中,給出的案例和建造者解決方案我認(rèn)為雖容易看懂,但不易理解且難以投入實際使用,可能還存在一些問題。下面我會通過一個簡單的案例,一步步分析為什么常規(guī)的建造者模式大家?guī)缀醵疾粫褂玫健?/p>
2.1 基本案例
案例的背景就以大家熟悉的“電腦”對象創(chuàng)建為例,想象以下,創(chuàng)建一個電腦對象應(yīng)該具備哪些東西(數(shù)據(jù))呢?大概會有CPU(中央處理器)、內(nèi)存、硬盤、顯卡、顯示器、鍵盤、鼠標(biāo)、聲卡、網(wǎng)卡、光驅(qū)等。所以,創(chuàng)建一個電腦對象可能會需要很多種數(shù)據(jù),但是根據(jù)用戶的需求不同,需要創(chuàng)建的電腦對象可能也存在差異性。如:
① 我僅需要電腦用于跑程序,那我只需要【CPU、內(nèi)存、硬盤】去創(chuàng)建電腦對象
② 我要跑深度學(xué)習(xí),那我除了以上組件(數(shù)據(jù))之外,還需要好的【顯卡】
③ 我要打游戲,那我除了以上組件(數(shù)據(jù))之外,還需要好的【顯示器、鍵盤、鼠標(biāo)、聲卡、網(wǎng)卡】等
④ 我要看DVD,那我就得需要【光驅(qū)】了。
…
你看看多頭疼,根據(jù)調(diào)用方使用場景不同,電腦對象的所需組件也有不同。難道你要遍歷所有場景給出不同的構(gòu)造方法,很明顯這種方案不太現(xiàn)實。但如果你仔細(xì)發(fā)現(xiàn)就可以看到,對象的創(chuàng)建是可以區(qū)分必要組件和非必要組件的。在這個例子中,不論用戶的需求是啥,都需要有【CPU、內(nèi)存、硬盤】才能創(chuàng)建電腦。那是否電腦類僅提供必要組件的構(gòu)造方法,非必要組件由Setter方法來負(fù)責(zé)呢?這已經(jīng)是個很好的方案,大部分的業(yè)務(wù)開發(fā)中可能都會使用這個方案。但設(shè)計模式不會止步于此,存在兩個疑問:(1)Setter方案存在什么問題?(2)有沒有更好的方案解決?
Setter方案存在兩個問題,第一個問題前面也提到了,對象數(shù)據(jù)初始化邏輯分散在各處,增加了使用不完整對象的風(fēng)險。第二個問題是使用方不清楚Setter方法具體含義,是僅用于對象初始化(類似于Construstor)還是用于對象數(shù)據(jù)運(yùn)行時修改(類似于其余普通函數(shù)),即責(zé)任不明。
為解決這個問題,我們前面提出中間添加builder層,由Builder來負(fù)責(zé)封裝對象構(gòu)建的多種表示的差異性。示例代碼如類圖如下:
① “電腦”對象類
public class Computer {
private Object cpu;
private Object memory;
private Object hardDisk;
private Object graphicsCard;
private Object monitor;
private Object keyboard;
private Object mouse;
private Object soundCard;
private Object networkCard;
private Object opticalDrive;
public Computer(Object cpu, Object memory, Object hardDisk) {
this.cpu = cpu;
this.memory = memory;
this.hardDisk = hardDisk;
}
public void setGraphicsCard(Object graphicsCard) {
this.graphicsCard = graphicsCard;
}
public void setMemory(Object memory) {
this.memory = memory;
}
// 其他可選屬性set方法省略...
public void doSomething() {
// ...
}
}
② Builder接口
public interface ComputerBuilder {
void setGraphicsCard(Object graphicsCard);
void setMonitor(Object monitor);
void setKeyboard(Object keyboard);
void setMouse(Object mouse);
void setSoundCard(Object soundCard);
void setNetworkCard(Object networkCard);
void setOpticalDrive(Object opticalDrive);
Computer buildComputer(Object cpu, Object memory, Object hardDisk);
Computer buildDLComputer(Object cpu, Object memory, Object hardDisk, Object graphicsCard);
}
③ Builder具體實現(xiàn)類
public class ConcreteComputerBuilder implements ComputerBuilder{
private Object graphicsCard;
private Object monitor;
private Object keyboard;
private Object mouse;
private Object soundCard;
private Object networkCard;
private Object opticalDrive;
@Override
public void setGraphicsCard(Object graphicsCard) {
this.graphicsCard = graphicsCard;
}
@Override
public void setMonitor(Object monitor) {
this.monitor = monitor;
}
@Override
public void setKeyboard(Object keyboard) {
this.keyboard = keyboard;
}
@Override
public void setMouse(Object mouse) {
this.mouse = mouse;
}
@Override
public void setSoundCard(Object soundCard) {
this.soundCard = soundCard;
}
@Override
public void setNetworkCard(Object networkCard) {
this.networkCard = networkCard;
}
@Override
public void setOpticalDrive(Object opticalDrive) {
this.opticalDrive = opticalDrive;
}
@Override
public Computer buildComputer(Object cpu, Object memory, Object hardDisk) {
// ...這里省略必要參數(shù)的校驗邏輯
Computer ins = new Computer(cpu, memory, hardDisk);
if(this.graphicsCard != null) {
ins.setGraphicsCard(this.graphicsCard);
}
// ... 省略其余可選參數(shù)的處理邏輯
return ins;
}
@Override
public Computer buildDLComputer(Object cpu, Object memory, Object hardDisk, Object graphicsCard) {
// ...這里省略必要參數(shù)的校驗邏輯
Computer ins = new Computer(cpu, memory, hardDisk);
if(graphicsCard == null) {
throw new RuntimeException("缺少顯卡組件,無法創(chuàng)建深度學(xué)習(xí)機(jī)器");
}
ins.setGraphicsCard(this.graphicsCard);
// ... 省略其余可選參數(shù)的處理邏輯
return ins;
}
}
完整的代碼如上,類圖中使用SetXXX省略了很多Set方法。這種就屬于建造者模式,調(diào)用方可通過ComputerBuilder來實現(xiàn)獲取Computer對象,而在ComputerBuilder中對于可選參數(shù)的處理通過封裝在了buildXXX方法中,并且提供了多種類型的builder方法供調(diào)用方使用。因此這樣就實現(xiàn)了對象的構(gòu)建與它的表示(代碼中給出了2種表示,還可以更多)解耦,表示雖不同但是實際上都是通過同一個Constructor來創(chuàng)建對象的。
在一些參考資料中,還給出了Director類,Director翻譯為導(dǎo)演類,意即就是將多種表示(如普通電腦、深度學(xué)習(xí)電腦、打游戲電腦等)預(yù)先封裝到Director中供調(diào)用方使用,調(diào)用方不再感知setXXX設(shè)置組件數(shù)據(jù)的過程了。
我認(rèn)為這種封裝會造成類的急速膨脹,而且效果不好。你根本無法預(yù)知調(diào)用方會有什么樣的場景,Director類也無法徹底解決問題。如上例,在Builder通過不同的方法返回不同對象也有同樣效果且沒有問題。
目前Setter方法問題通過這種方法已經(jīng)很好解決了,很多相關(guān)參考資料也到此結(jié)束了,但我認(rèn)為建造者模式的理解尚不能止步,因為還有遺漏的地方。之前的思考思路是,對象初始化Constructor構(gòu)造函數(shù)可能會存在很多可選參數(shù),不可能全部提供對應(yīng)的構(gòu)造方法。然后,我們分析了使用Setter方法解決可選參數(shù)的問題,但是Setter方法的使用增加了使用不成熟對象的風(fēng)險。之后,我們增加了builder層封裝處理了Setter方法,然后創(chuàng)建對象。
可以看出,我們之前只是從構(gòu)造函數(shù)數(shù)量問題上思考,但是這很難讓我們理解并使用建造者模式。為什么這么說呢?因為我們在平時開發(fā)中幾乎不會碰到這種情況。在大部分的開發(fā)場景下,當(dāng)函數(shù)形參數(shù)量大于7時,我們就會單獨(dú)封裝為一個類,因此我們很少會碰見要處理參數(shù)數(shù)量上的問題,大部分情況下我們是要處理類型問題。
2.2 理解“表示”
不同的表示就是指用于初始化對象的數(shù)據(jù)不確定(由調(diào)用方指定),包括數(shù)量不確定以及類型不確定兩個方面。數(shù)量不確定可通過上一小節(jié)的方案解決,但需要注意的是當(dāng)函數(shù)形參數(shù)量大于7時,我們就會單獨(dú)封裝為一個類,這也會轉(zhuǎn)換為類型不確定。類型不確定是啥意思呢?在第一章節(jié)內(nèi)我們提到大部分的復(fù)雜對象由于其所需數(shù)據(jù)多,一般創(chuàng)建對象都是通過其內(nèi)部配置類創(chuàng)建的,你不能要求所有的調(diào)用方都必須了解這個內(nèi)部配置類才能創(chuàng)建對象。
說起來有些許抽象,就以前一小節(jié)案例來說。創(chuàng)建一個深度學(xué)習(xí)機(jī)器肯定是需要顯卡組件(參數(shù)),細(xì)想這里會存在一個我們經(jīng)常忽略的問題,就是顯卡對于“創(chuàng)建電腦”來說會很復(fù)雜。創(chuàng)建電腦的過程你必須考慮顯卡的接口、頻率、功率等信息,這就意味著顯卡的類型不僅僅是個簡單Object類,而是一個相對復(fù)雜的參數(shù)類(定義為類GraphicsCard)。那問題來了,調(diào)用端為了意圖創(chuàng)建一個電腦還需要先創(chuàng)建一個GraphicsCard對象嗎?那其他組件呢?太麻煩啦,調(diào)用方能否只傳輸一個String表示顯卡的型號呢。繼續(xù)分析,電腦對象會提供String類型顯卡參數(shù)的構(gòu)造方法嗎?明顯不會,否則電腦對象內(nèi)部就得處理String到GraphicsCard對象的創(chuàng)建過程了,電腦類的職責(zé)不再單一,這就是構(gòu)建過程和表示耦合導(dǎo)致的結(jié)果。因此將String到GraphicsCard對象的過程就可以由Builder承擔(dān)了,這樣既滿足了調(diào)用方對于不同表示的需求,也保證了目標(biāo)對象構(gòu)建過程的職責(zé)清晰。
根據(jù)此,響應(yīng)代碼類圖如下:(代碼簡單不再提供具體代碼)
從類圖中看出,Builder提供了接受String類型數(shù)據(jù)來負(fù)責(zé)處理顯卡參數(shù)類型問題,Computer還是僅負(fù)責(zé)自己內(nèi)部的邏輯即可,外面的一切由Builder這個跑業(yè)務(wù)的給擺平。這么一看,建造者模式理解起來就豁然開朗,調(diào)用方要創(chuàng)建對象但是無法提供創(chuàng)建對象的條件,那就上Builder來處理這其中的GAP即可。這就達(dá)到了外界可以使用多種方式(表示)通過Builder創(chuàng)建目標(biāo)對象,
而實際Computer創(chuàng)建對象的動作可能還是同一套邏輯(Constructor)。
數(shù)量不確定 遠(yuǎn)沒有 類型不確定 帶來的問題嚴(yán)重。如果僅僅是數(shù)量問題,使用Setter方法方案即可,使用Builder封裝Setter稍微有點(diǎn)大材小用了。但是類型問題,就必須得使用Builder來解決了。
大部分的 數(shù)量問題 也都可能轉(zhuǎn)換為 類型問題。當(dāng)參數(shù)數(shù)量很多時,都會考慮將這些參數(shù)封裝起來,比如Configure類。目標(biāo)對象的創(chuàng)建僅通過Confugure對象數(shù)據(jù)來創(chuàng)建,而Builder就負(fù)責(zé)將外部數(shù)據(jù)轉(zhuǎn)換為Configure對象。
這是普遍的做法,說到這你是否覺得上文的兩個類圖有哪里讓人不適的地方?對,屬性太多啦,目標(biāo)對象屬性一套數(shù)據(jù),Builder也跟著一套數(shù)據(jù),代碼重復(fù)且可讀性會差。解決辦法就是封裝,要么使用Configure類,要么使用枚舉或其他都行。
大部分情況下我們是要處理類型問題,下面我們簡單看下Mybatis中用于創(chuàng)建SqlSessionFactory對象的建造者模式是怎么應(yīng)用的,下面給出SqlSessionFactoryBuilder的代碼截圖:
SqlSessionFactory對象的創(chuàng)建需要內(nèi)部配置類Configuration,而調(diào)用方可能提供多種不同的源數(shù)據(jù)及配置。這其中的轉(zhuǎn)換、驗證邏輯均有Builder類來負(fù)責(zé)處理。
建造者模式優(yōu)點(diǎn):文章來源:http://www.zghlxwxcb.cn/news/detail-617823.html
- 將對象創(chuàng)建過程與表示解耦,滿足單一職責(zé)原則
- 由于相互獨(dú)立,對象的創(chuàng)建方式十分容易擴(kuò)展
建造者模式缺點(diǎn):文章來源地址http://www.zghlxwxcb.cn/news/detail-617823.html
- 建造者模式幾乎沒有缺點(diǎn)。最好用于創(chuàng)建復(fù)雜對象,簡單對象使用該模式會增加代碼復(fù)雜度。
到了這里,關(guān)于設(shè)計模式-建造者模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!