“淺嘗”JavaScript設計模式

什麼是設計模式?設計模式:根據不同場景創建不同類型的對象的套路被稱爲設計模式。

使用設計模式的主要原因?①可維護性:設計模式有助於降低模塊間的耦合程度,這使對代碼進行重構和換用不同的模塊變得更容易,也使得程序員在大型團隊中的工作以及與其他程序員的合作變得更容易。
②溝通:設計模式爲處理不同類型的對象提供了一套通用的術語,程序員可以更簡明地描述自己的系統的工作方式,你不用進行冗長的說明,往往一句話,我是用了什麼設計模式,每個模式有自己的名稱,這意味着你可以在較高層面上進行討論,而不必涉足過多的細節
③性能:某些模式是起優化作用的模式,可以大幅度提高程序的運行速度,並減少需要傳送到客戶端的代碼量

設計模式實現設計模式比較容,懂得應該在什麼時候使用什麼模式比較困難,未搞懂設計模式的用途就盲目套用,是一種不安全的做法,你應該保證所選用的模式就是最恰當的那種,並且不要過度犧牲性能。


一、單例模式

確保單體對象只存在一個實例。

業務場景:當我們使用node啓動一個服務連接數據庫的時候我們一般會創建一個連接數據庫的實例(這個實例就是單例)。每個請求對於數據的請求都是通過這個單例的,不會爲沒個請求去創建單獨的實例,一個單例便於統一管理。

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
var Single = (function () {
  var instance
  var createSingle = function (name) {
    if (instance) {
      return instance
    }
    this.name = name
    instance = this
    return instance
  }
  return createSingle
})();
 
var a = new Single('123')
var b = new Single('456')
console.log(a === b) // true


二、工廠模式

工廠模式使用一個方法來決定究竟要實例化哪個具體的類。
  • 簡單工廠模式

使用一個總的工廠來完成對於所有類的分發。情景再現:一個寵物店裏面有着許多寵物,客人可以通過向寵物店傳遞消息 於是第二天我們就可以到寵物店獲取一隻貓
①工廠(寵物店)    ②傳參(傳遞消息) ③實例化對應類(貓)

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Cat{
   constructor() {
       this.name = '貓'
   }
}
class Dog{
   constructor() {
       this.name = '狗'
   }
}
class Factory {
    constructor(role){
      return this.switchRole(role)
    }
    switchRole(role){
      switch(role){
        case '貓':
            return new Cat()
        case '狗':
            return new Dog()
        default:
            return {}
      }
    }
}
var dog = new Factory('狗') // {name:'貓'}
var cat = new Factory('貓') // {name:'狗'}


簡單的工廠模式我們已經實現了,這時候我們需要又要發散我們的小腦袋去好好揣摩這個模式,我們發現如果每次寵物店又有了新的寵物可以出售,例如今天寵物店引進了烏龜、寵物豬,那我們不僅要先實現相對應的類,還要在Factory中的switchRole()補充條件。如果創建實例的方法的邏輯發生變化,工廠的類就會被多次修改

  • 複雜工廠模式

既然簡單工廠模式,不能滿足我們全部的業務需求,那就只能進化變身了。《javascript設計模式》 給了定義:真正的工廠模式與簡單工廠模式區別在於,它不是另外使用一個類或對象創建實例,而是使用子類工廠是一個將其成員對象的實例推送到子類中進行的類 也就是我們在定義我們看到真正的工廠模式,是提供一個工廠的父類抽象類,對於根據傳參實現實例化的過程是放在子類中實現。

再思考一個問題:貓、狗、烏龜、寵物豬、這些類是否可以再進行細分出現了加菲貓、波斯貓、柴犬、阿拉斯加等子類。在購買寵物的時候是否需要特別的條件? 上述工廠的子類可以解決這個問題:出現了專賣店專門賣貓的,賣狗的,賣寵物豬的,這些專賣店(工廠子類)各自維護自己的類,當你需要新的專賣店你可以重新實現一個工廠子類

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Cat{
   constructor() {
       this.name = '貓'
   }
}
class Garfield {
   constructor() {
       this.name = '加菲貓'
   }
}
class Persian {
   constructor() {
       this.name = '波斯貓'
   }
}
class Dog{
   constructor() {
       this.name = '狗'
   }
}
// 定義成爲抽象類,工廠的父類,不接受任何修改
class Factory {
    constructor(role){
      return this.createModule(role)
    }
    createModule(role){
        return new Error('我是抽象類不要改我,也不要是實例化我')
    }
}
// 貓的專賣工廠,需要重寫父類那裏繼承來的返回實例的方法。購買貓的邏輯可以放在找個類中實現。
class CatFactory extends Factory{
    constructor(role){
      super(role)
    }
    // 重寫createFactory的方法
    createModule(role){
        switch(role){
            case '加菲貓':
                return new Garfield()
            case '波斯貓':
                return new Persian()
            default:
                return {}
          }
    }
}
.... 狗、寵物豬、烏龜的都可以重新繼承父類Factory。
var catFac = new CatFactory('波斯貓')
console.log(catFac)


