那些你不經意間使用的設計模式(一) - 建立型模式

前端勸退師發表於2019-07-23

前言

前幾天我曾分享過幾張不那麼嚴謹的思維導圖,其中便有關於設計模式的一張:

那些你不經意間使用的設計模式(一) - 建立型模式
在很長的一段時間裡,我只能記住某幾種設計模式,並沒有很好的應用。

索性我們就以該圖為大綱,講講那些我們不經意間使用的設計模式 --- 建立型。

1. 三種工廠模式:Factory Pattern

那些你不經意間使用的設計模式(一) - 建立型模式
通常來說三種設計模式為:

  • 簡單工廠模式(Simple Factory)
  • 工廠方法模式(Factory method)
  • 抽象工廠模式(Abstract factory)

其核心就是:

工廠起到的作用就是隱藏了建立例項的複雜度,只需要提供一個介面,簡單清晰。 --- 摘自《前端面試之道》

而區別則是:

  • 簡單工廠模式,用來建立某一種產品物件的例項,用來建立單一物件。
  • 工廠方法模式,將建立例項推遲到子類中進行。
  • 抽象工廠模式,是對類的工廠抽象用來建立產品類簇,不負責建立某一類產品的例項。

其實從你會用jQuery開始,就已經在用工廠模式了:

JavaScript設計模式與實踐--工廠模式

1. jQuery$(selector)

jQuery$('div')new $('div')哪個好用?很顯然直接$()最方便 ,這是因為$()已經是一個工廠方法了。

class jQuery {
    constructor(selector) {
        super(selector)
    }
    //  ....
}

window.$ = function(selector) {
    return new jQuery(selector)
}
複製程式碼

2. ReactcreateElement()

React.createElement()方法就是一個工廠方法

  React.createElement('h1', null, 'Hello World!'),
複製程式碼

那些你不經意間使用的設計模式(一) - 建立型模式

3. Vue的非同步元件

通過promise的方式resolve出來一個元件

那些你不經意間使用的設計模式(一) - 建立型模式
對應原始碼:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
    // ...邏輯處理
    // async component
    let asyncFactory
    const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
    )
}

複製程式碼

2. 單例模式:Singleton Pattern

那些你不經意間使用的設計模式(一) - 建立型模式
單例模式是最簡單的設計模式之一。 用一句大白話來解釋就是:

例項一次後處處可用

單例模式的要點有三個:

  • 某個類只能有一個例項;
  • 它必須自行建立這個例項;
  • 它必須自行向整個系統提供這個例項。

從具體實現角度來說,就是以下三點:

  • 單例模式的類只提供私有的建構函式
  • 類定義中含有一個該類的靜態私有物件
  • 該類提供了一個靜態的公有的函式用於建立或獲取它本身的靜態私有物件。

同樣的,它也是我們最早接觸的一種設計模式:

1. 引用第三方庫

多次引用只會使用一個庫引用,如 jQuerylodashmoment等。

2. Vuex / Redux

全域性狀態管理store

VuexRedux資料儲存在單一store中,Mobx將資料儲存在分散的多個store

