當Android遇上設計模式之建造者(Builder)模式
眾所周知,無論是大部分書籍還是部落格,對設計模式的介紹也僅表現在簡單的java舉例層面,雖然是看懂了,但是在開發的過程中就是不知道如何應用到專案中,時間久了也容易忘記。因此從今天開始,我計劃從Android開發者角度寫一些關於設計模式系列文章,希望通過這個系列的文章我們不僅能夠理解這些設計模式的原理,更能夠將其應用到我們的實際專案中。
一、Builder模式原理剖析
Builder,譯為”建造者”,我們可以很容易聯想想到建築工人建房子,其大概的流程由打地基、修建主體框架、裝修等部分組成,且每個部分是由不同的建築工人完成。雖說不同的建築工人修建的風格不一樣,但是隻要按照上述的修建流程,就能夠得到一棟完整的房子,這就是傳說中的Builder模式!
1. Builder模式
所謂建造者(Builder)模式,即將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。通俗一點理解,該模式是為了將構建複雜物件的過程和它的部件解耦,使用構建過程和部件都可以自由擴充套件,同時對外部(客戶端)隱藏實現細節,客戶端只需呼叫相同的構建過程,就可以得到不同的產品(物件)。
根據Builder模式理論,以上述建房子為例,房子為要構建的複雜物件(Product),”打地基-修建主體框架-裝修”為構建過程(Director),”打地基”、”修建主體框架”、”裝修”為構建物件的部件(Builder),可以看出構建過程與它的部件是相互獨立的,而對於業主來說,他無法(無需)知道修建的整個流程和每個部分的具體細節,只需要事先告訴建築工人哪裡用什麼材料或怎麼設計,就可以得到他預想中的房子,並且每個部分建築工人還可以加入他自己的idea,業主最終可以獲取風格不同的房子。
2. UML類圖
根據Builder模式的定義,可以得到該模式的UML類圖,有以下幾個角色:
- (1) Builder:抽象Builder類,規定產品有那些部件,且部件的具體實現由其子類實現;
- (2) ConcreteBuilder:Builder子類,產品部件的具體實現;
- (3) Director:規定構建物件的過程;
- (4) Product:具體產品類;
從UML類圖可知,Builder和Director為聚合關係,Director類持有Builder的引用用於呼叫建立部件相關方法,而Builder是可以脫離Director而單獨存在的,即它們是相互獨立的;ConcreteBuilder和Builder為繼承關係,前者繼承於後者,且是後者的具體實現;ConcreteBuilder依賴於Product,ConcreteBuilder持有Product的引用才能完成其工作。
3. 時序圖
二、Android專案實戰
在Android原始碼中,建造者(Builder)模式應用非常廣泛,比較經典的就是AlertDialog內部實現,它就是通過Builder物件來組裝Dialog的各個部分,如title、Message、Button等,將Dialog的構造和表示進行分離。因此,為了進一步理解Builder模式,我們在不看AlertDialog原始碼的情況下,嘗試著根據Builder模式的原理來實現Dialog的建立。該專案的UML類圖如下:
- Builder,建造者抽象類。
Builder類是建造者抽象類,該類規定了一個Dialog物件由圖示、標題、內容、Button操作部件組成,並提供相關的抽象方法。程式碼如下:
/**Builder,建造一個Dialog物件的各個部分(部件)的抽象介面
* Created by jianddongguo on 2018/3/4.
*/
public abstract class Builder {
// 圖示
public abstract void setIcon(int id);
// 標題
public abstract void setTitle(String title);
// 內容
public abstract void setMessage(String[] content);
// 操作
public abstract void setOnDialogClickListener(String confirmText,String cancelText,
Dialog.OnDialogClickListener listener);
// 建立Dialog
public abstract Dialog create();
}
- AlertDialogBuilder,建造者具體類
繼承於Builder,用於建立一個Dialog物件各個組成部分的具體實現。比如我們需要建立的Dialog型別為提示對話方塊(AlertDialog),那麼對話方塊的ContentView應該是一個TextView,再給該TextView設定文字。也就是說,隨著建立Dialog型別不同,建造者具體類可以不同,比如我們需要建立一個列表對話方塊,那麼ContentView應該是一個ListView,該類的名字改為ListDialogBuilder。
/**
* Builder具體實現類,用於建造AlertDialog各部件
* Created by jianddongguo on 2018/3/4.
*/
public class AlertDialogBuilder extends Builder {
private Dialog mDialog = new AlertDialog();
private Context mContext;
public AlertDialogBuilder(Context ctx) {
mContext = ctx.getApplicationContext();
}
@Override
public void setIcon(int id) {
mDialog.setIcon(id);
}
@Override
public void setTitle(String title) {
mDialog.setTitle(title);
}
@Override
public void setMessage(String[] content) {
if (content != null || content.length > 0) {
TextView tv = new TextView(mContext);
tv.setText(content[0]);
tv.setTextSize(16);
mDialog.setContentView(tv);
}
}
@Override
public void setOnDialogClickListener(String confirmText, String cancelText, Dialog.OnDialogClickListener listener) {
mDialog.setClickButton(confirmText, cancelText,listener);
}
@Override
public Dialog create() {
return mDialog;
}
}
- Director
顧名思義,Director類是這整個故事的”導演”,雖然整個”劇組”的各個部分已經全部就緒,指揮棒(Builder)也已經交到Director手中,”導演”就會根據劇本安排好每個情節流程開始拍攝(construct),最終將所有情節根據預設好的劇本(流程)拍攝成電影(Dialog)。
/**Director層,
* Created by jianddongguo on 2018/3/4.
*/
public class Director {
private Builder mBuilder;
public Director(Builder builder) {
this.mBuilder = builder;
}
public void construct(int iconId, String title, String[] content, String confirmText, String cancelText, Dialog.OnDialogClickListener listener) {
mBuilder.setIcon(iconId);
mBuilder.setTitle(title);
mBuilder.setMessage(content);
mBuilder.setOnDialogClickListener(confirmText,cancelText,listener);
}
}
- Dialog,產品(抽象)類
/**Product層,產品類
* Created by jianddongguo on 2018/3/4.
*/
public abstract class Dialog {
protected int mIcon;
protected String mTitle;
protected View mContentView;
protected String mCancelText;
protected String mConfirmText;
protected Context mCtx;
protected OnDialogClickListener mClickListener;
public interface OnDialogClickListener {
void onCancle();
void onConfirm(String content);
}
public void setIcon(int icon) {
this.mIcon = icon;
}
public void setTitle(String title) {
this.mTitle = title;
}
public void setContentView(View view) {
this.mCtx = view.getContext();
this.mContentView = view;
}
public void setClickButton(String confirmText,String cancelText,OnDialogClickListener listener) {
this.mCancelText = cancelText;
this.mConfirmText = confirmText;
this.mClickListener = listener;
}
public abstract void show();
public abstract void dismiss();
}
- AlertDialog
繼承於Dialog的產品類,它不會理會一個Dialog產品建立流程和每個部分的實現具體細節,只需要關注自己提供建立所需資訊即可。程式碼如下:
/**
* Dialog,產品具體實現類
* Created by jianddongguo on 2018/3/4.
*/
public class AlertDialog extends Dialog {
private WindowManager mWindowManager;
private View mRootView;
@Override
public void show() {
// 使用WindowManager新增View到Window
mWindowManager = (WindowManager) mCtx.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT
, WindowManager.LayoutParams.WRAP_CONTENT);
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.width = 800;
params.height = 600;
params.gravity = Gravity.CENTER;
mRootView = LayoutInflater.from(mCtx).inflate(R.layout.layout_dialog, null);
ImageView ivIcon = (ImageView) mRootView.findViewById(R.id.iv_dialog_icon);
ivIcon.setImageResource(mIcon);
TextView tvTitle = (TextView) mRootView.findViewById(R.id.tv_dialog_title);
tvTitle.setText(mTitle);
LinearLayout contentLayout = (LinearLayout) mRootView.findViewById(R.id.llayout_content);
contentLayout.addView(mContentView);
Button btnCancel = (Button) mRootView.findViewById(R.id.btn_cancel);
btnCancel.setText(mCancelText);
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mClickListener != null) {
mClickListener.onCancle();
}
dismiss();
}
});
Button btnConfirm = (Button) mRootView.findViewById(R.id.btn_confirm);
btnConfirm.setText(mConfirmText);
btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mContentView instanceof TextView) {
if(mClickListener != null) {
mClickListener.onConfirm((String) ((TextView) mContentView).getText());
}
}
dismiss();
}
});
mWindowManager.addView(mRootView, params);
}
@Override
public void dismiss() {
if(mWindowManager != null) {
mWindowManager.removeView(mRootView);
}
}
}
客戶端呼叫(Activity)
// 例項化Builder構建物件 AlertDialogBuilder alertBuidler = new AlertDialogBuilder(this); // 指定構建流程,開始構建 Director alertDirect = new Director(alertBuidler); alertDirect.construct(R.mipmap.ic_launcher, "AlertDialog標準版", new String[]{"這是標準版測試"}, "確認", "取消", new Dialog.OnDialogClickListener() { @Override public void onCancle() { showToast("取消操作"); } @Override public void onConfirm(String content) { showToast("確認,列印內容:"+content); } }); // 得到產品 alertBuidler.create().show();
至此,一個標準的Builder模式流程實戰就完成了。由於Builder模式構建過程與部件高度解耦的特點,我們可以再不修改Director類任何程式碼的情況下,通過建立不同的Buidler子類,來建立風格不同的Dialog。比如,我需要建立一個列表對話方塊(ListDialog),只需要建立一個ListDialogBuilder子類(繼承於Builder)重新設計對話方塊組成部分就可以實現。當然,實際應用中可能不會按照這種標準的流程來使用Builder模式,會對其進行簡化,通常Builder類會直接承擔Director、Builder、ConcreteBuilder的責任。通過對上述專案重構,你會發現重構後的AlertDialog同Android原始碼中的實現結構是如此相似,程式碼如下:
public class AlertDialog2 {
protected int mIcon;
protected String mTitle;
protected String mCancelText;
protected String mConfirmText;
protected Context mCtx;
protected Dialog.OnDialogClickListener mClickListener;
private View mContentView;
private WindowManager mWindowManager;
private View mRootView;
public AlertDialog2(Context ctx) {
this.mCtx = ctx.getApplicationContext();
}
public interface OnDialogClickListener {
void onCancle();
void onConfirm(String content);
}
public void setIcon(int icon) {
this.mIcon = icon;
}
public void setTitle(String title) {
this.mTitle = title;
}
public void setContentView(View view) {
this.mContentView = view;
}
public void setClickButton(String confirmText, String cancelText, Dialog.OnDialogClickListener listener) {
this.mCancelText = cancelText;
this.mConfirmText = confirmText;
this.mClickListener = listener;
}
public void show() {
// 使用WindowManager新增View到Window
mWindowManager = (WindowManager) mCtx.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT
, WindowManager.LayoutParams.WRAP_CONTENT);
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.width = 800;
params.height = 600;
params.gravity = Gravity.CENTER;
mRootView = LayoutInflater.from(mCtx).inflate(R.layout.layout_dialog, null);
ImageView ivIcon = (ImageView) mRootView.findViewById(R.id.iv_dialog_icon);
ivIcon.setImageResource(mIcon);
TextView tvTitle = (TextView) mRootView.findViewById(R.id.tv_dialog_title);
tvTitle.setText(mTitle);
LinearLayout contentLayout = (LinearLayout) mRootView.findViewById(R.id.llayout_content);
contentLayout.addView(mContentView);
Button btnCancel = (Button) mRootView.findViewById(R.id.btn_cancel);
btnCancel.setText(mCancelText);
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mClickListener != null) {
mClickListener.onCancle();
}
dismiss();
}
});
Button btnConfirm = (Button) mRootView.findViewById(R.id.btn_confirm);
btnConfirm.setText(mConfirmText);
btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mContentView instanceof TextView) {
if (mClickListener != null) {
mClickListener.onConfirm((String) ((TextView) mContentView).getText());
}
}
dismiss();
}
});
mWindowManager.addView(mRootView, params);
}
public void dismiss() {
if (mWindowManager != null) {
mWindowManager.removeView(mRootView);
}
}
/**
* 建造者,AlertDialog內部類
* 此處採用鏈式呼叫
*/
public static class Builder {
private Context mCtx;
private AlertDialog2 mDialog;
public Builder(Context ctx) {
this.mCtx = ctx;
mDialog = new AlertDialog2(mCtx);
}
public Builder setIcon(int id) {
mDialog.setIcon(id);
return this;
}
public Builder setTitle(String title) {
mDialog.setTitle(title);
return this;
}
public Builder setMessage(String[] content) {
if (content != null || content.length > 0) {
TextView tv = new TextView(mCtx);
tv.setText(content[0]);
tv.setTextSize(16);
mDialog.setContentView(tv);
}
return this;
}
public Builder setOnDialogClickListener(String confirmText, String cancelText, Dialog.OnDialogClickListener listener) {
mDialog.setClickButton(confirmText, cancelText, listener);
return this;
}
public AlertDialog2 create() {
return mDialog;
}
}
}
最後說一句:
Builder模式的最大優勢是構建過程與組成解耦,兩者可自由擴充套件,且對外部隱藏實現細節。如果需要實現不同的內部細節,只需要建立一個新的Builder子類即可。
基於此,常用的場合有:
(1) 當初始化一個物件特別複雜,如引數多,且很多引數都具有預設值時;
(2) 當建造過程相對穩定,但物件內部的構建通常面臨著複雜的變化;