總結:複雜工廠模式將原有的簡單工廠模式下的工廠類變爲抽象類根據輸出實例的不同來構建不同的工廠子類。這樣既不會修改到工廠抽象類,符合設計原則,又提供了可拓展性。
三、橋接模式

將抽象與其實現隔離開來,以便二者獨立變化。

大家看到上面的那句話會覺得有點摸不清頭腦看一下下面的代碼:

[JavaScript] 純文本查看 複製代碼
1
2
3
4
5
6
7
8
//一個事件的監聽,點擊元素獲得id,根據獲得的id我們發送請求查詢對應id的貓
element.addEventListener('click',getCatById)
var getCatById = function(e){
   var id = this.id
   asyncRequst('Get',`cat.url?id=${id}`,function(resp){
       console.log('我已經獲取了信息')
   })
}


大家看一下getCatById這個api函數我們可以理解爲抽象 而點擊這個過程是實現的效果,但是我們發現getCatById與實現邏輯並沒有完全分割,getCatById是一個只能工作在瀏覽器中的API,根據事件監聽器函數的工作機制,事件對象自然會被作爲第一個參數傳遞給這個函數,在本例中並沒有使用,但是我們看到了var id = this.id,如果你要對這個API做單元測試,或者命令環境中執行,那就只能祝你好運了,任何一個API都不應該把它與任何特定環境攪在一起

[JavaScript] 純文本查看 複製代碼
1
2
3
4
5
6
// 改寫getCatById 將抽象與現實完全隔離,抽象完全依賴傳參,同時我們在別的地方也可以引用,不受制與業務
var getCatById = function(id,callback){
   asyncRequst('Get',`cat.url?id=${id}`,function(resp){
       console.log('我已經獲取了信息')
   })
}


這個時候我們發現了現在抽象出來的getCatById已經不能直接作爲事件函數的回調了,這時候我們要隆重的請出我們橋接模式 此處應該有撒花。

[JavaScript] 純文本查看 複製代碼
1
2
3
4
5
6
element.addEventListener('click',getCatByIdBridge)
var getCatByIdBridge(e){ // getCatByIdBridge 橋接元素
    getCatById(this.id,function(cat){
        console.log('request cat')
    })
}


我們可以看到getCatByIdBridge這個就是橋接模式的產物。溝通抽象與現實的橋樑。有了這層橋接,getCatById這個API的使用範圍就大大的拓寬了,沒有與事件對象捆綁在了一起。
總結:在實現API的時候,橋接模式非常有用,我們用這個模式來弱化API與使用他的類和對象之間的耦合,這種模式對於js中常見的事件驅動有大的裨益。
四、策略模式

定義一系列的規則,根據環境的不同我們執行不同的規則,來避免大量重複的工作。

策略模式根據上述的說法可以隱隱的猜到了,要實行策略模式我們需要兩個類:首先我們需要一個定義了一系列規則的策略類這個是整個策略模式的基石。之後有一個暴露對外方法的環境類通過傳遞不同的參數給環境類,環境類從策類中選取不同的方法執行,最終返回結果
業務場景:作爲一個前端難免跑不了一個表單驗證。當你不使用庫的時候難免需要手寫一些正則校驗、判空、判類型等方式。這個時候你的代碼就是如下的:

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// vue下的表單校驗
checkForm () {
  if (this.form.realName === '') {
    Toast.fail('真實姓名不能爲空')
    return false
  }
  if (!/^[\u4e00-\u9fa5]{0,}$/.test(this.form.realName)) {
    Toast.fail('請輸入中文')
    return false
  }
  if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(this.form.idCardNum)) {
    Toast.fail('請輸入正確的身份證格式')
    return false
  }
  return true
},


注意:這樣寫是沒有問題的,但是我們仔細看這個代碼會發現,這個代碼幾乎不可複用,即使在其他的地方我們需要對字段判定中文,我們就需要重新再用正則判定,其次當一個表單內部需要校驗字段很多,那麼上述方法將會過於冗餘
修改思路:將校驗的具體的實現我們放入策略類,這樣對於可重複使用檢驗規則我們使用一個單個方法統一維護,之後將暴露環境類給各個業務場景使用,傳入所需的方法名、檢測值然後調用策略返回結果。

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 策略類爲表單校驗正則及及自定義的規則
var rules = {
  // 是否中文
  isChinese: function (value) {
    if (/^[\u4e00-\u9fa5]{0,}$/.test(value)) {
      return true
    } else {
      return false
    }
  },
  //  是否不爲空
  notNull: function (value) {
    if (value !== '') {
      return true
    } else {
      return false
    }
  },
.... // 不同策略
}
 
 
// 環境類
var validate = function(rule,value) {
    return rules[rule](value);
};
 
