前端的設計模式系列 - 建造者模式

代碼也寫了幾年了,設計模式處於看了忘,忘了看的狀態,最近對設計模式有了點感覺,索性就再學習總結下吧。

大部分講設計模式的文章都是使用的 Java、C++ 這樣的以類爲基礎的靜態類型語言,作爲前端開發者,js 這門基於原型的動態語言,函數成爲了一等公民,在實現一些設計模式上稍顯不同,甚至簡單到不像使用了設計模式,有時候也會產生些困惑。

下面按照「場景」-「設計模式定義」- 「代碼實現」- 「更多場景」-「總」的順序來總結一下,如有不當之處,歡迎交流討論。

場景

如果我們定義了某個函數:

function getPhone(size, type, screen, price=100) {
  ...
}

如果這個函數很穩定那沒什麼問題,但如果經常變動,比如新增參數。

function getPhone(size, type, screen, price=100, discount) {
  ...
}

此時我們如果想繼續使用 price 的默認值,調用的時候還必須顯性的傳 undefined,getPhone(4.3, 'iOS', 'OLED', undefined, 0.8)。

如果再增加一個帶默認值的參數,就會看起來越來越怪。

function getPhone(size, type, screen, price=100, discount, mode='test') {
  ...
}

如果這個函數在很多地方都調用過,改的時候還需要保證修改後其他地方傳參是正常的。

此時可以藉助建造者模式的思想去改造它。

建造者模式

看下 維基百科 給的定義:

The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns.

建造者模式屬於創建型設計模式,也就是爲了生成對象。它將複雜的創建過程從構造函數分離出來,然後就可以在不改變原有構造函數的基礎上,創建各種各樣的對象。

GoF 書中提供的做法就是新創建一個 Builder 類,對象的創建委託給 Builder 類,原始的類不做操作,只負責調用即可。

Director 類在構造函數中持有一個 Builder 實例,然後調用 Builder 類的 buildPart 和 getResult 即可創建對象。未來有新的對象需要創建的話,只需要實現新的 Builder 類即可,無需修改 Director 實例。

原始的建造者模式把對象的創建完全抽離到了 Builder 類中,這可能會導致原始類沒啥用了,也許我們可以不全部抽離,Builder 類只負責接收參數即可。

以下示例來自極客時間的 設計模式之美

public class ResourcePoolConfig {
  private static final int DEFAULT_MAX_TOTAL = 8;
  private static final int DEFAULT_MAX_IDLE = 8;
  private static final int DEFAULT_MIN_IDLE = 0;

  private String name;
  private int maxTotal = DEFAULT_MAX_TOTAL;
  private int maxIdle = DEFAULT_MAX_IDLE;
  private int minIdle = DEFAULT_MIN_IDLE;

  public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
    if (StringUtils.isBlank(name)) {
      throw new IllegalArgumentException("name should not be empty.");
    }
    this.name = name;

    if (maxTotal != null) {
      if (maxTotal <= 0) {
        throw new IllegalArgumentException("maxTotal should be positive.");
      }
      this.maxTotal = maxTotal;
    }

    if (maxIdle != null) {
      if (maxIdle < 0) {
        throw new IllegalArgumentException("maxIdle should not be negative.");
      }
      this.maxIdle = maxIdle;
    }

    if (minIdle != null) {
      if (minIdle < 0) {
        throw new IllegalArgumentException("minIdle should not be negative.");
      }
      this.minIdle = minIdle;
    }
  }
  //...省略getter方法...
}

上邊的 ResourcePoolConfig 類構造函數需要 4 個參數,如果經常變動,未來可能會越來越多,代碼的可讀性和易用性都會變差。因此這裏可以用到建造者模式,但這裏的建造者模式只用來傳遞參數,其他的邏輯還是維持在 ResourcePoolConfig 類中不變。

public class ResourcePoolConfig {
  private String name;
  private int maxTotal;
  private int maxIdle;
  private int minIdle;

  private ResourcePoolConfig(Builder builder) {
    this.name = builder.name;
    this.maxTotal = builder.maxTotal;
    this.maxIdle = builder.maxIdle;
    this.minIdle = builder.minIdle;
  }
  //...省略getter方法...

