设计模式ts实战系列(上)

·  阅读 3295

本文提要

本文是一系列 ts 的设计模式实战总结,并不是单纯的介绍设计模式,而是从工作中的例子出发,因为这样才能让人体会到设计模式离我们很近。全篇每个设计模式都是从,概念、一句话概括、优缺点、实战几个方面来讲。

设计模式概念

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

如果我们把代码编程比作是战争的话,那么设计模式就是兵法。不会兵法肯定打不过会兵法的,即使能打过也要付出更多的代价。

设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

目的:使用设计模式是为了可重用代码,让代码更容易被他人理解、保证代码的可靠性。

设计模式原则

  • 开闭原则 对扩展开放,对修改关闭。保证程序的扩展性好,易于维护和升级
  • 单一职责原则 对一个类而言,应该仅有一个引起它变化的原因
  • 里氏代换原则 子类可以扩展父类的功能,但是不能改变父类原有的功能
  • 依赖倒置原则 抽象不依赖细节,细节应该依赖抽象。
  • 接口隔离原则 建立单一接口,代替庞大臃肿的接口。
  • 最小知识原则 一个对象应该对其他对象有最少的了解。类间解耦,弱耦合。

单例模式

特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

一句话概括

保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式场景

  • 电脑上的文件,无论在任何地方修改文件,它都只有一份
  • 古代的皇帝,也只能有一个
  • vue-router 跟 vuex 的 install 也是单例
  • 其实 js 中的字面量对象就是一个天然单例

优缺点

优点

  1. 减少内存开支 单例模式在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式就非常明显了
  2. 减少性能开销 由于单例模式只生成一个实例,所以,减少系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
  3. 避免对资源的多重占用 例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
  4. 设置全局的访问点 优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

缺点

  1. 单例很难扩展
  2. 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

常用的单例模式有两种

  • 懒汉模式
// 懒汉式,只有在调用 getInstance 的时候才会实例化 Singleton
class Singleton {
  static instance = null;
  // 获取实例方法
  static getInstance() {
    return this.instance || (this.instance = new Singleton());
  }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 == instance2); // true
复制代码
  • 饿汉模式
// 饿汉式,在类初始化的时候就已经创建好了实例
class Singleton {
  static instance = new Singleton();
  // 获取实例方法
  static getInstance() {
    return this.instance;
  }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 == instance2); // true
复制代码

工厂模式

目的

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行

使用场景

我们明确地计划不同条件下创建不同实例时

优缺点

优点

  1. 隐藏了对象创建的细节,将产品的实例化过程放到了工厂中实现。
  2. 客户端基本不用关心使用的是哪个产品,只需要知道用工厂的那个方法(或传入什么参数)就行了.
  3. 方便添加新的产品子类,每次只需要修改工厂类传递的类型值就行了。
  4. 遵循了依赖倒转原则。

缺点

  1. 适用于产品子类型差不多, 使用的方法名都相同的情况.
  2. 每添加一个产品子类,都必须在工厂类中添加一个判断分支(或一个方法),这违背了OCP(开闭原则)。

实现

比如我要有一个 Animal 工厂,这个工厂要生产动物。那么我要定义动物都有 Feature特征必须要有 color 颜色跟 bark 叫声。

1. 接口实现

// 定义工厂需要的动物特征
interface Feature {
  color: string;
  bark(): void;
}

// 定义动物类型名字
type name = 'cat' | 'dog'

// 子类必须要实现 Feature 接口的方法
// 这样我们就可以创建白色叫声喵喵喵的猫了
class Cat implements Feature {
  color = "白色";
  bark() {
    console.log(`${this.color} 喵喵喵`);
  }
}
// 创建 Dog 类
class Dog implements Feature {
  color = "黑色";
  bark() {
    console.log(`${this.color} 汪汪汪`);
  }
}
// 这就是一个动物工厂
class Animal {
  createAnimal(type: name) {
    switch (type) {
      case 'cat':
        return new Cat();
      case 'dog':
        return new Dog();
    }
  }
}

const animal = new Animal();
const cat = animal.createAnimal('cat');
const dog = animal.createAnimal('dog');

cat.bark()
dog.bark()
复制代码

