從OkHttp中學習設計模式--建造者模式

建造者模式

建造者模式(Builder Pattern):將對象的創建與表示相分離,使同樣的構建過程產生不一樣的表示,我們可以將這樣的設計模式我們稱之爲建造者模式,也稱構建者模式。一般用於比較複雜的構建對象。

OkHttp中的建造者模式

建造者模式,一般框架中命名規範的都會命名爲XXBuilder的構造器。在OkHttp中OkHttpClient中就有該類定義,包含一個內部靜態類Builder。此外OkHttp中的RequestResponseHttpUrlHeadersMultipartBody等大量使用了類似的建造者模式。由於使用的原因和方式都類似,下面我們從OkHttpClient來分析一下OkHttp中的建造者模式的使用。

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
......
  public static final class Builder {
    Dispatcher dispatcher;
    @Nullable Proxy proxy;
    int callTimeout;
    int connectTimeout;
......
    public Builder dispatcher(Dispatcher dispatcher) {
      if (dispatcher == null) throw new IllegalArgumentException("dispatcher == null");
      this.dispatcher = dispatcher;
      return this;
    }
    
    public Builder proxy(@Nullable Proxy proxy) {
      this.proxy = proxy;
      return this;
    }
    
    @IgnoreJRERequirement
    public Builder callTimeout(Duration duration) {
      callTimeout = checkDuration("timeout", duration.toMillis(), TimeUnit.MILLISECONDS);
      return this;
    }

    public Builder connectTimeout(long timeout, TimeUnit unit) {
      connectTimeout = checkDuration("timeout", timeout, unit);
      return this;
    }
......

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

OkHttpClient靜態內部類Builder封裝了一系列的可配置參數的設置方法,並返回該對象本身(例如dispatcher()proxy()callTimeout()等),方便用戶用鏈式編程技巧調用參數的配置,更利於代碼的閱讀和理解。

OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(new LogInterceptor())
				....
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .cache(cache)
                .build();

如上代碼所示,新建一個OkHttpClient的構造器Builder,然後添加一個log攔截器,在設置連接超時,寫超時,讀取超時,緩存,最後調用Builder.build()方法將Builder對象傳遞到OkHttpClient的構造方法裏完成參數的傳遞和對象的創建。
OkHttpClient.Builder的默認無參構造方法還設置了一系列默認的參數初始值,如果沒有配置的就會使用默認的初始參數,

.....
    public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      if (proxySelector == null) {
        proxySelector = new NullProxySelector();
      }
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      callTimeout = 0;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
    }

OkHttpClient.Builder另外hai 提供了根據原有OkHttpClient的配置參數的構造函數,代碼如下:


    Builder(OkHttpClient okHttpClient) {
      this.dispatcher = okHttpClient.dispatcher;
      this.proxy = okHttpClient.proxy;
      this.protocols = okHttpClient.protocols;
      this.connectionSpecs = okHttpClient.connectionSpecs;
      this.interceptors.addAll(okHttpClient.interceptors);
      this.networkInterceptors.addAll(okHttpClient.networkInterceptors);
      ......
      this.callTimeout = okHttpClient.callTimeout;
      this.connectTimeout = okHttpClient.connectTimeout;
      this.readTimeout = okHttpClient.readTimeout;
      this.writeTimeout = okHttpClient.writeTimeout;
      this.pingInterval = okHttpClient.pingInterval;
    }

OkHttp爲什麼要使用建造者模式?

我們現通過下面的表來對比使用構造模式的優劣:

優勢 劣勢
對象創建與表示分離,OkHttpClient負責對外提供功能,Builder負責對象的創建,符合類的單一職責 類增加
對象創建過程使用鏈式編程,集中參數配置,利於閱讀和維護,樣式美觀 -----
封裝了複雜對象的創建過程,使對象對外提供的api更純粹 -----

標準的構造者模式