  //我們將Builder類設計成了ResourcePoolConfig的內部類。
  //我們也可以將Builder類設計成獨立的非內部類ResourcePoolConfigBuilder。
  public static class Builder {
    private static final int DEFAULT_MAX_TOTAL = 8;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;

    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int maxIdle = DEFAULT_MAX_IDLE;
    private int minIdle = DEFAULT_MIN_IDLE;

    public ResourcePoolConfig build() {
      // 校驗邏輯放到這裏來做,包括必填項校驗、依賴關係校驗、約束條件校驗等
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      if (maxIdle > maxTotal) {
        throw new IllegalArgumentException("...");
      }
      if (minIdle > maxTotal || minIdle > maxIdle) {
        throw new IllegalArgumentException("...");
      }

      return new ResourcePoolConfig(this);
    }

    public Builder setName(String name) {
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      this.name = name;
      return this;
    }

    public Builder setMaxTotal(int maxTotal) {
      if (maxTotal <= 0) {
        throw new IllegalArgumentException("...");
      }
      this.maxTotal = maxTotal;
      return this;
    }

    public Builder setMaxIdle(int maxIdle) {
      if (maxIdle < 0) {
        throw new IllegalArgumentException("...");
      }
      this.maxIdle = maxIdle;
      return this;
    }

    public Builder setMinIdle(int minIdle) {
      if (minIdle < 0) {
        throw new IllegalArgumentException("...");
      }
      this.minIdle = minIdle;
      return this;
    }
  }
}

// 這段代碼會拋出IllegalArgumentException,因爲minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .setMaxIdle(10)
        .setMinIdle(12)
        .build();

這樣的話我們可以通過 ResourcePoolConfig.Builder() 來設置參數,將生成的參數對象傳遞給 ResourcePoolConfig 類的構造函數即可。

這裏可以看作是變種的建造者模式,我們不是創建不同的 Builder 類來創建對象,而是給 Builder 類傳遞不同的參數來創建不同的對象。

代碼實現

這裏也只討論變種的建造者模式。

在 js 中,我們同樣可以照貓畫虎的引入一個 Builer 類來接受參數,然後將創建參數對象傳遞給原始類。

但之所以在 Java 中引入新的 Builder 類是因爲 Java 只能通過類來創建對象,但在 js 中我們是可以通過字面量來創建對象的,並且 ES6 還提供了對象的解構語法,會讓我們使用起來更加簡潔。

我們只需要將參數列表聚合爲一個對象,然後通過解構取參數即可。

function getPhone(size, type, screen, price=100, discount) {
  console.log("size", size);
  console.log("type", type);
  console.log("screen", screen);
  console.log("price", price);
  console.log("discount", discount);
}

我們只需要改成:

function getPhone({ size, type='iOS', screen='OLED', price = 100, discount } = {}) {
    console.log("size", size);
    console.log("type", type);
    console.log("screen", screen);
    console.log("price", price);
    console.log("discount", discount);
}

getPhone({ size: 4, discount: 0.1, type: 'android' }); // 只需要傳遞需要的參數

上邊的寫法可以很方便的設置默認值,並且參數的順序也不再重要,未來再擴展的時候也不需要太擔心其他地方調用時候傳參是否會引起問題。

注意一下參數列表中 {...} = {} 後邊的大括號最好寫一下,不然如果用戶調用函數的時候什麼都沒有傳,解構就會直接失敗了。

function getPhone({ size, type='iOS', screen='OLED', price = 100, discount }) {
    console.log("size", size);
    console.log("type", type);
    console.log("screen", screen);
    console.log("price", price);
    console.log("discount", discount);
}

getPhone()

更多場景

通過對象來傳遞參數除了用在函數中以外,設計組件的時候,如果組件的參數會經常變動,並且越來越多,我們不妨引入一個 Object 類型的參數,然後將相關的參數內聚到 Object 中進行傳遞。

總

原始的建造者模式不清楚有沒有實際應用,目前還沒遇到,未來有的話再補充吧。

變種的建造者模式(只傳遞參數)在 js 中也很簡單,直接通過對象傳遞參數即可。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BkN21zibOExFCrwqb7Q9RA