裝飾模式-使用裝飾器來寫表單驗證外掛

lujs發表於2019-02-16

裝飾模式

描述

裝飾模式:裝飾模式是在不必改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能。它是通過建立一個包裝物件,也就是裝飾來包裹真實的物件。

適用性-百科

以下情況使用Decorator模式:

  1. 需要擴充套件一個類的功能,或給一個類新增附加職責。
  2. 需要動態的給一個物件新增功能,這些功能可以再動態的撤銷。
  3. 需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
  4. 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類。

程式碼示例

在裝飾模式中的各個角色有:

  1. 抽象構件(Component)角色:給出一個抽象介面,以規範準備接收附加責任的物件。
  2. 具體構件(Concrete Component)角色:定義一個將要接收附加責任的類。
  3. 裝飾(Decorator)角色:持有一個構件(Component)物件的例項,並實現一個與抽象構件介面一致的介面。
  4. 具體裝飾(Concrete Decorator)角色:負責給構件物件新增上附加的責任。
// Component類 定義一個物件介面,可以給這些物件動態的新增職責
abstract class Component {
  abstract Operation (): void
}

// ConcreteComponent 類定義一個具體的物件,也可以給這個物件新增職責
class ConcreteComponent extends Component {
  Operation () {
    console.log(`具體的物件操作`);
  }
}

// Decorator 裝飾抽象類,繼承了Component 從外類來擴充Component的功能,但是對於Component來說,無需知道Decorator的存在
abstract class Decorator extends Component{
  protected component: Component | null = null
  // 裝載Component
  SetComponent (component: Component) {
    this.component = component
  }
  // 重寫Operation,實際執行的是component的Operation
  Operation () {
    if (this.component !== null) {
      this.component.Operation()
    }
  }
}

// ConcreteDecorator 具體的裝飾物件 起到給Component類新增職責
class ConcreteDecoratorA extends Decorator {
  private addState: string = ``
  Operation () {
    // 先執行裝飾物件的Operation,如果有的話
    super.Operation()
    this.addState = `new stateA`
    console.log(`具體裝飾物件A的操作`);
  }
}

class ConcreteDecoratorB extends Decorator {
  Operation () {
    super.Operation()
    this.AddedBehavior()
    console.log(`具體裝飾物件b的操作`);
  }
  AddedBehavior () {
    console.log(`new state B`);
  }
}
// 呼叫
const c = new ConcreteComponent()
const d1 = new ConcreteDecoratorA()
const d2 = new ConcreteDecoratorB()

d1.SetComponent(c) // d1裝飾的是c
d2.SetComponent(d1) // d2裝飾的是d1
d2.Operation() // d2.Operation中先會呼叫d1的Operation,d1.Operation中先會呼叫c的Operation

js的裝飾器

js有自帶的裝飾器,可以用來修飾類和方法

例子 – 換衣服系統

實現一個換衣服系統,一個人可以穿各種服飾,以一定順序輸出穿捉的服裝

版本0

class Person0 {
  private name: string;
  constructor (name: string) {
    this.name = name
  }

  wearTShirts () {
    console.log(` T-shirts`)
  }

  wearBigTrouser () {
    console.log(` big trouser`)
  }

  wearSneakers () {
    console.log(`sneakers `)
  }

  wearSuit () {
    console.log(`suit`)
  }

  wearLeatherShoes () {
    console.log(`LeatherShoes`)
  }

  show () {
    console.log(this.name)
  }
}
const person0 = new Person0(`lujs`)
person0.wearBigTrouser()
person0.wearTShirts()
person0.wearSneakers()
person0.wearSuit()
person0.show()

版本1

上面的版本0,每次要新增不同的服飾,就需要修改person類,不符合開放-封閉原則,下面會抽離出服飾類,每個服飾子類都有新增服飾的方法,這樣就解耦了person和finery

class Person1 {
  private name: string;
  constructor (name: string) {
    this.name = name
  }
  show () {
    console.log(this.name)
  }
}

abstract class Finery {
  abstract show (): void
}

class Tshirts extends Finery {
  show () {
    console.log(` T-shirts`)
  }
}
class BigTrouser extends Finery {
  show () {
    console.log(` BigTrouser`)
  }
}
class Sneakers extends Finery {
  show () {
    console.log(` Sneakers`)
  }
}
// 呼叫
const person1 = new Person1(`lujs`)
const ts = new Tshirts()
const bt = new BigTrouser()
const sneakers = new Sneakers()
person1.show()
ts.show()
bt.show()
sneakers.show()

