設計模式ts實戰系列(上)

本文提要

本文是一系列 ts 的設計模式實戰總結,並非單純的介紹設計模式,而是從工做中的例子出發,由於這樣才能讓人體會到設計模式離咱們很近。全篇每一個設計模式都是從,概念、一句話歸納、優缺點、實戰幾個方面來說。前端

設計模式概念

設計模式(Design pattern)表明了最佳的實踐,一般被有經驗的面向對象的軟件開發人員所採用。設計模式是軟件開發人員在軟件開發過程當中面臨的通常問題的解決方案。這些解決方案是衆多軟件開發人員通過至關長的一段時間的試驗和錯誤總結出來的。vue

若是咱們把代碼編程比做是戰爭的話,那麼設計模式就是兵法。不會兵法確定打不過會兵法的,即便能打過也要付出更多的代價。react

設計模式,是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結。git

目的:使用設計模式是爲了可重用代碼,讓代碼更容易被他人理解、保證代碼的可靠性。github

設計模式原則

  • 開閉原則 對擴展開放,對修改關閉。保證程序的擴展性好,易於維護和升級
  • 單一職責原則 對一個類而言,應該僅有一個引發它變化的緣由
  • 里氏代換原則 子類能夠擴展父類的功能,可是不能改變父類原有的功能
  • 依賴倒置原則 抽象不依賴細節,細節應該依賴抽象。
  • 接口隔離原則 創建單一接口,代替龐大臃腫的接口。
  • 最小知識原則 一個對象應該對其餘對象有最少的瞭解。類間解耦,弱耦合。

單例模式

特色

  1. 單例類只能有一個實例。
  2. 單例類必須本身建立本身的惟一實例。
  3. 單例類必須給全部其餘對象提供這一實例。

一句話歸納

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點算法

單例模式場景

  • 電腦上的文件,不管在任何地方修改文件,它都只有一份
  • 古代的皇帝,也只能有一個
  • vue-router 跟 vuex 的 install 也是單例
  • 其實 js 中的字面量對象就是一個自然單例

優缺點

優勢vue-router

  1. 減小內存開支 單例模式在內存中只有一個實例,減小內存開支,特別是一個對象須要頻繁地建立銷燬時,並且建立或銷燬時性能又沒法優化,單例模式就很是明顯了
  2. 減小性能開銷 因爲單例模式只生成一個實例,因此,減小系統的性能開銷,當一個對象產生須要比較多的資源時,如讀取配置,產生其餘依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後永久駐留內存的方式來解決。
  3. 避免對資源的多重佔用 例如一個寫文件操做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做
  4. 設置全局的訪問點 優化和共享資源訪問,例如,能夠設計一個單例類,負責全部數據表的映射處理。

缺點vuex

  1. 單例很難擴展
  2. 與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。

經常使用的單例模式有兩種

  • 懶漢模式
// 懶漢式,只有在調用 getInstance 的時候纔會實例化 Singleton
class Singleton {
  static instance = null;
  // 獲取實例方法
  static getInstance() {
    return this.instance || (this.instance = new Singleton());
  }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 == instance2); // true
複製代碼
  • 餓漢模式
// 餓漢式,在類初始化的時候就已經建立好了實例
class Singleton {
  static instance = new Singleton();
  // 獲取實例方法
  static getInstance() {
    return this.instance;
  }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 == instance2); // true
複製代碼

工廠模式

目的

定義一個建立對象的接口,讓其子類本身決定實例化哪個工廠類,工廠模式使其建立過程延遲到子類進行編程

使用場景

咱們明確地計劃不一樣條件下建立不一樣實例時小程序

優缺點

優勢

  1. 隱藏了對象建立的細節,將產品的實例化過程放到了工廠中實現。
  2. 客戶端基本不用關心使用的是哪一個產品,只須要知道用工廠的那個方法(或傳入什麼參數)就好了.
  3. 方便添加新的產品子類,每次只須要修改工廠類傳遞的類型值就好了。
  4. 遵循了依賴倒轉原則。