const store = createStore(reducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
複製程式碼

3. Vue中第三方外掛的安裝

首當其衝的就是Vuex安裝:

let Vue // bind on install

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    // 如果發現 Vue 有值,就不重新建立例項了
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
複製程式碼

其它也類似,感興趣的可以去GitHub搜尋export function install (Vue)

那些你不經意間使用的設計模式(一) - 建立型模式

4. 簡單實現一個單例模式:

class SingletonClass {
  constructor() {
    if (!SingletonClass.instance) {
      SingletonClass.instance = this;
    }

    return SingletonClass.instance;
  }
  // other things
}

const instance = new SingletonClass();
Object.freeze(instance);

export default instance;
複製程式碼

3. 建造者模式:Builder Pattern

、建造者模式主要用於 “分步驟構建一個複雜的物件”

那些你不經意間使用的設計模式(一) - 建立型模式

這在其中“分步驟”是一個穩定的演算法,而複雜物件的各個部分則經常變化。

一句話:指揮者分配任務,建造者進行開發,各執其責,穩定在一個大的流程裡面去。

建造者模式概念擬物化解讀

那些你不經意間使用的設計模式(一) - 建立型模式
一位女士要建造一座別墅,需要找來一位包工頭,包工頭再將具體的任務分配給工人做,做完之後再給女士使用。

1. jQuery中的建造者

jQuery中建造者模式體現在:

$( "<div class= "foo">bar</div>" );

$( "<p id="test">foo <em>bar</em></p>").appendTo("body" );

var newParagraph = $( "<p />" ).text( "Hello world" );

$( "<input />" )
      .attr({ "type": "text", "id":"sample"});
      .appendTo("#container");
複製程式碼

下面是jQuery原始碼內部jQuery.prototype方法的一個片段,它將從傳遞給jQuery()選擇器的標記構建jQuery物件。

無論是否 document.createElement用於建立新元素,對元素(找到或建立)的引用都會注入到返回的物件中,因此.attr()可以在其後立即使用其他方法。

  // HANDLE: $(html) -> $(array)
    if ( match[1] ) {
      context = context instanceof jQuery ? context[0] : context;
      doc = ( context ? context.ownerDocument || context : document );
      
      //如果傳入的是單個字串,並且是單個標記
      //只需執行createElement並跳過其餘部分
    
      ret = rsingleTag.exec( selector );

      if ( ret ) {
        if ( jQuery.isPlainObject( context ) ) {
          selector = [ document.createElement( ret[1] ) ];
          jQuery.fn.attr.call( selector, context, true );

        } else {
          selector = [ doc.createElement( ret[1] ) ];
        }

      } else {
        ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
        selector = ( ret.cacheable ? jQuery.clone(ret.fragment) 
        : ret.fragment ).childNodes;
      }

      return jQuery.merge( this, selector );
複製程式碼

1. 建造者模式的理想實現

本質上,建造者模式的目標是減少建構函式所用的引數數量,並提供向物件新增靈活的行為方法。

// 使用建造者模式之前
const person1 = new Person('Peter', 26, true, 40074986, 4, 2);

// 使用建造者模式之後
const person1 = new Person();
person1
  .name('Peter')
  .age(26)
  .member(true)
  .phone(40074986)
  .children(4)
  .cars(2);
複製程式碼

2. ES6中的建造者模式

我們來假設一個商品錄入系統的業務場景,有四個必填資訊,分別是:名稱,價格,分類。 該build方法將返回最終的JavaScript物件。

/書籍建造者類
class ProductBuilder {
  constructor() {
    this.name = '';
    this.price = 0;
    this.category = '';
  }

  withName(name) {
    this.name = name
    return this
  }

  withPrice(price) {
    this.price = price
    return this
  }

  withCategory(category) {
    this.category = category
    return this
  }

  build() {
    return {
      name: this.name,
      price: this.price,
      category: this.category,
    }
  }
}

console.log(
  new ProductBuilder()
    .withName('《哈利·波特》')
    .withCategory('book')
    .withPrice('29.9')
    .build()
複製程式碼

雖然只有三個屬性,我們的構建器已經相當大,並且需要很多withers

構建器的大小隨欄位數量線性增長。我們有太多的withxxxx方法。我們其實可以自動建立這類withxxxx方法以簡化程式碼。

3. 建造者模式簡化

class ProductBuilder {
  constructor() {
    this.name = ''
    this.price = ''
    this.category = 'other'

    // 為每個屬性生成`wither`
    Object.keys(this).forEach(key => {
      const witherName = `with${key.substring(0, 1).toUpperCase()}${key.substring(1)}`
      this[witherName] = value => {
        this[key] = value
        return this
      }
    })
  }

  build() {
    // 獲取此生成器的所有非函式屬性的陣列
    const keysNoWithers = Object.keys(this).filter(key => typeof this[key] !== 'function')

    return keysNoWithers.reduce((returnValue, key) => {
      return {
        ...returnValue,
        [key]: this[key],
      }
    }, {})
  }
}

console.log(
  new ProductBuilder()
    .withName('《哈利波特》')
    .withCategory('book')
    .build()
)
複製程式碼

我們將所有的建造方法withxxxx在constructor呼叫時自動被建立,這裡我們使用了一些ES6的新語法:Object.keys獲取物件屬性陣列,...的合併物件的語法

最終我們得到了一種宣告式(易於理解)的方法,且可以動態新增屬性的建造者模式。

5. 提取公用部分,實現多個建造者

當你有許多建造者時,我們可以輕鬆地將其廣義部分提取到一個通用的父類中,從而可以非常輕鬆地建立新的建造者。

class BaseBuilder {
  init() {
    Object.keys(this).forEach((key) => {
      const witherName = `with${key.substring(0,1).toUpperCase()}${key.substring(1)}`;
      this[witherName] = (value) => {
        this[key] = value;
        return this;
      };
    });
  }

  build() {
    const keysNoWithers = Object.keys(this).filter((key) => (
      typeof this[key] !== 'function'
    ));

    return keysNoWithers.reduce((returnValue, key) => {
      return {
        ...returnValue,
        [key]: this[key]
      };
    }, {});
  }
}
複製程式碼

然後就可以建立多個建造者了:

class ProductBuilder extends BaseBuilder {
  constructor() {
    super();

    this.name = '《前端勸退祕訣》';
    this.price = 9.99;
    this.category = 'other';

    super.init();
  }
}
複製程式碼

可以看出,建造者模式的使用有且只適合建立極為複雜的物件。在前端的實際業務中,在沒有這類極為複雜的物件的建立時,還是應該直接使用物件字面或工廠模式等方式建立物件。

4. 原型模式

prototype...再講會被砍死吧。

5. 下一篇:結構型設計模式

原本打算又雙叒憋它個一萬多字,把所有設計模式寫個遍。

但是覺得吧,這樣閱讀體驗其實並不好(主要還是懶,想慢慢寫。)

那些你不經意間使用的設計模式(一) - 建立型模式

噢對了,現在還有靠譜內推的可以聯絡我

那些你不經意間使用的設計模式(一) - 建立型模式

  • 微信:huab119
  • 郵箱:454274033@qq.com

參考文章

作者掘金文章總集

需要轉載到公眾號的喊我加下白名單就行了。

公眾號

那些你不經意間使用的設計模式(一) - 建立型模式

那些你不經意間使用的設計模式(一) - 建立型模式

相關文章