問題概述
隨著項(xiàng)目進(jìn)度不斷地深入,類的設(shè)計(jì)將會(huì)越來越復(fù)雜,常常遇到這樣的情況:設(shè)計(jì)的類要用到很多屬性,有的是必須要有的(required fields),有些是可有可無的(optional fields),這樣就要求向構(gòu)造器(constructors)傳遞眾多的參數(shù),而如何合理構(gòu)建這樣需要大量參數(shù)的構(gòu)造器就會(huì)成為一個(gè)棘手的問題。
舉個(gè)例子,現(xiàn)在需要設(shè)計(jì)一個(gè)消息推送管理類,這個(gè)類被設(shè)計(jì)用來管理終端的推送消息服務(wù),要求傳入和消息推送服務(wù)器鏈接的所需要的信息、需要關(guān)注的主題列表,能夠推送消息的主題列表以及各種配置選項(xiàng)信息。以上提到的這些信息都是正常啟動(dòng)服務(wù)所必需的,讀者雖然不一定能完全理解這個(gè)類要做什么,但是可以感受到如果將這么多的數(shù)據(jù)都一股腦在構(gòu)造器里傳入,將是個(gè)比較糟糕的設(shè)計(jì),客戶端的代碼將是難以編寫和維護(hù)。
其實(shí)這樣的問題在工業(yè)界很常見,比如在著名的網(wǎng)絡(luò)通信框架Netty中,因?yàn)槭腔贜IO Socket通信,所以需要較為復(fù)雜的配置才能啟動(dòng)網(wǎng)絡(luò)連接。那Netty框架中是如何處理這個(gè)問題的呢?我們看下實(shí)例:
// Configure the bootstrap.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
//***Chain Builder Start***
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
.childOption(ChannelOption.AUTO_READ, false)
ChannelFuture f = b.bind(LOCAL_PORT).sync();
f.channel().closeFuture().sync();
//***Chain Builder End***
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}```
這是一段標(biāo)準(zhǔn)Netty框架中啟動(dòng)服務(wù)器端的代碼,請(qǐng)注意從注釋Chain Builder開始的這段“怪異的”代碼,這是針對(duì)ServerBootstrap對(duì)象的鏈?zhǔn)脚渲茫阪準(zhǔn)脚渲眠^程中每次調(diào)用方法都會(huì)返回這個(gè)對(duì)象的自引用,以便于后續(xù)繼續(xù)調(diào)用配置方法,這樣就形成一個(gè)配置的“流水線”,在流水線上所有需要的參數(shù)都被傳入,最后用來生成目標(biāo)對(duì)象(這里的目標(biāo)對(duì)象是ChannelFuture對(duì)象)。這樣生成實(shí)例的模式被稱為“***Builder Pattern***”(建造者模式)。
可能有的讀者對(duì)于這樣的設(shè)計(jì)模式感到奇怪:“為什么要多此一舉把代碼連成一串,分開一個(gè)一個(gè)地調(diào)用方法不是更清晰嗎”?存在即合理, 要說明Builder Pattern優(yōu)勢(shì)和應(yīng)用場(chǎng)景,我們首先要回顧下其他創(chuàng)建對(duì)象實(shí)例的方法,經(jīng)過對(duì)比和分析之后,Builder Pattern的意義也就自然清晰了。
#傳統(tǒng)創(chuàng)建對(duì)象實(shí)例的方法
最常見的解決方案是使用Telescoping Pattern(折疊模式)或者JavaBean模式。這兩種方法的利弊都很明顯,關(guān)于他們的分析文章很多,因?yàn)椴皇潜疚闹饕獌?nèi)容,所以只是簡(jiǎn)要討論。用興趣的讀者可以參考[這篇文章](http://blog.csdn.net/dm_vincent/article/details/8517175)深入了解。
1. Telescoping Pattern (TP)
應(yīng)用TP方法往往需要大量編寫的代碼,對(duì)于開發(fā)人員的壓力比較大,而且不易擴(kuò)展,只要新加一個(gè)要傳入的參數(shù),就需要重新構(gòu)建一個(gè)新的構(gòu)造器。此外,當(dāng)參數(shù)很多時(shí),由于傳入?yún)?shù)的順序是固定的,所以用戶往往需要查閱文檔才能找到應(yīng)該調(diào)用那種構(gòu)造器,用起來很不方便。過長(zhǎng)的參數(shù)列表也容易造成錯(cuò)誤,因參數(shù)順序錯(cuò)誤而造成的異常是很不好排查的。當(dāng)然,這樣的代碼閱讀起來也是很折磨,想一想一個(gè)構(gòu)造器有七八個(gè)參數(shù),沒有注釋是很難區(qū)分他們的,錯(cuò)一個(gè)都會(huì)有問題的。
2. JavaBean Pattern(JBP)
為了更靈活,JBP走了另一個(gè)極端,只提供一個(gè)無參構(gòu)造方法,然后通過setters方法來設(shè)置必要的域和可選的域。這樣代碼的編寫和閱讀都變得更加容易,但是卻帶來了新的安全問題:用戶可能在必要屬性還沒有配置完全好的情況就開始使用類對(duì)象。
如何讓代碼優(yōu)雅地保持JBP方法的靈活性同時(shí)又有著TP方法的安全性,這邊是Builder Pattern要做到工作
#分析Builder Pattern
接下來我們看看Builder Pattern是如何工作的,下面是《Effective Java》中給出的例子:
//Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {//Required parameters
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val){
calories = val;
return this; // return self reference
}
public Builder fat(int val)
{ fat = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public NutritionFacts build() { // return desired type object
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}```
Builder Pattern的工作一共分為三大步:
- 首先在類中創(chuàng)建靜態(tài)類Builder,其構(gòu)造函數(shù)中的參數(shù)為構(gòu)建類對(duì)象所必需的參數(shù);
- 接下來調(diào)用setter-like方法設(shè)置其他備選參數(shù),并且這些方法都會(huì)返回Builder對(duì)象的自引用;
- 最后通過build()方法,將整個(gè)Builder類實(shí)例傳遞給目標(biāo)類的私有構(gòu)造函數(shù)中,創(chuàng)建目標(biāo)對(duì)象。
最后創(chuàng)建對(duì)象的代碼范例是這樣的:
NutritionFactscocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();```
也就是我們?cè)谏厦嫣岬芥準(zhǔn)脚渲谩_@樣“流水線”配置可以優(yōu)雅清楚地設(shè)置所需的參數(shù),同時(shí),在通過Builder類的幫助下,只有在所有配置參數(shù)都設(shè)置完畢之后才會(huì)去生成對(duì)象,如果這個(gè)時(shí)候發(fā)現(xiàn)有參數(shù)沒有配置或者其值不符合要求,就可以在build()方法中直接拋出異常,避免生成對(duì)象。
所以Builder Pattern集TP和JBP的優(yōu)點(diǎn)于一身,安全而靈活,讀寫皆易懂,可擴(kuò)展能力強(qiáng)。
最后我們?cè)诨剡^頭來分析下前面提到的Netty框架中創(chuàng)建服務(wù)端Channel的代碼
ServerBootstrap b = new ServerBootstrap();
//Chain Builder Start
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
.childOption(ChannelOption.AUTO_READ, false)
ChannelFuture f = b.bind(LOCAL_PORT).sync();
f.channel().closeFuture().sync(); ```
ServerBootstrap類就相當(dāng)于Builder類,用于輔助設(shè)置所需的參數(shù)(類的名字就可以看出它的作用,只不過獨(dú)立存在而沒有作為目標(biāo)類的內(nèi)部類),流水作業(yè)配置將引導(dǎo)用戶得到最終的目標(biāo)對(duì)象——ChannelFuture實(shí)例。
值得注意的是,因?yàn)檫€需要對(duì)ChannelFuture對(duì)象進(jìn)行連續(xù)操作,所以后面的操作繼續(xù)采用了Builder Pattern中鏈?zhǔn)讲僮鞯哪J剑瑏肀WC代碼編寫的流暢性和優(yōu)雅,直接如下那樣“一鏈到底”也是未嘗不可。
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HexDumpProxyInitializer(REMOTE_HOST, REMOTE_PORT))
.childOption(ChannelOption.AUTO_READ, false)
.bind(LOCAL_PORT).sync();
.channel().closeFuture().sync(); ```
這充分體現(xiàn)了Netty框架在設(shè)計(jì)上良苦用心,以及Builder Pattern內(nèi)在的精髓。
不過這里要說明的,Netty框架很是復(fù)雜,并不是簡(jiǎn)單的套用了某種設(shè)計(jì)模式,往往多種模式根據(jù)實(shí)際情況混合使用,來達(dá)到最佳效果。這里的Netty實(shí)例代碼其實(shí)并不是嚴(yán)格的Builder Pattern,但是卻將其精髓鏈?zhǔn)脚渲玫膬?yōu)勢(shì)完善地體現(xiàn)出來,這也就要閱讀優(yōu)質(zhì)源碼的原因。
如果你手上也有需要大量參數(shù)的構(gòu)造器或是需要連續(xù)配置和操作的對(duì)象,為什么不試試Builder Patter和鏈?zhǔn)脚渲媚兀?/code>