你們好,我是高冷就是範兒,很久不見。最近比較忙,文章更新較慢,抱歉。😊今天咱們繼續來聊設計模式這個話題。前面已經講過幾個模式,若是沒有閱讀過的朋友能夠回顧一下。java
前文回顧
👉讓設計模式飛一下子|②單例模式
👉讓設計模式飛一下子|③工廠模式
👉讓設計模式飛一下子|④原型模式算法
那麼,今天咱們要來聊的是建造者模式,這也是GOF23的建立型模式中的最後一個模式。既然是建立型模式,天然又是一個用來建立對象的模式,這從這個模式的名字上也能看出來。數據庫
建造者,就是用來建立對象的。這聽上去就是一句廢話......編程
對象不就是建造出來的嗎?這怎麼還成了一個單獨的模式了?並且和前面說的那些模式有啥區別?設計模式
爲了方便理解,我仍是不套用書上或者網上的一些理論,但就本身學過的一些理解,結合生活的一些場景跟你們說說。api
舉個生活當中組裝電腦的例子,好比如今小明去電腦城組裝一臺電腦,一臺完整電腦確定會包含顯示器,鍵盤,鼠標等等一些配件,而每一個配件的配置參數那就成百上千了,並且每一個硬件上都會有一些專業的參數咱們也看不懂。然而,咱們須要買的只是一臺電腦,咱們只須要告訴賣電腦的人,咱們須要買什麼品牌,什麼檔次,什麼價格的電腦或配件便可,這樣電腦商家就會給咱們組裝好一臺各方面都比較合適的電腦。小明平時就簡單寫個文檔看個視頻啥的,對配置要求不高,因此只須要入門級的電腦便可。以下圖:數組
而後小明玩了幾年後上了大學,無心中染上了玩遊戲的毛病,一發不可收拾,原來的電腦配置已經卡出翔了。因而,他又想換臺遊戲本,以下圖:mybatis
而後小明大學畢業了,由於大學每天玩遊戲,畢業後實在混不下去了,據說學Java技術工資高,還能裝逼,因而二話沒說轉行學Java......可是學Java再用原來那個破電腦可不行了,怎麼樣也得用個MacBook Pro才配得上程序猿這個高大上的職位嘛,因而......app
上面這個是平常生活中很是常見的場景,那用代碼如何去實現呢?框架
若是你以前沒學過任何設計模式,你也許會以下設計(僞代碼):
public class Computer {
內存
CPU
硬盤
鼠標
...
}
public class Client{
void buy(){
//入門級
Computer comp = new Computer();
comp.set內存("4g");
comp.setCPU("i3");
comp.set硬盤("128g");
comp.set鼠標("雜牌");
...
}
把電腦交付給客戶...
}
複製代碼
這樣寫粗看沒有什麼毛病,咱們平時大部分時候就是這樣寫的。
那想一想,這樣寫後續會有什麼問題嗎?
如今小明電腦要升級了,換成遊戲本,這個時候麻煩來了,他得去修改上面buy()
方法體內的代碼了,將原來的
comp.set內存("4g");
comp.setCPU("i3");
comp.set硬盤("128g");
comp.set鼠標("雜牌");
複製代碼
修改爲新的配置:
comp.set內存("8g");
comp.setCPU("i5");
comp.set硬盤("256g");
comp.set鼠標("羅技牌");
複製代碼
再升級成MacBook Pro時也同理。並且若是我須要增長或者減小一些配置也一樣須要去修改原來的代碼。
顯然這個是違反「開閉原則」了。
那問題出在哪裏呢?
不難看出,comp
的setXX()
系列方法和Client
的api牢牢耦合了,並且Client
須要對象建立的細節一清二楚,不然無法完成對象建立。
那怎麼樣才能把對象的建立過程單獨從客戶端代碼中剝離出來,實現解耦呢?
若是還記得前面學過的工廠模式,有的同窗可能就會想到,工廠模式不就是解決這個問題的嗎?把對象建立和對象的使用分離實現解耦,使得客戶端並不須要關注對象的建立過程,須要與對應的工廠類打交道便可。由於咱們這邊會涉及到多個對象,並且這多個對象之間也存在一些關聯,因此此處能夠採用抽象工廠模式實現。因此通過優化,你可能會以下實現:
interface ComputerFactory{
內存 create內存();
CPU createCPU();
...
}
複製代碼
而後針對不一樣定位的電腦,建立其不一樣的Factory
實現類便可。
class 入門級 implements ComputerFactory{
內存 create內存(){
生產4G內存
}
CPU createCPU(){
生產i3內存
}
...
}
複製代碼
而後Client
的代碼會修改成以下:
//入門級
public class Client{
ComputerFactory factory;
void buy(){
Computer comp = new Computer();
comp.set內存(factory.create內存());
comp.setCPU(factory.createCPU());
...
}
把電腦交付給客戶...
}
複製代碼
這樣的話,若是我須要升級配置,只須要傳入不一樣的ComputerFactory
就能夠了,業務邏輯的代碼是不須要再動了。可是對於這個需求,這樣的解決方法是存在一些問題的。爲何呢?
以前在講工廠模式的時候說過,工廠模式主要是用來生產一個完整的產品的。也就是說,用工廠模式建立出來的對象,就是一個最終的產品了。雖然抽象工廠模式能夠一會兒建立多個產品,可是這多個產品其自己就是一個完整的最終的產品,直接就可使用了,無非是抽象工廠模式建立的這些個產品之類有一些關聯,屬於同一個類型的東西。
可是咱們的這個需求中,像內存、CPU、鼠標等這些,對於電腦而言,都只是整臺電腦的一個組成部分而已,他們並非一個完整的產品,須要將他們組合裝配起來才能構成一個完整的電腦對象。由於這個需求中,咱們須要獲得的是一個電腦對象,而並非內存、CPU、鼠標等這些個零件。
固然你也可使用工廠模式來建立這些個零件,可是,以後你也還須要本身去對這些零部件進行組裝,也就是說,你仍是須要對這個對象的組成細節瞭解清楚,不然,你仍是沒法建立出一個完整的對象。另外,還會出現多一個零件,少一個零件的問題,會增長客戶端的複雜度。
因而,咱們今天要講的主角——建造者模式就閃亮登場了。
建造者模式要解決的場景就是這個需求,致力於將一系列瑣碎的零部件組裝成一個完整的對象,而這其中具體的組裝細節客戶端是不須要知道的。
在建造者模式中,有一個抽象接口,裏面會定義一系列對象零部件的裝配的方法(組裝內存、CPU、鼠標等這些個零件)。而後會有一個組裝的人(好比電腦賣家),對這些個零部件進行組裝,最後給客戶端返回一個完整的對象。就好比上面組裝電腦的例子。
實現代碼以下:
//抽象接口定義完整對象的零部件裝配方法
abstract class ComputerBuilder{
protected Computer comp = new Computer();
abstract void build內存();
abstract void buildCPU();
abstract void build硬盤();
protected Computer getComputer(){
return comp;
}
}
複製代碼
而後針對於不一樣級別的電腦,能夠建立其對應的抽象接口的實現類,以完成對電腦的裝配,好比如今須要裝配一臺MacBook Pro,則實現以下:
class MacBookProBuilder extends ComputerBuilder{
void build內存() {
comp.set內存("16g");
}
void buildCPU() {
comp.setCPU("i7");
}
void build硬盤() {
comp.set硬盤("1T");
}
}
複製代碼
還要須要一個專門負責裝配的對象,好比電腦賣家,
class Seller{
ComputerBuilder builder;
Computer sell(){
builder.build內存();
builder.buildCPU();
builder.build硬盤();
return builder.getComputer();
}
}
複製代碼
這個時候在Client中的代碼變成了以下:
//MacBook Pro
public class Client{
Seller seller;
void buy(){
Computer comp = seller.sell();
}
把電腦交付給客戶...
}
複製代碼
咱們會發現,此時,客戶端已經完成解耦,咱們只須要告訴電腦賣家咱們須要什麼級別的電腦,他就會給咱們返回一臺裝配好的完整的對象,不再須要去了解各個零件是怎麼生產的(是經過單例模式建立的?仍是原型模式?仍是工廠模式?),也不須要知道這些零部件是怎麼拼裝起來的(是先裝配CPU,仍是先裝配硬盤?)。這樣就完美的解決了解耦的問題。
如今假設小明想再換一個配置的電腦,只須要再提供一個對應的Buidler
子類,完成對應零件的建立,而後將Builder
交給負責裝配的人(術語叫Director
),如賣家,就能夠了,Client
不再須要有任何的改動,符合「開閉原則」。
在對象沒有那麼複雜的狀況下,Director
也是能夠省略的,直接將裝配過程在Client
端實現便可。固然在這種狀況下,Client
是須要對該對象的組成結構有所瞭解的,也容易致使缺胳膊少腿的狀況。
另外,上面的代碼其實能夠修改成更爲優雅的寫法。
class MacBookProBuilder extends ComputerBuilder{
void build內存() {
comp.set內存("16g");
return this;
}
void buildCPU() {
comp.setCPU("i7");
return this;
}
void build硬盤() {
comp.set硬盤("1T");
return this;
}
}
複製代碼
在每個裝配的方法中都返回當前Builder
對象,這樣Director
中裝配邏輯能夠直接以清爽簡潔的鏈式風格書寫。
class Seller{
ComputerBuilder builder;
Computer sell(){
return builder.build內存().buildCPU().build硬盤().getComputer();
}
}
複製代碼
這種鏈式風格能夠很好的用來解決伸縮構造器反模式的問題。什麼意思?
好比如今某一個類,屬性極多,有上百個吧......
class A{
屬性1;
屬性2;
屬性3;
屬性4;
...省略100個屬性
}
複製代碼
如今我須要在某處建立該類對象,而且還須要對其中某一些屬性進行賦值。若是這個時候咱們採用構造器來建立就會比較麻煩,爲何呢?
由於我可能每一次建立所須要的屬性多是不同的。好比在應用某處須要建立一個A對象,須要使用屬性一、屬性二、屬性3,因而我會在A類中加入一個構造器,
class A {
//省略屬性
public A(屬性1,屬性2,屬性3){}
}
複製代碼
在應用另一處又須要建立一個A對象,須要使用屬性一、屬性二、屬性三、屬性4,因而你又須要添加一個構造器,
class A {
//省略屬性
public A(屬性1,屬性2,屬性3,屬性4){}
}
複製代碼
能夠想象,要是各處引用構造器特別多,而且參數還都不同,那畫面太美不敢想象。還有,當構造器重載太多,建立對象時選擇合適構造器都是一件很費神的事情。這個時候使用建造者模式的鏈式風格就很好的解決了這個問題。
class ABuilder{
A a = new A();
A set屬性1(xxx){... return this;}
A set屬性2(xxx){... return this;}
A set屬性3(xxx){... return this;}
A set屬性4(xxx){... return this;}
A build(){return a;}
...
}
複製代碼
這個時候我再建立一個A對象,以下,若是我須要修改屬性設置的個數,能夠很方便的進行調整,很好的解決了重載構造器的問題。
A a = new ABuilder().set屬性1(xxx),set屬性2(xxx).set屬性3(xxx).set屬性4(xxx).build();
複製代碼
關於建造者模式的核心內容就這些,咱們能夠作一下總結。
做爲建立型模式,建造者模式也是用來建立對象的,並且他和工廠模式看上去會比較類似,甚至難以區分。
不過建造者模式關鍵點在於建造和裝配分離。建造者最終只會生成一個完整的對象,可是這個對象通常來講是比較複雜的,裏面會分紅好幾個模塊,建造者模式強調的是這個裝配的過程。
而工廠模式,主要強調的是建立,固然這個建立有多是會同時建立一個(簡單工廠或者工廠方法模式)或者多個對象(抽象工廠模式)。雖然工廠模式建立的也有可能很複雜,可是他不關心對象會不會有裝配的過程,只要建立出來便可。
以下圖:
一言以蔽之,工廠模式強調建立,建造者模式強調組裝。
其實設計模式這東西並非很絕對很孤立的去看待,所以咱們沒有必要將每個模式的區別都分得特別明確,通常來講,設計模式也不是獨立使用的,會相互搭配。就好比這邊得工廠模式和建造者模式,二者側重點不一樣,可是徹底能夠結合使用。好比在使用工廠模式建立對象時,有可能每一個對象都會有比較明確的裝配過程,就能夠結合使用。反過來嗎,在使用建造者模式時,每一步的裝配所須要的零件,又有多是經過工廠模式(固然也有多是經過原型模式,單例模式)建立所得。
在建造者模式中,客戶端只須要與Director
交互,並不須要知道內部的對象構建和裝配的細節,屏蔽了系統複雜度。
能夠爲系統中添加多個Builder
,好比上面例子中爲不一樣品牌的電腦分別建立一個Builder
對象,來達到擴展系統功能的目的。同時,經過調整每個Builder
內部裝配的過程,有可能輕鬆對裝配過程當中的每一步進行細粒度的控制和定製。
沒有一個設計模式是完美的,每個設計模式都有其特定的使用場景。經過上面分析,不難發現,
建造者模式主要用於建立那些具備明顯組裝過程的一類複雜對象,而且這類對象中內部結構差別性都不大,基本結構都相同,而且比較穩定。好比上面的電腦的例子,無論啥牌子的電腦,啥級別的電腦,配件都是那些,CPU,內存,硬盤等,變不出花來了,無非就是每種配件的具體參數不一樣。能夠想象,若是是兩類差別性很大的對象,一類是電腦,一類是汽車,徹底是八竿子打不着的兩類產品,裝配過程更是天差地別,天然是無法使用建造者模式的。
一個類的各個組成部分的具體實現類或者算法常常面臨着變化,可是將他們組合在一塊兒的算法卻相對穩定。建造者模式提供一種封裝機制,將穩定的組合算法於易變的各個組成部分隔離開來。
建造者模式建立的對象通常內部變化是不大,不頻繁的。對於變更很頻繁的也是不適合用建造者模式的。就像買電腦,你三天兩頭換電腦,升級配置,而每升級一套配置,就須要從新建立一個全新的Builder
類,若是變更太多,系統中就須要維護大量的Builder
類,增長系統複雜度和維護難度。
最後我舉兩個我知道的在實際框架或者JDK
源碼中使用建造者模式的例子。其實建造者模式在實際開發中應用也是很是普遍的,並且也比較好識別,基本以xxxBuilder
命名的都是使用了建造者模式。
好比JDK
中,StringBuilder
類是一個經典的建造者模式實現,
//經典用法
String s = new StringBuilder("a").append("b").append("c").toString();
複製代碼
StringBuilder
繼承自AbstractStringBuilder
,這是一個抽象類,在它裏面定義了一個屬性value
數組。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//省略無關代碼
char[] value;
//省略無關代碼
}
複製代碼
當咱們調用StringBuilder
的構造器或者是append()
方法時,其實是對該value
數組操做。好比append()
爲例,
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
複製代碼
他會去調用父類的append()
方法,同時會return this
,實現鏈式風格的編程。在父類append()
方法中,最終會調用以下:
public AbstractStringBuilder append(CharSequence s, int start, int end) {
//省略一些校驗擴容操做
for (int i = start, j = count; i < end; i++, j++)
value[j] = s.charAt(i);
count += len;
return this;
}
複製代碼
其實就是往value
數組中設置傳入的字符串值。有點像ArrayList
的感受。
當調用toString()
方法時,其實就是把value
數組的內容轉成String
返回罷了。
public String toString() {
return new String(value, 0, count);
}
複製代碼
另外,在Mybatis
中也是大量使用了建造者模式。在Mybatis
啓動時,會作一系列的解析工做,好比mybatis-config.xml
文件解析,各Mapper.xml
,還有Mapper
接口上的註解等,這一系列的解析工做都是經過一系列的Builder
完成的。頂層是BaseBuilder
類。這些Builder
的層次結構以下圖,好比,XMLConfigBuilder
就是用來解析mybatis-config.xml
文件,XMLMapperBuilder
用來解析各Mapper.xml
等。
在這裏,BaseBuilder
做爲一個頂層抽象接口,裏面只定義了一些全部具體Builder
的屬性和方法,好比全局配置Configuration
對象。而其他Builder
子類做爲具體建造者完成各自的解析工做。咱們這邊以XMLConfigBuilder
爲例,XMLConfigBuilder.parse()
方法是解析的核心方法。裏面會調用BseBuilder
中的parseConfiguration()
方法。parseConfiguration()
方法就是整個裝配流程,
private void parseConfiguration(XNode root) {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
}
複製代碼
咱們以propertiesElement
爲例,這個方法會使用XPATH
解析properties
字段中的resource
,url
屬性,而且會將其設置到定義在BaseBuilder
中的configuration
中。其他的解析方法同理,就再也不贅述了。當全部的裝配工做完成以後,XMLConfigBuilder.parse()
就會將解析後的Configuration
對象返回。在SqlSessionFactoryBuilder
的build()
方法中會調用XMLConfigBuilder.parse()
,根據組裝好的Configuration
對象生成SqlSessionFactory
,進而建立SqlSession
以操做數據庫。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
//省略異常處理
}
複製代碼
好了,今天關於建造者模式的技術分享就到此結束。隨着本次建造者模式的結束,GOF23的建立型模式就都講完了。從下一篇開始,咱們會進入另一大類——結構型模式的世界。結構型模式的模式主要關注點是如何經過必定手段,將兩個或多個對象組合造成一個更大更強大的對象。下一篇我就將從結構型模式中最重要,也是最難理解的一個設計模式——代理模式開始講起,具體咱們下期再說,敬請期待。😊👏