缺點

  1. 適用於產品子類型差很少, 使用的方法名都相同的狀況.
  2. 每添加一個產品子類,都必須在工廠類中添加一個判斷分支(或一個方法),這違背了OCP(開閉原則)。

實現

好比我要有一個 Animal 工廠,這個工廠要生產動物。那麼我要定義動物都有 Feature特徵必需要有 color 顏色跟 bark 叫聲。

1. 接口實現

// 定義工廠須要的動物特徵
interface Feature {
  color: string;
  bark(): void;
}

// 定義動物類型名字
type name = 'cat' | 'dog'

// 子類必需要實現 Feature 接口的方法
// 這樣咱們就能夠建立白色叫聲喵喵喵的貓了
class Cat implements Feature {
  color = "白色";
  bark() {
    console.log(`${this.color} 喵喵喵`);
  }
}
// 建立 Dog 類
class Dog implements Feature {
  color = "黑色";
  bark() {
    console.log(`${this.color} 汪汪汪`);
  }
}
// 這就是一個動物工廠
class Animal {
  createAnimal(type: name) {
    switch (type) {
      case 'cat':
        return new Cat();
      case 'dog':
        return new Dog();
    }
  }
}

const animal = new Animal();
const cat = animal.createAnimal('cat');
const dog = animal.createAnimal('dog');

cat.bark()
dog.bark()
複製代碼

2. 抽象類實現

abstract class Feature {
  abstract color: string;
  abstract bark(): void;
}

// 枚舉可使用的動物類型
enum animalType {
  'cat',
  'dog'
}

// 子類繼承抽象類 Feature
// 這樣咱們就能夠建立白色叫聲喵喵喵的貓了
class Cat extends Feature {
  color = "白色";
  bark() {
    console.log(`${this.color} 喵喵喵`);
  }
}
// 建立 Dog 類
class Dog extends Feature {
  color = "黑色";
  bark() {
    console.log(`${this.color} 汪汪汪`);
  }
}
// 這就是一個動物工廠
class Animal {
  createAnimal(type: animalType) {
    switch (type) {
      case animalType.dog:
        return new Cat();
      case animalType.dog:
        return new Dog();
    }
  }
}

const animal = new Animal();
const cat = animal.createAnimal(animalType.cat);
const dog = animal.createAnimal(animalType.dog);

cat.bark()
dog.bark()
複製代碼

享元模式

模式定義

享元模式,運用共享技術,有效地支持大量的細粒度的對象,以免對象之間擁有相同內容而形成多餘的性能開銷。

享元(flyweight)模式的主要做用:性能優化,當系統建立過多類似的對象而致使內存佔用太高,能夠採用這種設計模式進行優化。

享元模式將對象的屬性區分爲內部狀態與外部狀態,內部狀態在建立的時候賦值,外部狀態在實際須要用到的時候進行動態賦值

對於內部狀態和外部狀態的區分,有幾點:

  1. 內部狀態存儲於對象內部
  2. 內部狀態能夠被一些對象共享
  3. 內部狀態獨立於具體場景,一般不會改變
  4. 外部狀態取決於具體場景,並根據場景變化,外部狀態不能被共享。

實戰

咱們要建立 100 個大小相同顏色不一樣的 div。

不使用享元模式的作法是:

  1. 建立一個建立 div 的類,CreateDiv。
  2. new CreateDiv() 建立 div
  3. 咱們須要 new 100 次。這樣就形成了很大的空間浪費。
interface Div {
  width: number;
  height: number;
  color: string;
}
const divStore: Div[] = [];

class CreateDiv {
  public width = 100;
  public height = 100;
  public color = this.randomColor()
  // 隨機顏色
  private randomColor () {
    const color = ['red', 'green', 'blue', 'white', 'black'];
    return color[Math.floor(Math.random() * color.length)];
  }
}