版本2

上面的版本1,單獨抽離了服飾類,這樣就可以隨意新增服飾而不會影響到person了,
但是在上面的呼叫程式碼中需要按順序去呼叫自定服飾的show,最好就是隻呼叫一次show就顯示出正確的穿衣服順序;需要把功能按正確的順序進行控制,下面用裝飾模式來實現實現

class Person2 {
  private name: string = ``
  setName (name: string) {
    this.name = name
  }

  show () {
    console.log(`裝扮`, this.name);
  }
}
class Finery2 extends Person2 {
  private component: Person2 | null = null
  Decorator (component: Person2) {
    this.component = component
  }
  show () {
    if (this.component !== null) {
      this.component.show()
    }
  }
}

class Tshirts2 extends Finery2 {
  show () {
    super.show()
    console.log(`穿tshirt`);
  }
}
class BigTrouser2 extends Finery2 {
  show () {
    super.show()
    console.log(`穿BigTrouser`);
  }
}

const p2 = new Person2()
const t1 = new Tshirts2()
const b1 = new BigTrouser2()
p2.setName(`p2`)
t1.Decorator(p2)
b1.Decorator(t1)
b1.show()

當系統需要新功能的時候,是向舊的類中新增新的程式碼。這些新加的程式碼通常裝飾了原有類的核心職責或主要行為,
比如用服飾裝飾人,但這種做法的問題在於,它們在主類中加入了新的欄位,新的方法和新的邏輯,從而增加了主類的複雜度
而這些新加入的東西僅僅是為了滿足一些只在某種特定情況下才會執行的特殊行為的需要。
而裝飾模式卻提供了一個非常好的解決方案,它把每個要裝飾的功能放在單獨的類中,
並讓這個類包裝它所要裝飾的物件,因此,當需要執行特殊行為時,客戶程式碼就可以在執行時根據需要有選擇地、按順序地使用裝飾功能包裝物件了

版本3

接下來我們使用js自帶的裝飾器來實現

class Person4 {
  private name: string = ``
  SetName (name: string) {
    this.name = name
  }
  show () {
    console.log(`裝備開始`, this.name)
  }
}
// 裝飾函式
type fn = () => void
function beforeShow(fns: fn[]) {
  return (target:any, name:any, descriptor:any) => {
    const oldValue = descriptor.value
    descriptor.value = function () {
      const value = oldValue.apply(this, arguments);
      fns.forEach(f:fn => {
        f()
      })
      return value
    }
  }
}
// 使用函式來代替服飾子類
const wearTShirts = () => {
  console.log(`wear Tshirts`);
}
const wearBigTrouser = () => {
  console.log(`wear BigTrouser`);
}
class Finery4 extends Person4 {
  private person: Person4 | null = null
  addPerson (person: Person4) {
    this.person = person
  }
  @beforeShow([wearBigTrouser, wearTShirts])
  show () {
    if (this.person !== null) {
      this.person.show()
    }
  }
}
// 需要修改服飾順序的時候,可以直接修改服飾類的裝飾函式順序,或者生成另一個類
class Finery5 extends Person4 {
  private person: Person4 | null = null
  addPerson (person: Person4) {
    this.person = person
  }
  @beforeShow([wearTShirts, wearBigTrouser])
  show () {
    if (this.person !== null) {
      this.person.show()
    }
  }
}
const p6 = new Person4()
const f6 = new Finery4()
p6.SetName(`lll`)
f6.addPerson(p6)
f6.show()

console.log(`換一種服飾`);
const p7 = new Person4()
const f7 = new Finery4()
p7.SetName(`lll`)
f7.addPerson(p7)
f7.show()

表單例子

版本0

一般我們寫表單提交都會想下面那樣先驗證,後提交,
但是submit函式承擔了兩個責任,驗證和提交。
我們可以通過裝飾器把驗證的方法剝離出來

const ajax = (url:string, data: any) => {console.log(`ajax`, url, data)}
class Form {
  state = {
    username: `lujs`,
    password: `lujs`
  }
  validata = ():boolean => {
    if (this.state.username === ``) {
      return false
    }
    if (this.state.password === ``) {
      return false
    }
    return true
  }
  submit = () => {
    if (!this.validata()) {
      return
    }
    ajax(`url`, this.state)
  }
}

