TS 设计模式之策略模式

975 阅读7分钟

故事的主人公叫小 M ,他一直想在工作之外再赚点钱,某一天,他突发奇想,想去开一家网店,卖什么呢?最近新发布的 13 香 iPhone、iPad,肯定很多人买,那我去卖保护壳?肯定大卖,说干就干,第二天他就把自己的网店支愣起来了。

虽然网店申请好了,但是他的产品还没设计好,于是乎,他开始当「架构师」,设计他的产品。

一口吃不成个胖子,他想先让他家的保护壳可以自定义颜色和材质,为了方便所有的保护壳使用,他做了一个父类,实现了设置颜色、设置材质这两个功能。

image.png

type Color = 'black' | 'red' | 'green';
type Material = '橡胶' | '皮质' | '硬质塑料'

class 保护壳 {
  private color: Color = 'black'
  private material: Material = '橡胶'

  constructor() {
    // ...
  }

  设置颜色(color: Color) {
    this.color = color
  }

  设置材质(material: Material) {
    this.material = material;
  }

  whoIAm() {
    console.log('我是保护壳')
  }
}

设置好了这个保护壳父类后,继承这个父类的 iPhone 保护壳和 iPad 保护壳的代码就特别少,小 M 洋洋得意说:看我的设计水平多高,以后我的店做大了,想做小米手机的、华为手机的,根本不需要写代码,只需要继承一下就好!

class IPhone extends 保护壳 {
  whoIAm() {
    console.log('我是 iPhone 保护壳')
  }
}

class IPad extends 保护壳 {
  whoIAm() {
    console.log('我是 iPad 保护壳')
  }
}

刚好,有用户下单说,我想要一个红色、皮质的 iPad 保护壳,小 M 直接就用刚才建的 Ipad 类运行下面两行代码,嗖的一下就做好了:

const a = new IPad()
a.设置材质('皮质')
a.设置颜色('red')

随着时间的推移,他家的网店慢慢做大了,又支持了小米手机、小米平板、华为手机、华为平板等数十种产品。

做好了这一步,小 M 并不满足,我那只是一个开始,为了能占领某某平台的榜首,我需要我的保护壳与众不同!

先让保护壳支持用户 DIY 吧?

怎么实现呢,小 M 想,这简单呀,就在父类加一个 DIY 方法就好了,这样我们店里卖的全部保护壳就都能一下支持 DIY 了:

class 保护壳 {
  ...

  DIY() {
     console.log('我是父类的 DIY')
    // 用户自己 DIY
  }
  ...
}

确实,这样以来,所有保护壳的子类都能进行 DIY 了。但是这只是短暂的黎明。

就在此时,小 M 遇到了开网店的第一个打击,虽然它设计好了方案,但是工厂那里由于原材料不够,暂时不能让 iPad 支持 DIY,而我们的 DIY 方法加到了所有的保护壳中,要赶紧的下线掉。

怎么办呢?小 M 想,这可难不倒我,我直接让 iPad 保护壳的类里,写一个空的 DIY 方法,覆盖一下父类的的 DIY 方法就好。

class IPad extends 保护壳 {
  DIY() {
      // 什么也不做,只用了覆盖
  }

  whoIAm() {
    console.log('我是 iPad 保护壳')
  }
}

这次问题算是解决了,但是又有问题了。

有一位顾客要求华为平板保护壳的 DIY 功能更丰富一点。没办法,顾客是爹呀,这时候 小 M 不得不找这个保护壳,按照他的要求修改 DIY 函数。

后来那位顾客的一个朋友发现,你的保护壳不错哎,但是他使用的是 iPad ,所以,也想在 iPad 里面有一样的 DIY 功能,小 M 又得去 iPad 的子类里面去修改。

这种情况经常发生,他按照上面的方法在每个子类里面改来改去,发现重复代码越来越多,但是又很难复用。

终于有一天,他忍受不了每次都要去子类里面修改的痛苦了,决心要修改一下代码的架构。

他发现 DIY 这个行为经常因为各种原因被修改,所以第一步,他打算先把这个行为抽象出来:

type DeviceType = 'mi-11' | 'mid-pad-5' | 'iphone' | 'ipad'

interface DIYBehavior {
  DIY: () => void
}

接下来他根据可不可以 DIY,做了两个实现了这个接口的类。