let count = 100;
while (count--) {
  const innerDiv = new CreateDiv();
  divStore.push(innerDiv);
}

const sizeof = require('object-sizeof')

console.log(sizeof(divStore)) // 5688
複製代碼

享元模式來作

// 將 div 屬性設置成內部跟外部兩部分
interface Div {
  outer: {
    width: number;
    height: number;
  };
  innter: {
    color: string;
  };
}
// 用來儲存 Div
const divStore: Div[] = [];
// 建立外部 div 類
class CreateOuterDiv {
  width: number = 100;
  height: number = 100;
}
class CreateInnerDiv {
  public color = this.randomColor()
  // 隨機顏色
  private randomColor () {
    const color = ['red', 'green', 'blue', 'white', 'black'];
    return color[Math.floor(Math.random() * color.length)];
  }
}
// 建立外部 div
const outerDiv = new CreateOuterDiv();
let innerDiv: number;
let count = 100;

while (count--) {
  // 建立內部 div
  innerDiv = new CreateInnerDiv();
  divStore.push({
    outer: outerDiv,
    innter: innerDiv
  });
}

const sizeof = require('object-sizeof')
// 由於這個方法會把引用的對象也所有算一遍,因此咱們拆開來算

// 驗證:100 * (innerDiv + outerDiv)= 5400 與上面算的 5688 很接近,能夠認爲這個方法是準確的
console.log(100 * (sizeof(innerDiv) + sizeof(outerDiv))) // 5400
// 100 * innerDiv + outerDiv = 1638
console.log(100 * sizeof(innerDiv) + sizeof(outerDiv)) // 1638
複製代碼

從上面的計算結果來看減小了很大的內存,由於 divStore 數組對象中 outerDiv 其實只有一個,都是它的引用而已。咱們的內存佔用是 100 * innerDiv + outerDiv,而不使用享元模式的空間是 100 * (innerDiv + outerDiv)

策略模式

定義

定義一系列的算法, 把它們一個個封裝起來, 而且使它們可相互替換。

優缺點

優勢

  1. 算法能夠自由切換。
  2. 避免使用多重條件判斷。
  3. 擴展性好,符合開閉原則。

缺點

  1. 策略類會增多。
  2. 全部策略類都須要對外暴露。

實戰

在vue中有一個合併選項策略 optionMergeStrategies,它的功能就是把選項添加一些策略,能夠達到咱們對選項數據操做的目的

官方例子,將選項 _my_option 添加策略,讓它的值加一

Vue.config.optionMergeStrategies._my_option = function (parent, child, vm) {
  return child + 1
}

const Profile = Vue.extend({
  _my_option: 1
})

// Profile.options._my_option = 2
複製代碼

咱們來簡單實現一下這個合併選項策略

// 策略模式 store
const optionMergeStrategies: { [prop: string]: any } = {};

// 給 _my_option 添加策略
optionMergeStrategies._my_option = function(value) {
  return value + 1
}

// 聲明 data
const data = {
  // 添加策略
  _my_option: 1,
  // 未添加策略
  option: 1
};

// 響應式
function reactive (data) {
  const hander = {
    get(target, key, value) {
      const v = Reflect.get(target, key, value);
      // 此屬性存在策略
      if (typeof optionMergeStrategies[key] === 'function') {
        return optionMergeStrategies[key](v)
      }
      return v
    }
  };
  return new Proxy(data, hander);
}

const proxy = reactive(data);
// 測試是否添加了響應
proxy._my_option = 10
proxy.option = 10

console.log(proxy._my_option, proxy.option); // 11 10
複製代碼

這樣你就能夠作更多的事情了,好比驗證手機號,郵箱等等,不再用寫不少的 if else 了,並且你也能夠隨時更換策略。符合了設計模式的開閉原則。

發佈訂閱者模式

定義

當對象間存在一對多關係時,則使用觀察者模式(Observer Pattern)。好比,當一個對象被修改時,則會自動通知它的依賴對象。觀察者模式屬於行爲型模式。

