一盏筐、簡(jiǎn)介
建造者模式(Builder Pattern)使用多個(gè)簡(jiǎn)單的對(duì)象一步一步構(gòu)建成一個(gè)復(fù)雜的對(duì)象座泳。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式诞仓,它提供了一種創(chuàng)建對(duì)象的最佳方式蛇损。通過(guò)建造者模式赁温,可以讓一個(gè)包含多構(gòu)造函數(shù)坛怪,多可選參數(shù)和濫用setters方法的復(fù)雜事物簡(jiǎn)單化。
假設(shè)你有一個(gè)包含大量屬性的類(類新建之后就不可改變)股囊,就像下面的User類一樣袜匿。
public class User {
private final String firstName; //required
private final String lastName; //required
private final int age; //optional
private final String phone; //optional
private final String address; //optional
...
}
現(xiàn)在想象一下,在你的類中有一些屬性是必須的稚疹,有一些是可選的居灯。你會(huì)如何創(chuàng)建這個(gè)類的實(shí)例?因?yàn)樗械膶傩远急宦暶鞒蒮inal類型内狗,所以你必須在構(gòu)造方法中設(shè)置它們怪嫌,但是你也想讓這個(gè)類的客戶端有忽略可選屬性的機(jī)會(huì)。
1.1 方案一
一個(gè)首先想到的可選方案是提供多個(gè)構(gòu)造方法柳沙。第一個(gè)構(gòu)造方法是只接收必須屬性作為參數(shù)岩灭,第二個(gè)是接收所有必須屬性和第一個(gè)可選屬性,第三個(gè)是接收所有必須屬性和兩個(gè)可選屬性赂鲤,依次類推噪径。實(shí)現(xiàn)起來(lái)如下所示:
public User(String firstName, String lastName) {
this(firstName, lastName, 0);
}
public User(String firstName, String lastName, int age) {
this(firstName, lastName, age, '');
}
public User(String firstName, String lastName, int age, String phone) {
this(firstName, lastName, age, phone, '');
}
public User(String firstName, String lastName, int age, String phone, String address) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.phone = phone;
this.address = address;
}
1.2 方案二
遵循JavaBean的規(guī)則,有一個(gè)默認(rèn)的無(wú)參構(gòu)造方法并且都有g(shù)etter和setter方法数初。就像這樣:
public class User {
private String firstName; // required
private String lastName; // required
private int age; // optional
private String phone; // optional
private String address; //optional
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
這種方法看起來(lái)很容易閱讀和維護(hù)找爱。在客戶端里我可以只創(chuàng)建一個(gè)空對(duì)象,然后只設(shè)置那些我感興趣的屬性妙真。那么缴允,這種方法有什么問(wèn)題?這種解決方案有兩個(gè)主要的問(wèn)題珍德。
- 第一個(gè)問(wèn)題是該類的實(shí)例狀態(tài)不固定练般。如果你想創(chuàng)建一個(gè)User對(duì)象,該對(duì)象的5個(gè)屬性都要賦值锈候,那么直到所有的setXX方法都被調(diào)用之前薄料,該對(duì)象都沒(méi)有一個(gè)完整的狀態(tài)。這意味著在該對(duì)象狀態(tài)還不完整的時(shí)候泵琳,一部分客戶端程序可能看見(jiàn)這個(gè)對(duì)象并且以為該對(duì)象已經(jīng)構(gòu)造完成摄职。
- 第二個(gè)不足是User類是易變的。你將會(huì)失去不可變對(duì)象帶來(lái)的所有優(yōu)點(diǎn)获列。
1.3 方案三
建造者模式實(shí)現(xiàn)
public class User {
private final String firstName; // required
private final String lastName; // required
private final int age; // optional
private final String phone; // optional
private final String address; // optional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
public String getPhone() {
return phone;
}
public String getAddress() {
return address;
}
public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
private String address;
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public UserBuilder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
使用建造者模式有如下優(yōu)勢(shì):
- User構(gòu)造方法是私有的谷市,這意味著該類不能在客戶端代碼里直接實(shí)例化。
- 該類是不可變的击孩。所有屬性都是final類型的迫悠,在構(gòu)造方法里面被賦值。另外巩梢,我們只為它們提供了getter方法创泄。
- builder類使用流式接口風(fēng)格艺玲,讓客戶端代碼閱讀起來(lái)更容易。
- builder類構(gòu)造方法只接收必須屬性鞠抑,為了確保這些屬性在構(gòu)造方法里賦值饭聚,只有這些屬性被定義成final類型。
- 現(xiàn)在搁拙,試圖創(chuàng)建一個(gè)新的User對(duì)象的客戶端代碼看起來(lái)如何那秒梳?讓我們來(lái)看一下:
public User getUser() {
return new User.UserBuilder('Jhon', 'Doe')
.age(30)
.phone('1234567')
.address('Fake address 1234')
.build();
}
需要注意的是要在builder的參數(shù)拷貝到建造對(duì)象之后再驗(yàn)證參數(shù),這樣驗(yàn)證的就是建造對(duì)象的字段感混,而不是builder的字段端幼。這么做的原因是builder類不是線程安全的,如果我們?cè)趧?chuàng)建真正的對(duì)象之前驗(yàn)證參數(shù)弧满,參數(shù)值可能被另一個(gè)線程在參數(shù)驗(yàn)證完和參數(shù)被拷貝完成之間的某個(gè)時(shí)間修改婆跑。這段時(shí)間周期被稱作“脆弱之窗”。
正確姿勢(shì):
public User build() {
User user = new user(this);
if (user.getAge() > 120) { // user的成員變量user.getAge()
throw new IllegalStateException(“Age out of range”); // thread-safe
}
return user;
}
錯(cuò)誤姿勢(shì):
public User build() {
if (age > 120) { // UserBuilder的成員變量age
throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
}
// This is the window of opportunity for a second thread to modify the value of age
return new User(this);
}
除了上述使用建造者模式的優(yōu)點(diǎn)之外庭呜,建造者模式還有的一個(gè)優(yōu)點(diǎn)是builder可以作為參數(shù)傳遞給一個(gè)方法滑进,讓該方法擁有為客戶端創(chuàng)建一個(gè)或者多個(gè)對(duì)象的能力,而不需要知道創(chuàng)建對(duì)象的任何細(xì)節(jié)募谎。為了這么做你可能通常需要一個(gè)如下所示的簡(jiǎn)單接口:
public interface Builder<T> {
T build();
}
借用之前的User例子扶关,UserBuilder類可以實(shí)現(xiàn)Builder<User>。如此数冬,我們可以有如下的代碼:
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}
例如:
public List<User> buildUserList(Builder<User> userBuilder){
List<User> mList = new ArrayList<>();
for (int i = 0; i < 5; i ++) {
mList.add(userBuilder.build());
}
return mList;
}
二节槐、實(shí)踐
2.1 Android源碼中(AlertDialog.Builder)
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Dialog");
builder.setMessage("這是 android.support.v7.app.AlertDialog 中的樣式");
builder.setNegativeButton("取消", null);
builder.setPositiveButton("確定", null);
builder.show();
2.2 開(kāi)源庫(kù)(UniversalImageLoader,Glide)初始化配置
public static Glide get(Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
Context applicationContext = context.getApplicationContext();
List<GlideModule> modules = new ManifestParser(applicationContext).parse();
GlideBuilder builder = new GlideBuilder(applicationContext);
for (GlideModule module : modules) {
module.applyOptions(applicationContext, builder);
}
glide = builder.createGlide();
for (GlideModule module : modules) {
module.registerComponents(applicationContext, glide);
}
}
}
}
return glide;
}
2.3 通用組件封裝
在我們的項(xiàng)目開(kāi)發(fā)過(guò)程中,會(huì)包含很多的功能拐纱。如二維碼掃描铜异、圖片選擇、地址選擇等秸架。(1)起初使用這些功能揍庄,我們直接在應(yīng)用工程中開(kāi)發(fā)功能代碼,需要修改的話直接改主工程代碼东抹;
(2)明顯第一種方式對(duì)于復(fù)用不是太友好蚂子,于是我們將組件抽成module,通過(guò)module依賴的形式引用缭黔。module提供config接口供外部實(shí)現(xiàn)食茎,在實(shí)現(xiàn)類中配置參數(shù),實(shí)現(xiàn)自己需要的效果馏谨。如下所示:
public class ScanModuleConfigImpl implements IScanModuleConfig {
@Override
public int getMaskColor() {
return ResourcesUtils.getColor(R.color.scan_mask);
}
@Override
public int getAngleColor() {
return ResourcesUtils.getColor(R.color.white);
}
@Override
public int getTitleBarHeight() {
return 70;
}
@Override
public float getTitleTextSize() {
return 18;
}
@Override
public int getTipAlpha() {
return 0xff;
}
@Override
public Drawable getSlideIcon() {
return ResourcesUtils.getDrawable(R.drawable.ic_scan_slider);
}
@Override
public String getCustomTitle() {
return ResourcesUtils.getString(R.string.scan_title);
}
@Override
public String getTip() {
return ResourcesUtils.getString(R.string.scan_tips);
}
@Override
public int getTipMargin() {
return 33;
}
@Override
public int getScanFrameTopMargin() {
return -1;
}
@Override
public int getScanFrameLeftMargin() {
return -1;
}
@Override
public int getScanFrameRightMargin() {
return -1;
}
@Override
public boolean isRequestFullScreen() {
return true;
}
}
(3)第二種實(shí)現(xiàn)方式已經(jīng)很靈活别渔,但是書(shū)寫起來(lái)還是略微復(fù)雜,且不利于閱讀。想到建造者模式:將一個(gè)復(fù)雜的構(gòu)建與其表示相分離钠糊,使得同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表示∫疾福考慮到這些組件基本都是功能獨(dú)立且與業(yè)務(wù)解耦抄伍,我們其實(shí)可以將這樣組件看成是一個(gè)復(fù)雜對(duì)象。核心功能是不變的部分(如二維碼掃碼組件的掃碼功能和圖片選擇器的圖片展示)管宵,因?yàn)樵诓煌瑧?yīng)用中使用截珍,視覺(jué)和一些細(xì)節(jié)會(huì)有差異,這樣我們可以將這些視為可變部分箩朴。如下圖所示岗喉,是基于建造者模式設(shè)計(jì)的二維碼掃描組件。
如上圖所示炸庞,QrScan
是組件提供給外部操作組件的操作類钱床。用戶通過(guò)設(shè)置QrScanConfigration
來(lái)初始化得到不同表現(xiàn)形式的二維碼掃描組件,這里QrScanConfigration
對(duì)象是基于建造者模式構(gòu)建的埠居。在組件內(nèi)部查牌,通過(guò)QrScanProxy
統(tǒng)一分發(fā)處理QrScanConfigration
攜帶的配置信息±暮荆基于建造者模式纸颜,封裝組件,并通過(guò)Maven
管理绎橘,可通過(guò)如下方式使用組件:
-
build.gradle配置中導(dǎo)入組件
compile 'com.netease.scan:lib-qr-scan:1.0.0'
-
AndroidManifest配置
// 設(shè)置權(quán)限 <uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> // 注冊(cè)activity <activity android:name="com.netease.scan.ui.CaptureActivity" android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.NoActionBar"/>
-
初始化
在需要使用此組件的Activity的onCreate方法中胁孙,或者在自定義Application的onCreate方法中初始化。public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // // 默認(rèn)配置 // QrScanConfiguration configuration = QrScanConfiguration.createDefault(this); // 自定義配置 QrScanConfiguration configuration = new QrScanConfiguration.Builder(this) .setTitleHeight(53) .setTitleText("來(lái)掃一掃") .setTitleTextSize(18) .setTitleTextColor(R.color.white) .setTipText("將二維碼放入框內(nèi)掃描~") .setTipTextSize(14) .setTipMarginTop(40) .setTipTextColor(R.color.white) .setSlideIcon(R.mipmap.capture_add_scanning) .setAngleColor(R.color.white) .setMaskColor(R.color.black_80) .setScanFrameRectRate((float) 0.8) .build(); QrScan.getInstance().init(configuration); } }
-
啟動(dòng)組件相關(guān)方法(在QrScan.java類中已經(jīng)提供)
-
開(kāi)啟掃描界面
public void launchScan(Context context, IScanModuleCallBack callback) { QrScanProxy.getInstance().setCallBack(callback); CaptureActivity.launch(context); }
-
關(guān)閉掃描界面
public void finishScan(CaptureActivity activity) { activity.finish(); }
-
重啟掃描功能
public void restartScan(CaptureActivity activity) { activity.restartCamera(); }
-
-
啟動(dòng)掃描并處理掃描結(jié)果
QrScan.getInstance().launchScan(MainActivity.this, new IScanModuleCallBack() { @Override public void OnReceiveDecodeResult(final Context context, String result) { mCaptureContext = (CaptureActivity)context; AlertDialog dialog = new AlertDialog.Builder(mCaptureContext) .setMessage(result) .setCancelable(false) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); QrScan.getInstance().restartScan(mCaptureContext); } }) .setPositiveButton("關(guān)閉", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); QrScan.getInstance().finishScan(mCaptureContext); } }) .create(); dialog.show(); } });
最終運(yùn)行效果如下圖所示:
二維碼展示Gif
類似的称鳞,圖片選擇器涮较,也可拆分可變和不可變部分,基于建造者模式封裝實(shí)現(xiàn)。
- 調(diào)用
ImageSelectorConfiguration configuration = new ImageSelectorConfiguration.Builder(this)
.setMaxSelectNum(9)
.setSpanCount(4)
.setSelectMode(ImageSelectorConstant.MODE_MULTIPLE)
.setTitleHeight(48)
.build();
- 實(shí)現(xiàn)效果

以上是本人在項(xiàng)目開(kāi)發(fā)過(guò)程中關(guān)于Builder模式實(shí)踐總結(jié)的一些思考窟哺,可能會(huì)有考慮不周的地方嗤军,歡迎大家批評(píng)指正。關(guān)于組件復(fù)用苫亦,大家如果有更好的方式,期待能夠一起交流討論~