如何構(gòu)建含有大量參數(shù)的構(gòu)造器:淺談Builder Pattern的使用和鏈?zhǔn)脚渲?/h1>

問題概述

隨著項(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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容