結合 Android 淺談 Builder 模式

IAM四十二發表於2017-04-18

前言

Builder模式,物件建立型的設計模式。說起設計模式,可能會覺得有點高深莫測,其實不然,我們每天寫程式碼都在多多少少的和各種各樣的設計模式接觸,只是沒有察覺而已。這裡就來說一說Builder模式。

Android中的Builder模式

在Android開發中,什麼時候會用到Builder模式呢?其實很簡單,就是當你想使用一個控制元件時或者是一個物件時,沒有辦法直接把他New 出來;那麼這個控制元件(物件)的實現多半就是用到了Builder模式。

AlertDialog.Builder

    private void InitView() {
        //直接建立物件
        TextView mTextView = new TextView(this);
        Button mButton = new Button(this);

        // 用Builder模式建立Dialog
        AlertDialog.Builder builder=new 
                AlertDialog.Builder(this)
                .setTitle("My Dialog")
                .setMessage("This is Test Dialog")
                .setIcon(R.drawable.application_icon);
        AlertDialog dialog=builder.create();
    }複製程式碼

如上面的程式碼,我們可以按照普通的方式(new)建立TextView物件和Button物件;但是輪到AlertDialog時,卻需要首先建立一個AlertDialog.Builder物件,然後通過這個Builder物件才能建立AlertDialog的一個例項。同樣都是Android的控制元件,差距為什麼這麼大呢?(因為Dialog複雜呀!o(╯□╰)o)。

下面可以結合AlertDialog的原始碼簡單分析一下。

    protected AlertDialog(@NonNull Context context) {
        this(context, 0);
    }複製程式碼
  • 首先是他的構造方法,可以看到這個方法是用 protected 修飾的,這意味著除了AlertDialog的子類之外,其他類是無法訪問這個方法的。AlertDialog的其他兩個過載的構造方法也是用到protected關鍵字修飾,有興趣的同學可以自己參考原始碼;因此,在Activity或者是Fragment裡,我們是無法直接建立AlertDialog的例項的,而是需要通過Builder物件。
        public static class Builder {
        private final AlertController.AlertParams P;

        public Builder(@NonNull Context context) {
            this(context, resolveDialogTheme(context, 0));
        }
        public Builder(@NonNull Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }
        public Builder setTitle(@Nullable CharSequence title) {
            P.mTitle = title;
            return this;
        }
        public Builder setMessage(@Nullable CharSequence message) {
            P.mMessage = message;
            return this;
        }
        public Builder setIcon(@DrawableRes int iconId) {
            P.mIconId = iconId;
            return this;
        }

    .....
}複製程式碼
  • Builder類是AlertDialog內部的一個靜態類。在這個類裡有一個很關鍵的屬性P,可以關注一下,這個變數是final型別的;除此之外剩下的就是一系列的setxxx 方法,用於設定AlertDialog的不同屬性,例如上面列舉的三個方法,可以分別設定AlertDialog的Title,Message及 Icon 資訊。在這些方法中,都是把引數直接傳遞給了之前所說的P這個例項,而且每一個方法的返回值都是Builder類自身,這樣就方便開發者鏈式呼叫每一個方法,這樣不僅寫起來簡單,而且讀起來也很有邏輯感
        public AlertDialog create() {
            // We can't use Dialog's 3-arg constructor with the createThemeContextWrapper param,
            // so we always have to re-set the theme
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            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;
        }複製程式碼
  • 最後,在Builder類的create方法中完成了AlertDialog的建立;萬變不離其宗,AlertDialog的例項化,還是通過new建立出來,並通過之前所說的例項P和dialog例項實現了某種關聯(具體如何實現暫不展開討論),總之就是把之前通過Builder方法設定的一系列引數都配置到了最終的AlertDialog之上。

OKHttp中的Request.Builder

對於OKHttp,相信大家都不陌生,在構造Request物件時,就用到了Request.Builder。顧名思義,這裡也用到了Builder模式。

        findViewById(R.id.get).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText("");
                loading.setVisibility(View.VISIBLE);
                OkHttpClient client = new OkHttpClient();
                Request.Builder builder = new Request.Builder()
                        .url(BASE_URL)
                        .method("GET", null);

                Request request = builder.build();
                Call mCall = client.newCall(request);
                mCall.enqueue(new MyCallback());
            }
        });複製程式碼

以上是一個很典型的關於OKHttp的使用方式,這裡Request物件也不是直接建立,而是通過首先建立一個Request.Builder物件,再通過他的build方法建立出最終的request物件。
這裡可以粗略的看一下Request類的原始碼。

public final class Request {
  private final HttpUrl url;
  private final String method;
  private final Headers headers;
  private final RequestBody body;
  private final Object tag;