一般設計模式類相關的書籍介紹的建造者模式,不同於上面我們介紹的OkHttpClient中介紹的使用方式。其類圖大概如下所示:
通用建造者類圖
一般會有以下四個角色:
產品類(Product):要創建的目標對象,有可能是抽象類,並且有多個子類,也有可能就是直接要創建的產品類。
創建者(Builder):用於創建產品對象的建造器接口,一般是抽象類或者接口,具體產品創建由其子類實現,可以實現多種建造器實現不同的產品創建。有時候如果只需要一個創建者,也可以直接定義爲具體的直接創建者類,比如OkHttp中使用的就是。
直接創建者(ConcreteBuilder):實現了產品各種部件的創建接口的實現類,用於創建產品對象的創建類。
指揮者(Director):也稱爲導演類,指定複雜對象具體的創建順序,根據傳入的創建器創建對象。一般高層調用者根據需要傳入相應的構造器創建所需要的對象。

示例

具體示例我就參考這篇博客:設計模式 | 建造者模式及典型應用
比如工廠流水線組裝電腦,先裝CPU,內存,顯卡,主板…等等。

public class Computer {
    private String brand;
    private String cpu;
    private String mainBoard;
    private String hardDisk;
    private String displayCard;
    private String power;
    private String memory;
    // 省略 getter, setter, toString
}
public abstract class Builder {
    protected Computer computer = new Computer();

    public abstract void buildBrand();
    public abstract void buildCPU();
    public abstract void buildMainBoard();
    public abstract void buildHardDisk();
    public abstract void buildDisplayCard();
    public abstract void buildPower();
    public abstract void buildMemory();
    public Computer createComputer() {
        return computer;
    }
}

具體的建造者,LenovoComputerBuilderDellComputerBuilder


public class LenovoComputerBuilder extends Builder{
    @Override
    public void buildBrand() {
        computer.setBrand("聯想電腦");
    }
    @Override
    public void buildCPU() {
        computer.setCpu("i7 cpu");
    }
    @Override
    public void buildMainBoard() {
        computer.setMainBoard("Lenovo主板");
    }
    @Override
    public void buildHardDisk() {
        computer.setHardDisk("256GB SSD");
    }
    @Override
    public void buildDisplayCard() {
        computer.setDisplayCard("獨立2GB");
    }
    @Override
    public void buildPower() {
        computer.setPower("3芯 鋰離子電池 65W AC適配器");
    }
    @Override
    public void buildMemory() {
        computer.setMemory("8GB");
    }
}

public class DellComputerBuilder extends Builder {
    @Override
    public void buildBrand() {
        computer.setBrand("戴爾電腦");
    }
    @Override
    public void buildCPU() {
        computer.setCpu("i5-8300H 四核");
    }
    @Override
    public void buildMainBoard() {
        computer.setMainBoard("戴爾主板");
    }
    @Override
    public void buildHardDisk() {
        computer.setHardDisk("1T + 128GB SSD");
    }
    @Override
    public void buildDisplayCard() {
        computer.setDisplayCard("GTX1060 獨立6GB");
    }
    @Override
    public void buildPower() {
        computer.setPower("4芯 鋰離子電池 180W AC適配器");
    }
    @Override
    public void buildMemory() {
        computer.setMemory("4G + 4G");
    }
}

指揮者 ComputerDirector,指揮構建過程

public class ComputerDirector {
    public Computer constructA(Builder builder) {
        // 流水線A:逐步構建複雜產品對象
        Computer computer;
        builder.buildBrand();
        builder.buildCPU();
        builder.buildDisplayCard();
        builder.buildHardDisk();
        builder.buildMainBoard();
        builder.buildMemory();
        builder.buildPower();
        computer = builder.createComputer();
        return computer;
    }

	//有時候會出現不同生產線根據資源不同,調整組裝順序
    public Computer constructB(Builder builder) {
        // 流水線B:逐步構建複雜產品對象
        Computer computer;
        builder.buildCPU();
        builder.buildDisplayCard();
        builder.buildHardDisk();
        builder.buildMainBoard();
        builder.buildMemory();
        builder.buildPower();
        //最後貼品牌標籤
        builder.buildBrand();
        computer = builder.createComputer();
        return computer;
    }
}

客戶端測試

public class Test {
    public static void main(String[] args) {
        ComputerDirector director = new ComputerDirector();

        Builder lenovoBuilder = new LenovoComputerBuilder();
        Computer levonoComputer = director.construct(levonosBuilder);
        System.out.println(levonoComputer.toString());

        Builder dellBuilder = new DellComputerBuilder();
        Computer dellComputer = director.construct(dellBuilder);
        System.out.println(dellComputer.toString());
    }
}