//業務執行
const isChinese = validate('isChinese',value)
const notNull = validate('notNull',value)
const checkResult = isChinese||notNull
if(checkResult){
    .....
}


總結:一個簡單的策略模式就已經實現了,將校驗的抽象方法放在策略類,然後根據參數的不同去調用不同的策略,實現了策略的複用,也實現了代碼的簡化。但是這樣的策略是你心中真正的愛麼兄弟?
缺點:上述的代碼已經滿足了策略模式,但是對於具體業務的支持似乎還有點小瑕疵,首先上述的業務執行,你會發現對於單個校驗我們返回的是boolean值,如果我們需要有失敗的回調函數的調用,那就還是需要判斷語句的加入,真的是魚和熊掌不可兼得,也就是說上述的代碼,只能支持表單項全部檢測完成後總的失敗回調,對於當個表單項的失敗無法支持,用戶往往輸入完成全部表單項後才被告知表單中有錯誤,而且還不知道具體是哪個
優化:修改環境類,參數對象形成的數組或單個參數對象傳入,參數對象傳入時提供失敗函數,對外提供一個檢驗方法,遍歷檢查參數對象數組,全部成功返回true,失敗執行失敗回調函數同時返回false。

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Validate {
  constructor () {
    this.cache = []
    if (Array.isArray(arguments[0])) {
      // 數組的單個元素{rule:string[規則的名稱],value:any[校驗的值],efn:fn[失敗的回調]}
      this.cache = arguments[0]
    }
    // 傳入參數爲對象時
    this.cache.push(arguments[0])
  }
  // 執行校驗,失敗的話執行失敗的回調,成功靜默,所有的參數符合規則則返回true
  valid () {
    let i = 0
    for (const value of this.cache) {
      if (rules[value.rule] && rules[value.rule](value.value)) {
        i++
      } else {
        if (value.efn) value.efn()
        return false
      }
    }
    return i === this.cache.length
  }
}


總結:策略模式可以應用使用判斷語句多的時候,判斷內容爲同種模式的情況下。策略模式中的策略類可以進行復用,從而避免很多地方的複製粘貼,獨立的策略類易於理解、切換、拓展。
五、中介者模式

中介者模式的作用就是解除對象與對象之間的緊耦合關係。對象與對象中的通信以中介者爲媒介觸發。中介者使各對象之間耦合鬆散,而且可以獨立地改變它們之間的交互。中介者模式使網狀的多對多關係變成了相對簡單的一對多關係。