2. 抽象类实现

abstract class Feature {
  abstract color: string;
  abstract bark(): void;
}

// 枚举可以使用的动物类型
enum animalType {
  'cat',
  'dog'
}

// 子类继承抽象类 Feature
// 这样我们就可以创建白色叫声喵喵喵的猫了
class Cat extends Feature {
  color = "白色";
  bark() {
    console.log(`${this.color} 喵喵喵`);
  }
}
// 创建 Dog 类
class Dog extends Feature {
  color = "黑色";
  bark() {
    console.log(`${this.color} 汪汪汪`);
  }
}
// 这就是一个动物工厂
class Animal {
  createAnimal(type: animalType) {
    switch (type) {
      case animalType.dog:
        return new Cat();
      case animalType.dog:
        return new Dog();
    }
  }
}

const animal = new Animal();
const cat = animal.createAnimal(animalType.cat);
const dog = animal.createAnimal(animalType.dog);

cat.bark()
dog.bark()
复制代码

享元模式

模式定义

享元模式,运用共享技术,有效地支持大量的细粒度的对象,以避免对象之间拥有相同内容而造成多余的性能开销。

享元(flyweight)模式的主要作用:性能优化,当系统创建过多相似的对象而导致内存占用过高,可以采用这种设计模式进行优化。

享元模式将对象的属性区分为内部状态与外部状态,内部状态在创建的时候赋值,外部状态在实际需要用到的时候进行动态赋值

对于内部状态和外部状态的区分,有几点:

  1. 内部状态存储于对象内部
  2. 内部状态可以被一些对象共享
  3. 内部状态独立于具体场景,通常不会改变
  4. 外部状态取决于具体场景,并根据场景变化,外部状态不能被共享。

实战

我们要创建 100 个大小相同颜色不同的 div。

不使用享元模式的做法是:

  1. 创建一个创建 div 的类,CreateDiv。
  2. new CreateDiv() 创建 div
  3. 我们需要 new 100 次。这样就造成了很大的空间浪费。
interface Div {
  width: number;
  height: number;
  color: string;
}
const divStore: Div[] = [];

class CreateDiv {
  public width = 100;
  public height = 100;
  public color = this.randomColor()
  // 随机颜色
  private randomColor () {
    const color = ['red', 'green', 'blue', 'white', 'black'];
    return color[Math.floor(Math.random() * color.length)];
  }
}

let count = 100;
while (count--) {
  const innerDiv = new CreateDiv();
  divStore.push(innerDiv);
}

const sizeof = require('object-sizeof')

console.log(sizeof(divStore)) // 5688
复制代码

享元模式来做

// 将 div 属性设置成内部跟外部两部分
interface Div {
  outer: {
    width: number;
    height: number;
  };
  innter: {
    color: string;
  };
}
// 用来储存 Div
const divStore: Div[] = [];
// 创建外部 div 类
class CreateOuterDiv {
  width: number = 100;
  height: number = 100;
}
class CreateInnerDiv {
  public color = this.randomColor()
  // 随机颜色
  private randomColor () {
    const color = ['red', 'green', 'blue', 'white', 'black'];
    return color[Math.floor(Math.random() * color.length)];
  }
}
// 创建外部 div
const outerDiv = new CreateOuterDiv();
let innerDiv: number;
let count = 100;

while (count--) {
  // 创建内部 div
  innerDiv = new CreateInnerDiv();
  divStore.push({
    outer: outerDiv,
    innter: innerDiv
  });
}

const sizeof = require('object-sizeof')
// 因为这个方法会把引用的对象也全部算一遍,所以我们拆开来算

// 验证:100 * (innerDiv + outerDiv)= 5400 与上面算的 5688 很接近,可以认为这个方法是准确的
console.log(100 * (sizeof(innerDiv) + sizeof(outerDiv))) // 5400
// 100 * innerDiv + outerDiv = 1638
console.log(100 * sizeof(innerDiv) + sizeof(outerDiv)) // 1638
复制代码

从上面的计算结果来看减少了很大的内存,因为 divStore 数组对象中 outerDiv 其实只有一个,都是它的引用而已。我们的内存占用是 100 * innerDiv + outerDiv,而不使用享元模式的空间是 100 * (innerDiv + outerDiv)