版本1

先把驗證函式單獨寫成外掛
現在submit函式只有提交資料這個功能,而驗證功能寫成了裝飾器

interface RequestData {
  username: string
  password: string
}
type Vality = (data: RequestData) => boolean
type ValityFail = (data: RequestData) => void
const validata = (data: RequestData):boolean => {
  if (data.username === ``) {
    return false
  }
  if (data.password === ``) {
    return false
  }
  console.log(`驗證通過`)
  return true
}
function verify(vality:Vality,  valityFail: ValityFail) {
  return (target:any, name:string, descriptor:any) => {
    const oldValue = descriptor.value
    descriptor.value = function (requestData: RequestData) {
      // 驗證處理
      if (!vality(requestData)) {
        // 驗證失敗處理
        valityFail(requestData)
        return
      }
      // console.log(this, ` == this`)
      return oldValue.apply(this, arguments)
    }
    return descriptor
  }
}
class Form1 {
  state = {
    username: ``,
    password: `password`
  }
  @verify(validata, () => console.log(`驗證失敗`))
  submit(requestData: RequestData) {
    ajax(`url`, requestData)
  }
}
console.log(`表單驗證例子1開始---`)
const f1 = new Form1
f1.submit(f1.state)

f1.state.username = `lujs`
f1.submit(f1.state)
console.log(`表單驗證例子1結束---`)

#### 版本2

把驗證器寫成單獨的外掛

/**
 * 一個使用裝飾功能的表單驗證外掛
 */
// 先定義一下希望外掛的呼叫方式, 輸入一個陣列,內容可以是字串或者物件
state = {
  username: `lujs`,
  myEmail: `123@qq.com`,
  custom: `custom`
}
@validate([
  `username`, // fail: () =>console.log(`username wrong`)
  {
    key: `myEmail`,
    method: `email`
  },
  {
    key: `myEmail`,
    method: (val) => val === `custom`,
    fail: () => alert(`fail`)
  }
])
submit(requestData: RequestData) {
  ajax(`url`, requestData)
}

./validator.ts

export interface Validator {
  notEmpty (val: string):boolean
  notEmail (val: string):boolean
}
export const validator: Validator = {
  notEmpty: (val: string) => val !== ``,
  notEmail: (val: string) => !/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/.test(val)
}

export interface V {
  key: string
  method: keyof Validator | ((val: any) => boolean)
  fail?: (val: any) => void
}
export function verify(
  vality: Array<V | string>,
) {
  return (target:any, propertyKey:string, descriptor:PropertyDescriptor) => {
    const oldValue = descriptor.value
    descriptor.value = function (requestData: {[p: string]: any}) {
      // 驗證處理
      const flag = vality.every((v) => {
        console.log(typeof v, v, ` == this`)
        if (typeof v === `string`) {
          const val = requestData[v]
          // 預設進行empty判斷
          if (!validator.notEmpty(val)) {
            return false
          }
        } else {
          // 物件的情況
          const val = requestData[v.key]
          console.log(val, ` => val`)
          console.log(v, ` => v`)
          if (typeof v.method === `string`) {
            if (!validator[v.method](val)) {
              if (v.fail) {
                v.fail.apply(this, requestData)
              }
              return false
            }
          } else {
            console.log(v.method(val), val)
            if (!v.method(val)) {
              if (v.fail) {
                v.fail.apply(this, requestData)
              }
              return false
            }
          }
        }
        return true
      })
      if (!flag) {
        return
      }
      return oldValue.apply(this, arguments)
    }
    return descriptor
  }
}

./form

import {verify} from `./validator`

const ajax = (url:string, data: any) => {console.log(`ajax`, url, data)}
class Form2 {
  state = {
    username: `lujs`,
    myEmail: `123@qq.com`,
    custom: `custom`
  }
  @verify([
    `username`, // fail: () =>console.log(`username wrong`)
    {
      key: `myEmail`,
      method: `notEmail`
    },
    {
      key: `myEmail`,
      method: (val) => val !== `custom`,
      fail: (val) => console.log(val)
    }
  ])
  submit(requestData: {[p: string]: any}) {
    ajax(`url`, requestData)
  }
}

const f2 = new Form2
f2.submit(f2.state)

例子來自《大話設計模式》《javascript設計模式與開發實踐》

相關文章