如果当前可以 DIY,就是这个类:

class CanDIY implements DIYBehavior {
  private deviceType: DeviceType

  constructor(deviceType: DeviceType) {
    this.deviceType = deviceType;
  }

  DIY() {
    console.log(`我要对这个${this.deviceType}进行 DIY`)
  }
}

当不能 DIY 的时候,就用这个:

class CanNotDIY implements DIYBehavior {
  private deviceType: DeviceType

  constructor(deviceType: DeviceType) {
    this.deviceType = deviceType;
  }

  DIY() {
    console.log(`目前对这个${this.deviceType}不能进行 DIY`)
  }
}

我们完全把 DIY 这个行为的逻辑抽离了出去,那怎么用呢?

假如,目前我们的 iPad 平板支持 DIY,我们就可以按照如下的方式组织我们的 iPad 类:

class IPad extends 保护壳 {
  private ipadDIY: DIYBehavior

  constructor() {
    super();
    this.ipadDIY = new CanDIY('ipad')
  }

  DIY() {
    this.ipadDIY.DIY()
  }
  ...
}

也就是说,我们把 DIYBehavior 作为了我们保护壳的一个属性,只要是进行 DIY,就通过这个属性来做。

改成这样之后,我们设想一下,假如我们的平板的 DIY 的功能又增强了,支持了各种酷炫的功能,以前的设计我们可能要把每个支持 DIY 子类的 DIY 方法都改一下。

但是现在,我们完全不需要改动它们,我们只需要改动 CanDIY 这个类的 DIY 方法就好了!

不仅如此,如果我们某个类突然不支持 DIY 功能了,没关系,我们可以增加一个 setter,运行时修改这个变量:

class IPad extends 保护壳 {
  private _ipadDIY: DIYBehavior

  constructor() {
    super();
    this._ipadDIY = new CanDIY('ipad')
  }

  set ipadDiY(ipadDiY: DIYBehavior) {
    this._ipadDIY = ipadDiY;
  }

  DIY() {
    this._ipadDIY.DIY()
  }

  whoIAm() {
    console.log('我是 iPad 保护壳')
  }
}

const a = new Ipad()

a.ipadDiY = new CanNotDIY('ipad')
a.DIY() // 目前对这个ipad不能进行 DIY

有没有感觉这样子组织方便、灵活了好多。我们再也不用担心 DIY 这个行为对我们的各个子类有别的什么影响了。

上面我们使用到的设计模式就是策略模式,下面是比较官方的定义,大家大概明白意思就好,反正感觉有点不像「人话」。

策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中华民国交个人所得税”就有不同的算税方法。 —— 维基百科

不知道大家看了上面的例子,在看这个定义是不是更理解了一点。

总结来说,我们可以把一些「行为」抽离出来,单独维护,以便我们可以灵活的对这些「行为」进行改动。

举一个比较常见的例子,如果我们想开发一个缓存模块,在这个缓存模块里的缓存有内存级别、session 级别、localStorge 级别,此时对于设置缓存、清除缓存的方法,我们就可以借助策略模式,把他们抽离出来。

另外,我们的 React 中一般会调用 this.setState 来更新视图,这个方法可以在 ReactDOM 下使用,也可以在 ReactNative 下使用,这是因为 React 中的更新器(updater)会根据平台的不同而不同,任你以后多加几个平台,只要实现了更新器规定的接口,就能正常的使用 React。这里的设计也是使用了策略模式。

在上面的示例中,我们实现 DIYBehaviour 这个接口使用的是类,这在其他语言中是很自然的,但这有点不太符合在 JavaScript 中使用习惯,我们在理解了策略模式后,遵循着它的准则,我们也可以使用函数来实现,这可能用起来更舒服、方便一点:

function canDiY(deviceType: DeviceType): DIYBehavior {
  return {
    DIY() {
      console.log(`我要对这个${deviceType}进行 DIY`)
    } 
  }
}

class IPad extends 保护壳 {
  private _ipadDIY: DIYBehavior

  constructor() {
    super();
    this._ipadDIY = canDIY('ipad') // 调用形式稍微改一下
  }
  ...
}

到这里,小 M 终于开心的完成了对这段代码的重构,我们的故事就结束了。

谢谢各位的阅读,同时祝各位中秋假期快乐。