1. 程式人生 > >Java 設計模式情景分析——建造者模式

Java 設計模式情景分析——建造者模式

當我們遇到類似汽車的裝配,需要車輪、方向盤、發動機,還有各種小零件時,為了在構建過程中隱藏實現細節,就可以使用建造者模式 (Builder模式) 將部件和組裝過程分離,使得構建過程和部件都可以自由擴充套件,兩者之間的耦合也降到最低。接下來我們看一下定義,建造者模式是將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。

1.建造者模式的使用情景

1)相同的方法,不同的執行順序,產生不同的事件結果時;
2)多個部件,都可以裝配到一個物件中,但是產生的執行結果又不相同時;
3)產品類非常複雜,或者產品類中的呼叫順序不同產生了不同的作用時;
4)當初始化一個物件特別複雜時;

2.程式中使用建造者模式的優缺點

- 建造者模式
優點 良好的封裝性,使用建造者模式可以使客戶端不必知道產品內部組成的細節;建造者獨立,擴充套件性好。
缺點 會產生多餘的 Builder 物件,消耗記憶體。

3.建造者模式的UML類圖

1.經典模式

類圖

  • Product產品類:產品的抽象類
  • Builder:抽象Builder類,規範產品的組建
  • ConcreteBuilder:具體的Builder類
  • Director:統一組裝過程

值得注意的是,在 Android 開發中,Director 物件經常會被省略,而直接使用一個 Builder 來進行物件的組裝,這個 Builder 通常為鏈式呼叫,下面我們看一下鏈式呼叫的類圖。

2.簡化模式 鏈式呼叫

鏈式呼叫的類圖

  • Product產品類:產品的抽象類
  • Builder:抽象Builder類,規範產品的組建
  • ConcreteBuilder:具體的Builder類,鏈式呼叫

下面我們給出的實現也基於鏈式呼叫的。

4.建造者模式的實現——鏈式呼叫

1.定義Product產品類(以組裝計算機為例):

