設計模式之裝飾器模式

裝飾器模式的定義

裝飾器模式(decorator pattern):允許向一個先有的對象增添新的功能,同時又不改變其解構。

裝飾器模式的優點

  1. 裝飾器模式與繼承關係的目的都是要擴展對象的功能,但是裝飾器模式可以提供比繼承更多的靈活性。裝飾器模式允許系統動態決定貼上一個需要的裝飾,或者除掉一個不需要的裝飾。繼承關係是不同,繼承關係是靜態的,它在系統運行前就決定了

  2. 通過使用不同的具體裝飾器以及這些裝飾類的排列組合,設計師可以創造出很多不同的行爲組合

裝飾器模式的缺點

  1. 由於使用裝飾器模式,可以比使用繼承關係需要較少數目的類。使用較少的類,當然使設計比較易於進行。但是另一方面,由於使用裝飾器模式會產生比使用繼承關係更多的對象,更多的對象會使得查錯變得困難,特別是這些對象看上去都很像。

裝飾器模式和適配器模式的區別

  1. 適配器模式的意義是要將一個接口轉變成另外一個接口,它的目的是通過改變接口來達到重複使用的目的

  2. 裝飾器模式不要改變被裝飾對象的接口,而是恰恰要保持原有的接口哦,但是增強原有接口的功能,或者改變原有對象的處理方法而提升性能

示例代碼

恩,來一個場景:假設我們要去報名上學,通常學校都是漢語(普通話), 但是我們爲了更好的教育,可能會報雙語學校,那麼我麼就可以學會英語和漢語了

實現思路:

  1. 創建學校實例,
  2. 創建適配器,即雙語學校
  3. 創建學校實例,調用learn方法
// 裝飾器模式
// 裝飾只是讓原來的功能更強大,並不改變原來的功能
// 1. 學校實例
class School {
  constructor (name) {
    this.name = name
  }
  learn (language) {
    console.log(`learn ${language}`)
  }
}

// 2. 創建適配器(雙語學校類)
class DoubleLanguageSchool {
  constructor (name) {
    this.school = new Durk(name)
  }
  learn (language) {
    this.school.learn(language)
    console.log(`learn English`)
  }
}
// 3. 創建實例,調用learn方法
let s = new DoubleLanguageSchool('Leo')
s.learn('chinese')

常見開發場景

  • 開發產品,複用類,任意組合類, 以咖啡店爲例,我們在點咖啡的時候,除了最簡單的咖啡,可能需要加奶,加糖,加泡沫…相對的價格什麼的都會變化,我們這樣子就需要使用適配模式了
// 某些情況下裝飾模式回優於原型繼承模式
// 1. 咖啡類
class Coffee {
  make (water) {
    return `${water}+咖啡`
  }
  cost () {
    return 10
  }
}
// 2. 牛奶咖啡
class MilkCoffee {
  constructor (parent) {
    this.parent = parent
  }
  make (water) {
    return `${this.parent.make(water)}+牛奶`
  }
  cost () {
    return this.parent.cost() + 5
  }
}

// 加糖咖啡
class SugarCoffee {
  constructor(parent) {
    this.parent = parent
  }
  make(water) {
    return `${this.parent.make(water)}+糖`
  }
  cost() {
    return this.parent.cost() + 3
  }
}
// 我們通過一下類, 就可以自由組合
let coffee = new Coffee()
let milkCoffee = new MilkCoffee(coffee)
let sugerCoffee = new SugarCoffee(milkCoffee)

let ret = milkCoffee.make('水')
let cost = milkCoffee.cost()
console.log(ret, cost)

let ret2 = sugerCoffee.make('水')
let cost2 = sugerCoffee.cost()
console.log(ret2, cost2)
  • 處理事件之前和之後處理額外的邏輯

    // AOP 就是在覈心函數執行之前或者之後執行額外邏輯,俗稱切面編程
    
    /**
     * @desc 定義before方法
     * @param {function} beforeFn
     */
    Function.prototype.before = function (beforeFn) {
      let self = this;
      return function () {
        beforeFn.apply(this, arguments)
        self.apply(this, arguments)
      }
    }
    
    /**
     * @desc 定義after方法
     * @param {function} afterFn
     */
    Function.prototype.after = function (afterFn) {
      let self = this;
      return function () {
        self.apply(this, arguments)
        afterFn.apply(this, arguments)
      }
    } 
    /**
     * @desc 購物
     * @param {number} money 
     * @param {string} product 
     */
    function buy(money, product) {
      console.log(`花${money}${product}`)
    }
    
    buy = buy.before(() => {console.log('銀行取錢')})
    buy = buy.after(()=>{console.log('將剩餘的錢存銀行')})
    // 調用
    buy(1888, '遊戲機')
    
  • 使用場景之埋點

    • 服務端腳本

      let Koa = require('koa')
      let app = new Koa()
      const _ = require('koa-route');
      let json = {}
      const res = {
        report: (ctx) => {
          console.log(ctx.query)
          let name = ctx.query.name
          if (json[name]) {
            json[name]++
          } else {
            json[name] = 1
          }
          ctx.body = 'hello'
        },
      
        json: (ctx) => {
          ctx.body = json
        }
      };
      
      app.use(_.get('/report', res.report));
      app.use(_.get('/', res.json));
      app.listen(3000)
      
    • 客戶管代碼

      <!-- 使用場景之埋點 -->
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
      </head>
      <body>
        <button data-name="watermelon" id="watermelon">西瓜</button>
        <button data-name="apple" id="apple">蘋果</button>
        <script>
          let watermelon = document.querySelector('#watermelon')
          let apple = document.querySelector('#apple')
          /**
           * @desc 定義after方法
           * @param {function} afterFn
           */
          Function.prototype.after = function (afterFn) {
            let self = this;
            return function () {
              self.apply(this, arguments)
              afterFn.apply(this, arguments)
            }
          }
      
          
          function click() {
            console.log('你點擊了'+ this.dataset.name)
          }
          click = click.after(function () {
            let img = new Image()
            img.src = `http://localhost:3000/report?name=${this.dataset.name}`
          })
          Array.from(document.querySelectorAll('button')).forEach(btn => {
            btn.addEventListener('click', click)
          })
        </script>
      </body>
      </html>
      
  • 使用場景之表單驗證

    <!-- 使用場景之埋點 -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>表單效驗</title>
    </head>
    <body>
      用戶名 <input type="text" id="username">
      密碼 <input type="password" id="password">
      <button id="submit">提交</button>
      <script>
        /**
       * @desc 定義before方法
       * @param {function} beforeFn
       */
        Function.prototype.before = function (beforeFn) {
          let self = this;
          return function () {
            // 這裏要拿到上一次效驗完畢的驗證結果
            if (beforeFn.apply(this, arguments)) {
              self.apply(this, arguments)
            }
          }
        }
        function submit(params) {
          console.log('提交表單')
        }
        submit = submit.before(function () {
          
          let username = document.getElementById('username').value
          if (!username) {
            alert('用戶名不能爲空')
            return false
          }
          return true
        })
        submit = submit.before(function () {
            let username = document.getElementById('username').value
            if (username.length < 4) {
              alert('用戶名長度最少爲4')
              return false
            }
            return true
          })
        document.getElementById('submit').addEventListener('click', submit)
      </script>
    </body>
    </html>
    
  • 其他使用場景

    • axios的請求發起之前的函數
    • vue的路由調整鉤子函數
    • 很多場景,就不一一贅述了.

總結

多學習,有助於睡眠

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