優缺點

優勢

  1. 觀察者和被觀察者是抽象耦合的。
  2. 創建一套觸發機制。

缺點

  1. 若是一個被觀察者對象有不少的直接和間接的觀察者的話,將全部的觀察者都通知到會花費不少時間。
  2. 若是在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能致使系統崩潰。
  3. 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。

實戰

好比公衆號,有多我的訂閱,天天定時發送公衆號文章

  1. 創建一個 Persen 類,用於建立人物(觀察者/訂閱着)
  2. 創建 Subject 類,用於創建與觀察者之間的關係(被注入到觀察者的依賴)
  3. 修改狀態觸發更新
// 公衆號訂閱者
abstract class Persen {
  abstract update(): void;
  protected subject: Subject;
}
// 狀態
type state = 'await' | 'publish'
// 依賴
class Subject {
  private _state: state = 'await'
  // 依賴集合
  subs: Persen[] = [];
  // 防止頻繁設置狀態
  lock = false
  // 設置狀態,若是是發佈狀態的話,就發佈文章
  set state(state: state) {
    // 鎖上以後就不能設置狀態了,只有鎖解開後才能夠設置狀態
    if (this.lock || (this._state = state) === 'await') return;
    this.lock = true;
    Promise.resolve().then(() => {
      this.notify();
      this.lock = false;
    });
  }
  // 得到當前狀態
  get state(): state {
    return this._state
  }
  // 添加訂閱
  attach(persen: Persen) {
    this.subs.push(persen)
  }
  // 通知更新
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}
// 建立一個 Tom
class Tom extends Persen {
  constructor(subject: Subject) {
    super();
    subject.attach(this)
  }
  update() {
    console.log('通知到了 Tom');
  }
}
// 建立一個 Jick
class Jick extends Persen {
  constructor(subject: Subject) {
    super();
    subject.attach(this)
  }
  update() {
    console.log('通知到了 Jick');
  }
}
// 實例化依賴
const subject = new Subject()

// Tom Jick 訂閱公衆號
new Tom(subject)
new Jick(subject)

// 由於設置了 lock 因此在一次 event loop 只會執行一次 
subject.state = 'publish'
subject.state = 'await'
console.log(subject.state) // publish
subject.state = 'publish'

setTimeout(() => {
  subject.state = 'publish'
}, 1000)

// 通知到了 Tom
// 通知到了 Jick
// 一秒後...
// 通知到了 Tom
// 通知到了 Jick
複製代碼

裝飾器模式

定義

裝飾器模式(Decorator Pattern)容許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是做爲現有的類的一個包裝。

一句話歸納

這種模式建立了一個裝飾類,用來包裝原有的類,並在保持類方法簽名完整性的前提下,提供了額外的功能。

優缺點

優勢

  1. 更好的可讀性
  2. 裝飾類和被裝飾類能夠獨立發展,不會相互耦合
  3. 裝飾模式是繼承的一個替代模式
  4. 裝飾模式能夠動態擴展一個實現類的功能。

缺點

  1. 多層裝飾比較複雜。

實戰

好比咱們要點擊一個按鈕,可是這個按鈕點擊時咱們想給他加上埋點並作一些登錄的邏輯

我這裏使用了 es7 的語法糖,固然不用語法糖也能夠作,可是我以爲用的話更簡潔一些

// Button 類,內部有一個 click 方法
// 對click方法作了兩個修飾
// 一個是添加埋點,一個是登錄
class Button {
  @BuridDecorator
  @LoginDecorator
  click() {
    console.log('點擊 dom')
  }
}
// 登錄邏輯的裝飾器
function LoginDecorator(target, name, descriptor) {
  const oriFun = target[name]
  descriptor.value = async function() {
    const code = await Login();
    if (code === 0) {
      console.log('登錄成功')
      oriFun.call(this, ...arguments)
    }
  }
}
// 設置埋點的裝飾器
function BuridDecorator(target, name, descriptor) {
  console.log(`${name} 方法添加了一個埋點`)
}
// 登錄邏輯
async function Login () {
  return new Promise((resolve, reject)=> {
    setTimeout(() => {
      resolve(0)
    }, 1000)
  })
}
// 點擊按鈕
const btn = new Button()
btn.click();