  private Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tag = builder.tag != null ? builder.tag : this;
  }
    // 省略部分....

  public static class Builder {
    private HttpUrl url;
    private String method;
    private Headers.Builder headers;
    private RequestBody body;
    private Object tag;

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
    }


    public Builder url(HttpUrl url) {
      if (url == null) throw new NullPointerException("url == null");
      this.url = url;
      return this;
    }

    /**
     * Sets the URL target of this request.
     *
     * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this
     * exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs.
     */
    public Builder url(String url) {
      if (url == null) throw new NullPointerException("url == null");

      // Silently replace websocket URLs with HTTP URLs.
      if (url.regionMatches(true, 0, "ws:", 0, 3)) {
        url = "http:" + url.substring(3);
      } else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
        url = "https:" + url.substring(4);
      }

      HttpUrl parsed = HttpUrl.parse(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }

    /**
     * Sets the URL target of this request.
     *
     * @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code
     * https}.
     */
    public Builder url(URL url) {
      if (url == null) throw new NullPointerException("url == null");
      HttpUrl parsed = HttpUrl.get(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }

    /**
     * Sets the header named {@code name} to {@code value}. If this request already has any headers
     * with that name, they are all replaced.
     */
    public Builder header(String name, String value) {
      headers.set(name, value);
      return this;
    }


    /** Removes all headers on this builder and adds {@code headers}. */
    public Builder headers(Headers headers) {
      this.headers = headers.newBuilder();
      return this;
    }

    public Builder method(String method, RequestBody body) {
      if (method == null) throw new NullPointerException("method == null");
      if (method.length() == 0) throw new IllegalArgumentException("method.length() == 0");
      if (body != null && !HttpMethod.permitsRequestBody(method)) {
        throw new IllegalArgumentException("method " + method + " must not have a request body.");
      }
      if (body == null && HttpMethod.requiresRequestBody(method)) {
        throw new IllegalArgumentException("method " + method + " must have a request body.");
      }
      this.method = method;
      this.body = body;
      return this;
    }


    public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }
  }
}複製程式碼
  • 首先,Request是final型別的,因此他不會有子類;再有就它唯一的構造方法是private的;因此,對於開發者來說就無法通過普通的方法(通過new)建立一個Request物件了。

  • Builder類,是一個靜態內部類,通過其構造方法可以看出,Request 預設的請求方式是GET請求方式,同時當我們在建立Builder物件時,如果沒有提供url 引數時會丟擲異常,這是合理的也是必須的,一個Http請求如果沒有url那麼一切都是空談,這裡丟擲異常 十分必要。在method方法中,會根據引數修改具體的請求方法,同時會根據請求方法判斷是否需要RequestBody。

  • 最後,通過build方法建立了Request,可以看到這裡呼叫的就是Request唯一的構造方法,傳遞的引數就是當前Builder例項。這樣建立的Request物件就是根據我們構造出來的Builder例項所量身定製的Request。便於下一步進行同步或非同步的網路請求。

至此,你可能會有疑問,所謂的Builder模式有什麼意義?

為什麼Android系統中建立一個AlertDialog要這麼複雜,像TextView一樣,直接new出來一個例項然後set各種屬性不也一樣可用嗎?
Request 物件的建立不使用Builder模式一樣也是可以的呀?上面各種異常處理,方法執行用普通的方式也可實現,Builder模式的價值在哪裡呢?

帶著這些疑問,讓我們去好好理解一下Builder模式。

Builder 模式

定義

將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示

適用場景

1.相同的方法,不同的執行順序,產生不同的事件結果
2.多個部件或零件,都可以裝配到同一個物件中,但是產生的執行結果又不相同
3.產品類非常複雜,或者產品中的呼叫順序不同產生了不同的作用
4.需要初始化一個物件特別複雜的物件,這個物件有很多引數,且有預設值

看這樣的概念也許有些抽象,下面還是通過程式碼來看看。在之前工廠方法模式中,我們用工廠方法模式列舉了Mobike於Ofo 物件生成的例子。這裡依舊以二者為例,看看用Builder模式怎麼寫。

public final class Bicycle {
    public static final int SHARED = 1;
    public static final int PRIVATE = 0;