public abstract class Computer {
    protected String board;
    protected String display;
    protected String os;
    protected Computer
() { } // 設定主機板 public void setmBoard(String board) { this.board = board; } // 設定顯示器 public void setmDisplay(String display) { this.display = display; } // 設定作業系統 public abstract void setmOS(); @Override public String toString() { return "Computer{" + "board='" + board + '\'' + ", display='" + display + '\'' + ", os='" + os + '\'' + '}'; } }

具體的 Computer 類:

public class MacBook extends Computer {
    protected MacBook() {
    }
    @Override
    public void setmOS() {
        os = "mac OS Sierra";
    }
}

2.定義抽象Builder類,規範產品的組建:

public abstract class Builder {
    // 設定主機板
    public abstract Builder setBoard(String board);
    // 設定顯示器
    public abstract Builder setDisplay(String display);
    // 設定作業系統
    public abstract Builder setOS();
    // 建立 Computer
    public abstract Computer create();
}

具體的Builder類:

public class MacBookBuilder extends Builder {
    private Computer mComputer = new MacBook();
    @Override
    public Builder setBoard(String board) {
        mComputer.setmBoard(board);
        return this;
    }
    @Override
    public Builder setDisplay(String display) {
        mComputer.setmDisplay(display);
        return this;
    }
    @Override
    public Builder setOS() {
        mComputer.setmOS();
        return this;
    }
    @Override
    public Computer create() {
        return mComputer;
    }
}

鏈式呼叫的關鍵點是每個 setter 方法都返回自身,也就是 return this; 這樣就使得 setter 方法可以為鏈式呼叫。

3.測試程式碼:

@Test
public void demo() {
    // 鏈式呼叫
    Computer computer = new MacBookBuilder().setBoard("Intel").setDisplay("Retina").setOS().create();
    System.out.println("Computer info:" + computer.toString());
}

列印結果:Computer info:Computer{board=’Intel’, display=’Retina’, os=’mac OS Sierra’}

5.Android系統原始碼中的應用情景

在Android系統原始碼中,最常用到建造者模式的就是 AlertDialog.Builder,使用該 Builder 構建複雜的 AlertDialog 物件,組裝 Dialog 的各個部分,達到構建和表示分離的效果,其使用的也是鏈式呼叫,下面來看一個簡單的應用:

new android.support.v7.app.AlertDialog.Builder(context)
        .setTitle("AlertDialog")
        .setMessage("Something important.")
        .setCancelable(false) // 設定點選Dialog以外的介面不消失,按返回鍵也不起作用
        .setPositiveButton("OK", new android.content.DialogInterface.OnClickListener() {
            @Override
            public void onClick(android.content.DialogInterface dialogInterface, int i) {
            }
        })
        .setNegativeButton("Cancel", null)
        .show();

我們看一下 android.support.v7.app.AlertDialog 的相關原始碼:

public class AlertDialog extends AppCompatDialog implements DialogInterface {
    // AlertController 接收 Builder 成員變數 P 中的各個引數
    final AlertController mAlert;
    // 建構函式
    protected AlertDialog(Context context) {
        this(context, 0);
    }
    protected AlertDialog(Context context, @StyleRes int themeResId) {
        super(context, resolveDialogTheme(context, themeResId));
        mAlert = new AlertController(getContext(), this, getWindow());
    }
    // 程式碼省略
    // 實際上呼叫 mAlert 的 setTitle 方法
    @Override
    public void setTitle(CharSequence title) {
        super.setTitle(title);
        mAlert.setTitle(title);
    }
    public void setMessage(CharSequence message) {
        mAlert.setMessage(message);
    }
    // 程式碼省略
    public static class Builder {
        // 1.儲存 AlertDialog 的各個引數,例如 title、message
        private final AlertController.AlertParams P;
        // 程式碼省略
        public Builder(Context context) {
            this(context, resolveDialogTheme(context, 0));
        }
        public Builder(Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }
        @NonNull
        public Context getContext() {
            return P.mContext;
        }
        // 程式碼省略
        // 2.設定各種引數
        public Builder setTitle(CharSequence title) {
            P.mTitle = title;
            return this;
        }
        public Builder setMessage(CharSequence message) {
            P.mMessage = message;
            return this;
        }
        // 程式碼省略
        // 3.構建 AlertDialog,傳遞引數
        public AlertDialog create() {
            // 4.呼叫 new AlertDialog 構造物件,並且將引數傳遞給個體 AlertDialog
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            // 5.將 P 中的引數應用到 dialog 中的 mAlert 物件中
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }
        public AlertDialog show() {
            final AlertDialog dialog = create();
            dialog.show();
            return dialog;
        }
    }
}

上述程式碼中,Builder 類可以設定 AlertDialog 中的各個引數,這些引數都儲存在型別為 AlertController.AlertParams 的成員變數 P 中,在呼叫 create() 方法時會建立 AlertDialog,並且將 P 中儲存的引數應用到 AlertDialog 的 mAlert 物件中,即 P.apply(dialog.mAlert); 程式碼段。我們再看 apply 方法的實現:

public void apply(AlertController dialog) {
    if (mCustomTitleView != null) {
        dialog.setCustomTitle(mCustomTitleView);
    } else {
        if (mTitle != null) {
            dialog.setTitle(mTitle);
        }
        if (mIcon != null) {
            dialog.setIcon(mIcon);
        }
        if (mIconId != 0) {
            dialog.setIcon(mIconId);
        }
        if (mIconAttrId != 0) {
            dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
        }
    }
    if (mMessage != null) {
        dialog.setMessage(mMessage);
    }
    if (mPositiveButtonText != null) {
        dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                mPositiveButtonListener, null);
    }
    if (mNegativeButtonText != null) {
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                mNegativeButtonListener, null);
    }
    if (mNeutralButtonText != null) {
        dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                mNeutralButtonListener, null);
    }
    // 如果設定了 mItems,則表示是單選或者多選列表,此時建立一個 ListView
    if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
        createListView(dialog);
    }
    // 將 mView 設定給 Dialog
    if (mView != null) {
        if (mViewSpacingSpecified) {
            dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                    mViewSpacingBottom);
        } else {
            dialog.setView(mView);
        }
    } else if (mViewLayoutResId != 0) {
        dialog.setView(mViewLayoutResId);
    }
}

在 apply() 方法中,只是將 AlertParams 引數設定到 AlertController 中,然後當我們獲取到 AlertDialog 物件後,再通過 show() 方法就可以顯示這個對話方塊了。

建造者模式在 Android 開發中 也較為常用,通常作為配置類的構造器將配置的構建和表示分離開來,同時也是將配置從目標類中隔離出來,避免過多的 setter 方法。