策略模式

定义

定义一系列的算法, 把它们一个个封装起来, 并且使它们可相互替换。

优缺点

优点

  1. 算法可以自由切换。
  2. 避免使用多重条件判断。
  3. 扩展性好,符合开闭原则。

缺点

  1. 策略类会增多。
  2. 所有策略类都需要对外暴露。

实战

在vue中有一个合并选项策略 optionMergeStrategies,它的功能就是把选项添加一些策略,可以达到我们对选项数据操作的目的

官方例子,将选项 _my_option 添加策略,让它的值加一

Vue.config.optionMergeStrategies._my_option = function (parent, child, vm) {
  return child + 1
}

const Profile = Vue.extend({
  _my_option: 1
})

// Profile.options._my_option = 2
复制代码

我们来简单实现一下这个合并选项策略

// 策略模式 store
const optionMergeStrategies: { [prop: string]: any } = {};

// 给 _my_option 添加策略
optionMergeStrategies._my_option = function(value) {
  return value + 1
}

// 声明 data
const data = {
  // 添加策略
  _my_option: 1,
  // 未添加策略
  option: 1
};

// 响应式
function reactive (data) {
  const hander = {
    get(target, key, value) {
      const v = Reflect.get(target, key, value);
      // 此属性存在策略
      if (typeof optionMergeStrategies[key] === 'function') {
        return optionMergeStrategies[key](v)
      }
      return v
    }
  };
  return new Proxy(data, hander);
}

const proxy = reactive(data);
// 测试是否添加了响应
proxy._my_option = 10
proxy.option = 10

console.log(proxy._my_option, proxy.option); // 11 10
复制代码

这样你就可以做更多的事情了,比如验证手机号,邮箱等等,再也不用写很多的 if else 了,而且你也可以随时更换策略。符合了设计模式的开闭原则。

发布订阅者模式

定义

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。

优缺点

优点

  1. 观察者和被观察者是抽象耦合的。
  2. 建立一套触发机制。

缺点

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

实战

比如公众号,有多个人订阅,每天定时发送公众号文章

  1. 建立一个 Persen 类,用于创建人物(观察者/订阅着)
  2. 建立 Subject 类,用于建立与观察者之间的关系(被注入到观察者的依赖)
  3. 修改状态触发更新
// 公众号订阅者
abstract class Persen {
  abstract update(): void;
  protected subject: Subject;
}
// 状态
type state = 'await' | 'publish'
// 依赖
class Subject {
  private _state: state = 'await'
  // 依赖集合
  subs: Persen[] = [];
  // 防止频繁设置状态
  lock = false
  // 设置状态,如果是发布状态的话,就发布文章
  set state(state: state) {
    // 锁上之后就不能设置状态了,只有锁解开后才可以设置状态
    if (this.lock || (this._state = state) === 'await') return;
    this.lock = true;
    Promise.resolve().then(() => {
      this.notify();
      this.lock = false;
    });
  }
  // 获得当前状态
  get state(): state {
    return this._state
  }
  // 添加订阅
  attach(persen: Persen) {
    this.subs.push(persen)
  }
  // 通知更新
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}
// 创建一个 Tom
class Tom extends Persen {
  constructor(subject: Subject) {
    super();
    subject.attach(this)
  }
  update() {
    console.log('通知到了 Tom');
  }
}
// 创建一个 Jick
class Jick extends Persen {
  constructor(subject: Subject) {
    super();
    subject.attach(this)
  }
  update() {
    console.log('通知到了 Jick');
  }
}
// 实例化依赖
const subject = new Subject()

// Tom Jick 订阅公众号
new Tom(subject)
new Jick(subject)

// 因为设置了 lock 所以在一次 event loop 只会执行一次 
subject.state = 'publish'
subject.state = 'await'
console.log(subject.state) // publish
subject.state = 'publish'

setTimeout(() => {
  subject.state = 'publish'
}, 1000)

// 通知到了 Tom
// 通知到了 Jick
// 一秒后...
// 通知到了 Tom
// 通知到了 Jick
复制代码

装饰器模式

定义

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

一句话概括

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