場景分析:設定兩個角色房東租客房東租客組成的是多對多的網格關係,一個房東可以與多名租客接觸,詢問是否有租房意願,同理租客可以向多名房東打探是否有房源出售。
前置條件我們實例化兩個角色房東租客,我們使用簡單工廠模式

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 房東類
class Owener{
    constructor(name){
        this.name=name
    }
    // 想要出租
    sell(renters=[]){
        renters.map(renter=>{
            renter.onMessage(`${this.name}想要出租房子`)
        })
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
// 租客類
class Renter{
    constructor(name){
        this.name=name
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Factory{
    constructor(role,name){ 
        this.name = name
        return this.switchRole(role,name)
    }
    switchRole(role,name){
        switch (role) {
            case 'owner':
                return new Owener(name)
            case 'renter':
                return new Renter(name)
            default:
                return new Error('無當前角色')
        }
    }
}
var owner1 = new Factory('owner','房東一')
var owner2 = new Factory('owner','房東二')
var renter1 = new Factory('renter','租客一')
var renter2 = new Factory('renter','租客二')
var renter3 = new Factory('renter','租客三')
owner.sell([renter1,renter2,renter3])


上述代碼完成了房東一 發佈出租房子的意願,三名租客接受到了消息。反向同理也可以實現。租客發佈消息,房東收到,但是我們發現房東 與租客之間。還是存在緊密的耦合,房東租客之間不同的關係需要自行不同的方法,那整個房東類會變得及其臃腫,反之亦然。於是我們引入中介者模式

中介者模式在上述例子中理解爲中介公司,扮演了維護房東 和租客關係的橋樑,租客房東類只要考慮各自的行爲,不需要考慮行爲會給那些關係對象帶來影響。這些任務交給我們的中介者

優化:生成一個中介者來處理房東租客的相互調用,以及確定相互關係,中介者提供一個雙向的方法,供房東租客調用,然後實現對相關對象的分發

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Owener{
    constructor(name){
        this.name=name
    }
    // 想要出租
    sell(mediator){
        mediator.sendMessage('owner',`${this.name}想要出租房子`)
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Renter{
    constructor(name){
        this.name=name
    }
    // 想要租房
    rent(mediator){
        mediator.sendMessage('renter',`${this.name}想要租房子`)
    }
    // 收到消息
    onMessage(msg){
        console.log(`${this.name}知道了${msg}`)
    }
}
class Factory{
    constructor(role,name){ 
        this.name = name
        return this.switchRole(role,name)
    }
    switchRole(role,name){
        switch (role) {
            case 'owner':
                return new Owener(name)
                break;
            case 'renter':
                return new Renter(name)
                break;
            default:
                return new Error('無當前角色')
                break;
        }
    }
}
class Mediator{
    constructor(owner,renter){
      // 房東集合
      this.owner = owner
      // 房客集合
      this.renter = renter
    }
    sendMessage(role,msg){
       if(role === 'owner'){
           for(const value of this.renter){
               value.onMessage(msg)
           }
       }
       if(role === 'renter'){
           for(const value of this.owner){
               value.onMessage(msg)
           }
       }
    }
}
var owner1 = new Factory('owner','房東一')
var owner2 = new Factory('owner','房東二')
var renter1 = new Factory('renter','租客一')
var renter2 = new Factory('renter','租客二')
var renter3 = new Factory('renter','租客三')
var mediator = new Mediator([owner1,owner2],[renter1,renter2,renter3])
owner1.sell(mediator)
租客一知道了房東一想要出租房子
租客二知道了房東一想要出租房子
租客三知道了房東一想要出租房子
renter1.rent(mediator)
房東一知道了租客一想要租房子
房東二知道了租客一想要租房子


總結房東租客各自維護自己的行爲,通知調用中介者。中介者維護對象關係以及對象方法的調用。優點:對象的關係在中介者中可以自由定義、一目瞭然。減少了兩個類的臃腫。去除了兩個對象之間的緊耦合。缺點:兩個關係類的不臃腫換來了中介者類的臃腫。
六、裝飾者模式

爲對象增添特性的技術,它並不使用創建新子類這種手段

場景分析:看了別的文章都是自行車的場景那我也來唄,嘻嘻~,自行車商店:出售A、B、C、D四種牌子的自行車,這時候可以選擇配件車燈、前置籃。如果按照正常創造子類的方法,我們首先定義 A、B、C、D四個牌子的自行車父類,然後再根據配件通過繼承父類來實現子類。

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// A自行車父類
class Bicycle{
    constructor(){
        this.type = 'A'
    }
    getBicycle(){
        console.log('A自行車的售價100')
    }
}
// 擁有車燈的A類自行車類
class BicycleDeng extends Bicycle{
    constructor(){
        super()
    }
    setDeng(){
        console.log('我安裝上了大燈')
    }
}


上述代碼我們可以發現:當我們需要窮盡場景中可能出現的自行車的時候:A類有車燈、A類有前置欄、B類有車燈....一共要4*2一共八個類。這個時候我們使用的是創建新子類的手段將所有情況窮舉到實體類上面,想要實例化一個對象只要找到對應實體類就好了。
優化:如果自行車的類型增多,配件增多就會出現實體類幾何增加,你們應該不會想把自己的餘生花在維護實體類上吧,來吧出來吧我們的裝飾者模式。首先回歸業務場景:A、B、C、D我們可以暫時稱爲組件。而我們的配件就是裝飾者主要思路替換創建新子類,每個裝飾者維護單個裝飾者類,將組件作爲實例傳入裝飾者類中進行‘裝飾’可以重寫原有方法、也可以新增方法。 這樣我們只要維護組件類加裝飾者類 4+2一個6個類

[JavaScript] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Bicycle{
    constructor(){
        this.type = 'A'
        // 自行車售價
        this.price = 5000
    }
    getBicycle(){
        console.log(`A自行車的售價${this.price}`)
    }
}
class BicycleDecorator{
    constructor(bicycle){
        // 傳入的組件
        this.bicycle = bicycle
        // 大燈售價
        this.dengPrice = 200
    }
    // 新增方法
    openDeng(){
        console.log('我打開了燈')
    }
    // 重寫方法
    getBicycle(){
        console.log(`A自行車的售價${this.bicycle.price + this.dengPrice}`)
    }
}
// 先創建類型自行車
var a = new Bicycle()
// 裝飾後替換原有
a = new BicycleDecorator(a)
a.getBicycle() // A自行車的售價5200


總結:裝飾者模式爲對象增添特性提供了新的思路,去除了創建新的子類對應最小單位實體類,通過傳遞一個父類來進行對於父類增加新特性的方法是,保留了父類原有方法也具有延展性。真香~~
結束語本文提供了部分的設計模式以及自己的理解,理解可能存在偏差歡迎討論,本文參考的是《javascript設計模式》。之後如有設計模式的補充還會繼續更新。


文章轉載自:https://juejin.im/post/5eb3be806fb9a043426818c7

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章