专栏首页前端技匠设计模式ts实战系列(上)

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

设计模式概念

如果我们把代码编程比作是战争的话,那么设计模式就是兵法。

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

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

设计模式原则

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

本文主要讲的设计模式

  • 单例模式
  • 工厂模式
  • 享元模式
  • 策略模式
  • 观察者模式
  • 适配器模式
  • 装饰器模式

单例模式

特点

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

一句话概括

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

优缺点

优点

  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(); // 微信分享

本文分享自微信公众号 - 前端技匠(gh_057b814fc3f7),作者:罗学

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 实战设计模式系列-Strategy(策略)

        项目最近需要写一个逻辑srv,srv的业务逻辑比较简单,收包、解包、根据命令字进行业务处理、回包。 考虑每一次请求都是一项任务,而逻辑srv是一个任务管...

    王亚昌
  • 实战设计模式系列-State(状态)

            state模式中,我们将状态逻辑和动作实现进行分离,当一个操作中要维护大量的case分支语句的时候,并且这些分支也都要依赖于对象的状态时,sta...

    王亚昌
  • 实战设计模式系列-Singleton(单件)

        单件模式的应用场景大家都不陌生,目的也很明确,就是一个类保证只有一个实际,比如项目中的资源管理器,或打log的类,都比较适合单件模式,话不多说,先贴一段...

    王亚昌
  • 实战设计模式系列-Facade(外观)

    项目在做数据迁移时,因为数据结构从FormatOld调整成了FormatNew,所以对数据的读取方式发生了变化,而迁移过程持续比较久,所以在读取数据时需要对两种...

    王亚昌
  • 实战设计模式系列-AbstractFactory(抽象工厂)

        假设我们需要写一个迷宫游戏,游戏支持用户自由选择游戏的场景,比如可以选择普通的迷宫,或者是有魔法的迷宫等。但所有的迷宫都有一些共同的元素,包括墙壁、门、...

    王亚昌
  • iOS设计模式系列:单例设计模式

    单例设计模式从字面意思上来说,就是一个类在系统运行时,只创建一个实例。可以用于需要被多次调用的或者多次使用的资源中。比如我们常见的网络请求类、工具类等等。

    Originalee
  • 设计模式实战-适配器模式,承上启下

    适配器,其实很好理解,生活中也随处可见,比如电源适配器、usb 适配器等等,那么适配器模式,也被称为Wrapper 模式。

    架构师修炼
  • 设计模式系列 - 单例模式

    我不知道大家工作或者面试时候遇到过单例模式没,面试的话我记得我当时在17年第一次实习的时候,就遇到了单例模式,面试官是我后来的leader,当时就让我手写单例,...

    敖丙
  • 设计模式系列| 代理模式

    代理模式是一种结构型设计模式,让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。

    狼王编程

扫码关注云+社区

领取腾讯云代金券