// click 方法添加了一個埋點
// 登錄成功
// 點擊 dom
複製代碼

適配器模式

一句話歸納

適配器模式(Adapter Pattern)是做爲兩個不兼容的接口之間的橋樑。這種類型的設計模式屬於結構型模式,它結合了兩個獨立接口的功能。

優缺點

優勢

  1. 可讓任何兩個沒有關聯的類一塊兒運行。
  2. 提升了類的複用。
  3. 增長了類的透明度。
  4. 靈活性好。

缺點

  1. 過多地使用適配器,會讓系統很是零亂,不易總體進行把握。

實戰

舉一個例子,你要寫一個頁面兼容各個端的小程序,那麼你就須要根據環境調用不一樣小程序的 sdk 方法。好比在支付寶中有一個 zhifubaoShare 的分享方法,在微信中有一個 weixinShare 的分享方法。(固然一個sdk還有不少方法,咱們只拿分享來舉例子)可是咱們在工做中其實只但願調用一個 share 方法就能實現不一樣端的分享。下面咱們用適配器模式來作一個 Adapter 適配器。

// =============== 定義接口與類型 ==============================
// 支付寶接口
interface ZhifubaoInerface {
  zhifubaoShare(): void;
}
// 微信接口
interface WeixinInterface {
  weixinShare(): void;
}
// adapter 接口
interface AdapterInterface {
  share(): void;
}
// 合併全部 sdk 類型
interface MergeSdk extends ZhifubaoInerface, WeixinInterface {}
// 支持的平臺類型
type platform = 'weixin' | 'zhifubao';


// =============== 代碼邏輯實現 ==============================
// 微信 sdk 類實現
class WeixinSdk implements WeixinInterface {
  weixinShare() {
    console.log('微信分享');
  }
}
// 支付寶 sdk 類實現
class ZhifubaoSdk implements ZhifubaoInerface {
  zhifubaoShare() {
    console.log('支付寶分享');
  }
}
// adapter 類實現
class Adapter implements AdapterInterface {
  constructor() {
    this.sdk = this.getPlatfromSdk();
  }
  // 掛載 sdk
  private sdk: MergeSdk;
  // 根據 ua 獲取到平臺
  private getPlatform(): platform {
    // 默認寫了 weixin
    return 'weixin';
  }
  // 將全部 sdk 方法放進一個 map 裏
  private getPlatfromSdk() {
    const map = {
      weixin: WeixinSdk,
      zhifubao: ZhifubaoSdk
    };
    const platform = this.getPlatform();
    return new map[platform]() as MergeSdk;
  }
  // 分享功能
  // 實際項目中還有參數的問題,這裏爲了代碼的簡潔就不寫了
  public share() {
    const platform = this.getPlatform();

    switch (platform) {
      case 'weixin':
        this.sdk.weixinShare();
        break;
      case 'zhifubao':
        this.sdk.zhifubaoShare();
        break;
      default:
        console.log('此方法不存在');
    }
  }
}

const adapter = new Adapter();
// 由於咱們默認設置了 weixin 平臺
adapter.share(); // 微信分享
複製代碼

最後有兩件小事

  1. 有想入羣的學習前端進階的加我微信 luoxue2479 回覆加羣便可
  2. 有錯誤的話歡迎在留言區指出,一塊兒討論,也能夠加我微信
  3. 天天在羣裏會有專題討論 github.com/luoxue-vict…
  4. 鄙人公衆號【前端技匠】,一塊兒來學習吧。
相關文章
相關標籤/搜索