优缺点

优点

  1. 更好的可读性
  2. 装饰类和被装饰类可以独立发展,不会相互耦合
  3. 装饰模式是继承的一个替代模式
  4. 装饰模式可以动态扩展一个实现类的功能。

缺点

  1. 多层装饰比较复杂。

实战

比如我们要点击一个按钮,但是这个按钮点击时我们想给他加上埋点并做一些登陆的逻辑

我这里使用了 es7 的语法糖,当然不用语法糖也可以做,但是我觉得用的话更简洁一些

// Button 类,内部有一个 click 方法
// 对click方法做了两个修饰
// 一个是添加埋点,一个是登陆
class Button {
  @BuridDecorator
  @LoginDecorator
  click() {
    console.log('点击 dom')
  }
}
// 登陆逻辑的装饰器
function LoginDecorator(target, name, descriptor) {
  const oriFun = target[name]
  descriptor.value = async function() {
    const code = await Login();
    if (code === 0) {
      console.log('登陆成功')
      oriFun.call(this, ...arguments)
    }
  }
}
// 设置埋点的装饰器
function BuridDecorator(target, name, descriptor) {
  console.log(`${name} 方法添加了一个埋点`)
}
// 登陆逻辑
async function Login () {
  return new Promise((resolve, reject)=> {
    setTimeout(() => {
      resolve(0)
    }, 1000)
  })
}
// 点击按钮
const btn = new Button()
btn.click();

// click 方法添加了一个埋点
// 登陆成功
// 点击 dom
复制代码

适配器模式

一句话概括

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

优缺点

优点

  1. 可以让任何两个没有关联的类一起运行。
  2. 提高了类的复用。
  3. 增加了类的透明度。
  4. 灵活性好。

缺点

  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。

实战

举一个例子,你要写一个页面兼容各个端的小程序,那么你就需要根据环境调用不同小程序的 sdk 方法。比如在支付宝中有一个 zhifubaoShare 的分享方法,在微信中有一个 weixinShare 的分享方法。(当然一个sdk还有很多方法,我们只拿分享来举例子)但是我们在工作中其实只希望调用一个 share 方法就能实现不同端的分享。下面我们用适配器模式来做一个 Adapter 适配器。

// =============== 定义接口与类型 ==============================
// 支付宝接口
interface ZhifubaoInerface {
  zhifubaoShare(): void;
}
// 微信接口
interface WeixinInterface {
  weixinShare(): void;
}
// adapter 接口
interface AdapterInterface {
  share(): void;
}
// 合并所有 sdk 类型
interface MergeSdk extends ZhifubaoInerface, WeixinInterface {}
// 支持的平台类型
type platform = 'weixin' | 'zhifubao';


// =============== 代码逻辑实现 ==============================
// 微信 sdk 类实现
class WeixinSdk implements WeixinInterface {
  weixinShare() {
    console.log('微信分享');
  }
}
// 支付宝 sdk 类实现
class ZhifubaoSdk implements ZhifubaoInerface {
  zhifubaoShare() {
    console.log('支付宝分享');
  }
}
// adapter 类实现
class Adapter implements AdapterInterface {
  constructor() {
    this.sdk = this.getPlatfromSdk();
  }
  // 挂载 sdk
  private sdk: MergeSdk;
  // 根据 ua 获取到平台
  private getPlatform(): platform {
    // 默认写了 weixin
    return 'weixin';
  }
  // 将所有 sdk 方法放进一个 map 里
  private getPlatfromSdk() {
    const map = {
      weixin: WeixinSdk,
      zhifubao: ZhifubaoSdk
    };
    const platform = this.getPlatform();
    return new map[platform]() as MergeSdk;
  }
  // 分享功能
  // 实际项目中还有参数的问题,这里为了代码的简洁就不写了
  public share() {
    const platform = this.getPlatform();

    switch (platform) {
      case 'weixin':
        this.sdk.weixinShare();
        break;
      case 'zhifubao':
        this.sdk.zhifubaoShare();
        break;
      default:
        console.log('此方法不存在');
    }
  }
}

const adapter = new Adapter();
// 因为我们默认设置了 weixin 平台
adapter.share(); // 微信分享
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改