整個過程的簡單類圖大致如下:
Computer構造模式類圖
和標準的建造者模式類圖很相似,Lenovo品牌和Dell品牌電腦一般都是行業規定的標準配置,所以可以分爲兩個不同的構造器來構建對象,所以分爲兩個單獨的建造器類很合理,也符合類的單一職責。

對比OkHttp中的建造者模式的不同

對比OkHttp中的建造模式,可以發現有些不同:

  • OkHttp中只有一個構造器,並沒有子類: OkHttp中OkHttpClient只能構造一種對象,區別只在於配置參數的值不同;示例Computer模擬生產過程中,有了兩種市場標準的不同品牌的兩種不同產品,每種品牌的參數標準基本固定,所以可以分爲兩個不同的構造器,如果不分開不符合類的單一職責。
  • OkHttp中構造器爲產品類的靜態內部類 因爲OkHttp中只有唯一的專屬構造器,所以不必單獨列出一個類,也利於其他人理解(專屬構造器),命名也可以簡單命名爲Buildernew的時候調用OkHttpClient.Builder()一看就知道是OkHttpClient的專屬構造器。從而不必像示例中需要指明是LvenovoComputerBuilder或者DellComputerBuilder
  • OkHttp中沒有指揮者Director 因爲只有唯一的一個構造器,所以有時候我們是可以省略改類型,我們使用的時候也通常會封裝在一個單例類中,並提供一個方法返回構造成功的OkHttpClient對象,也相當於提供了一個Director的方法。

我喜歡將類似與OkHttpClient中的建造者模式稱之爲配置式的建造者模式,因爲建造器的參數都是相當於配置類型的,構造器像是創建了一個配置文件,然後將他傳遞給產品對象,創建對應的產品類對象。還有類似的比如:Retrofit,Glide,AlertDialog等常用的Android框架裏面都使用了類似的建造者模式。
我們通過圖表對比下配置式的構造者模式標準的構造者模式

配置式的構造者模式 標準的構造者模式
建造者的參數屬於配置類型的,基本沒有其他作用 建造者的參數可能會有其他的邏輯,包括但不限於配置類型
建造者具有普適性,參數順序不影響最後對象的生成 建造者可能存在多個,參數順序可能會影響最後的對象生成
建造器一般只有一個 一般具有一個抽象的建造器,多個具體建造器
一般不含有指揮者 一般會有一個建造者
參數一般都具有默認值,可以一個都不配置,直接使用默認值 不同構造器的參數默認值基本不同,一般需要指定

建造者模式的總結

任何模式都有一定的侷限性和適用範圍,建造者模式也一樣,在適合的環境下才更合理,有時候可能只具備其中某些,比如上文中的 配置式的構建者模式

優缺點:

建造者模式的主要優點如下,:

  • 在建造者模式中,客戶端不必知道產品內部組成的細節,將產品本身與產品的創建過程解耦,使得相同的創建過程可以創建不同的產品對象。
  • 每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者,用戶使用不同的具體建造者即可得到不同的產品對象。由於指揮者類針對抽象建造者編程,增加新的具體建造者無須修改原有類庫的代碼,系統擴展方便,符合 “開閉原則”。
  • 可以更加精細地控制產品的創建過程。將複雜產品的創建步驟分解在不同的方法中,使得創建過程更加清晰,也更方便使用程序來控制創建過程。

建造者模式的主要缺點如下:

  • 建造者模式所創建的產品一般具有較多的共同點,其組成部分相似,如果產品之間的差異性很大,例如很多組成部分都不相同,不適合使用建造者模式,因此其使用範圍受到一定的限制。
  • 如果產品的內部變化複雜,可能會導致需要定義很多具體建造者類來實現這種變化,導致系統變得很龐大,增加系統的理解難度和運行成本。

適用場景:

  • 需要生成的產品對象有複雜的內部結構,這些產品對象通常包含多個成員屬性。
  • 需要生成的產品對象的屬性相互依賴,需要指定其生成順序。
  • 對象的創建過程獨立於創建該對象的類。在建造者模式中通過引入了指揮者類,將創建過程封裝在指揮者類中,而不在建造者類和客戶類中。
  • 隔離複雜對象的創建和使用,並使得相同的創建過程可以創建不同的產品。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章