lulusir / my-blog

my-blog
https://github.com/lulusir/my-blog/issues
13 stars 1 forks source link

装饰模式的例子 #18

Open lulusir opened 6 years ago

lulusir commented 6 years ago

装饰模式

描述

装饰模式:装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

适用性-百科

以下情况使用Decorator模式:

  1. 需要扩展一个类的功能,或给一个类添加附加职责。
  2. 需要动态的给一个对象添加功能,这些功能可以再动态的撤销。
  3. 需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。
  4. 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

    代码示例

    在装饰模式中的各个角色有:

  5. 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
  6. 具体构件(Concrete Component)角色:定义一个将要接收附加责任的类。
  7. 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并实现一个与抽象构件接口一致的接口。
  8. 具体装饰(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

```typescript
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
> 把验证器写成单独的插件
```typescript
/**
 * 一个使用装饰功能的表单验证插件
 */
// 先定义一下希望插件的调用方式, 输入一个数组,内容可以是字符串或者对象
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设计模式与开发实践》

AboyL commented 6 years ago

ts吧

lulusir commented 6 years ago

@AboyL 是的,最近在学ts,就用ts来写了