    @IntDef({SHARED, PRIVATE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface bicycleType {
    }

    protected String color;
    protected String name;
    protected double charge;
    protected int number;
    protected int type;

    protected Bicycle(BicycleBuilder builder) {
        this.color = builder.color;
        this.name = builder.name;
        this.charge = builder.chager;
        this.number = builder.number;
        this.type = builder.type;
    }

    public static class BicycleBuilder {


        private String color;
        private String name;
        private double chager;
        private int number;
        private int type;

        public BicycleBuilder() {
            this.color = "黑色";
            this.name = "永久";
            this.chager = 0;
            this.number = 0;
            this.type = Bicycle.PRIVATE;
        }

        public BicycleBuilder setColor(String color) {
            this.color = color;
            return this;
        }

        public BicycleBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public BicycleBuilder setCharge(double chager) {
            this.chager = chager;
            return this;
        }

        public BicycleBuilder setNumber(int number) {
            this.number = number;
            return this;
        }

        public BicycleBuilder setType(@bicycleType int type) {
            this.type = type;
            return this;
        }

        public Bicycle build(){
            return new Bicycle(this);
        }
    }

    @Override
    public String toString() {
        String typeStr= type == SHARED ? "共享單車": "私人車輛";

        return "Bicycle{" +
                "color='" + color + '\'' +
                ", name='" + name + '\'' +
                ", charge=每分鐘" + charge +"/元"+
                ", number=" + number +
                ", type=" + typeStr +
                '}';
    }
}複製程式碼

在這裡Bicycle類包含5個特有的屬性,同時將其構造方法設定為protected。通過BicycleBuilder 類來真正實現建立Bicycle的例項。這裡BicycleBuilder的預設的構造方法,會建立一個普通的黑色永久牌私人自行車,而通過BicycleBuilder提供的幾個方法我們便可以建立不同的Bicycle例項。比如下面這種實現:

public class BuilderPatternActivity extends AppCompatActivity {
    private TextView bike_result;
    private Bicycle mBicycle;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_builder_pattern);
        bike_result = V.f(this, R.id.bike_result);
    }

    /**
     * 普通自行車
     * @param view
     */
    public void NormalBike(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder();
        mBicycle=builder.build();
        updateView(mBicycle);
    }
    /**
     * 膜拜單車
     * @param view
     */
    public void Mobike(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder()
                .setColor("橙色")
                .setName("膜拜單車")
                .setCharge(1.0)
                .setNumber(10010)
                .setType(Bicycle.SHARED);
        mBicycle=builder.build();
        updateView(mBicycle);
    }
    /**
     * OFO 單車
     * @param view
     */
    public void Ofo(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder()
                .setColor("黃色")
                .setName("OFO單車")
                .setCharge(0.5)
                .setNumber(40010)
                .setType(Bicycle.SHARED);
        mBicycle=builder.build();
        updateView(mBicycle);
    }


    private void updateView(Bicycle mBicycle) {
        bike_result.setText("");
        bike_result.setText(mBicycle.toString());
    }
}複製程式碼

通過Bicycle.BicycleBuilder 提供的一系列set方法,我們建立了mobike例項和ofo單車例項。

這就是Builder模式,正如定義和使用場景中提到的那樣,通過Builder模式,在同樣的構建過程下,我們可以建立不同的結果;通常來說,我們要建立的物件是很複雜的,有很多引數,這些引數中有些是必須的,比如OKHttp中Request的url引數,有些引數又會有預設值;總之,Builder模式,一種物件建立型的設計模式;為我們建立物件提供了一種思路。

最後,再說一個使用了Builder模式的東西-RxJava。說到RxJava我們很容易想到觀察者模式。不錯,RxJava最核心的思想就是觀察者模式;但是想一想我們使用RxJava的過程。

        ArrayList<String> datas = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            datas.add("item_" + i);
        }
        Observable.just(datas)
                .flatMap(new Func1<ArrayList<String>, Observable<String>>() {
                    @Override
                    public Observable<String> call(ArrayList<String> strings) {
                        return Observable.from(strings);
                    }
                })
                .map(new Func1<String, Integer>() {
                    @Override
                    public Integer call(String s) {
                        return s.hashCode();
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer Integer) {
                        Log.e(MainActivity.class.getSimpleName(), "call---->" + Integer);
                    }
                });複製程式碼

如上程式碼所示,在subscribe 方法執行之前,通過各種各樣的操作符,原始資料一個ArrayList變成了一個Integer型別的資料,也就是說我們使用操作符的過程,就是一個Builder模式構建的過程,直到生成我們最終需要的產品為止。這和Builder模式的定義以及使用場景是完全符合的。

Builder模式 VS 工廠方法模式

工廠模式一般都是建立一個產品,注重的是把這個產品建立出來就行,只要建立出來,不關心這個產品的組成部分。從程式碼上看,工廠模式就是一個方法,用這個方法就能生產出產品。

建造者模式也是建立一個產品,但是不僅要把這個產品建立出來,還要關係這個產品的組成細節,組成過程。從程式碼上看,建造者模式在建造產品時,這個產品有很多方法,建造者模式會根據這些相同方法但是不同執行順序建造出不同組成細節的產品。

工廠模式關心整體,建造者模式關心細節

最後

現在回到我們之前提出的問題,Builder模式的意義是什麼?看完之後你可能已經得到答案了,沒有任何實質意義,Builder模式的使用並不會使我們的程式碼執行速度加快。設計模式總的來說就是對是封裝、繼承、多型和關聯的反覆使用;是一種程式設計技巧,讓我們能寫出高質量程式碼的技巧。

最後再說一句,嚴格來說本文討論的Builder模式並不是標準意義上的Builder模式,在這裡我們從Android原始碼的角度出發,簡化了Builder模式,為了方便鏈式呼叫及習慣,捨棄了原本應有的Director角色。對正統的Builder模式感興趣的同學可以再去深入研究。

相關文章