1. 程式人生 > >當Android遇上設計模式之建造者(Builder)模式

當Android遇上設計模式之建造者(Builder)模式

眾所周知,無論是大部分書籍還是部落格,對設計模式的介紹也僅表現在簡單的java舉例層面,雖然是看懂了,但是在開發的過程中就是不知道如何應用到專案中,時間久了也容易忘記。因此從今天開始,我計劃從Android開發者角度寫一些關於設計模式系列文章,希望通過這個系列的文章我們不僅能夠理解這些設計模式的原理,更能夠將其應用到我們的實際專案中。

一、Builder模式原理剖析

 Builder,譯為”建造者”,我們可以很容易聯想想到建築工人建房子,其大概的流程由打地基、修建主體框架、裝修等部分組成,且每個部分是由不同的建築工人完成。雖說不同的建築工人修建的風格不一樣,但是隻要按照上述的修建流程,就能夠得到一棟完整的房子,這就是傳說中的Builder模式!

1. Builder模式

 所謂建造者(Builder)模式,即將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。通俗一點理解,該模式是為了將構建複雜物件的過程和它的部件解耦,使用構建過程和部件都可以自由擴充套件,同時對外部(客戶端)隱藏實現細節,客戶端只需呼叫相同的構建過程,就可以得到不同的產品(物件)。
根據Builder模式理論,以上述建房子為例,房子為要構建的複雜物件(Product),”打地基-修建主體框架-裝修”為構建過程(Director),”打地基”、”修建主體框架”、”裝修”為構建物件的部件(Builder),可以看出構建過程與它的部件是相互獨立的,而對於業主來說,他無法(無需)知道修建的整個流程和每個部分的具體細節,只需要事先告訴建築工人哪裡用什麼材料或怎麼設計,就可以得到他預想中的房子,並且每個部分建築工人還可以加入他自己的idea,業主最終可以獲取風格不同的房子。

2. UML類圖

 根據Builder模式的定義,可以得到該模式的UML類圖,有以下幾個角色:

image
- (1) Builder:抽象Builder類,規定產品有那些部件,且部件的具體實現由其子類實現;
- (2) ConcreteBuilder:Builder子類,產品部件的具體實現;
- (3) Director:規定構建物件的過程;
- (4) Product:具體產品類;

 從UML類圖可知,Builder和Director為聚合關係,Director類持有Builder的引用用於呼叫建立部件相關方法,而Builder是可以脫離Director而單獨存在的,即它們是相互獨立的;ConcreteBuilder和Builder為繼承關係,前者繼承於後者,且是後者的具體實現;ConcreteBuilder依賴於Product,ConcreteBuilder持有Product的引用才能完成其工作。

3. 時序圖

image

二、Android專案實戰

 在Android原始碼中,建造者(Builder)模式應用非常廣泛,比較經典的就是AlertDialog內部實現,它就是通過Builder物件來組裝Dialog的各個部分,如title、Message、Button等,將Dialog的構造和表示進行分離。因此,為了進一步理解Builder模式,我們在不看AlertDialog原始碼的情況下,嘗試著根據Builder模式的原理來實現Dialog的建立。該專案的UML類圖如下:

image

  • 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) 當建造過程相對穩定,但物件內部的構建通常面臨著複雜的變化;