結合 Android 淺談 Builder 模式

前言

Builder模式,對象創建型的設計模式。說起設計模式,可能會覺得有點高深莫測,其實不然,我們每天寫代碼都在多多少少的和各種各樣的設計模式接觸,只是沒有察覺而已。轉載自IAM四十二的博客,這裏就來說一說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模式感興趣的同學可以再去深入研究。

轉載自:IAM四十二的博客原文地址:https://juejin.im/post/58f630a544d904006c0eedc9

發佈了36 篇原創文章 · 獲贊 20 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章