我常用的设计模式

2,209 阅读32分钟

前言

  本文主要是分享关于设计模式的相关内容,对于设计模式,想必大家应该都不陌生,像单例、发布订阅、工厂等设计模式应该都很熟悉了,应该也知道怎么实现这几个常见的设计模式,毕竟面试的时候经常会被问到,但是自己在项目开发中可能不太会应用设计模式,if elsefor循环一把梭,照样能把项目跑起来,但是这样也容易因为设计缺陷、很多地方不合理性对后期的迭代、拓展等带来一定的困难。这种代码写多了,对自己的成长也没有太大的帮助,更多的是在重复,人总是要有点成长。了解设计模式,能帮助自己提升自己的代码设计能力,接触的更多,思考面也会更广。

  设计模式这块对于大多数同学而言,可能就只是将其当作理论知识来看,面试才会用到的八股文。是的,较之前,我也是这么看待设计模式的,设计模式对我而言看山是山,看水是水,看山又不是山,看水又不是水,总之就是觉得深不可测,用起来就是不会,只停留在印象中,好像有这个东西。随着工作经验的积累,接触的业务增加,我们需要思考如何写出健壮性、拓展性强的代码,设计合理的代码结构。设计模式会是一个比较好的方向,合理应用能帮助我们做出更好的代码设计、也能提供更多的解决问题的思路。

举例

  在和大家分享自己常用到的设计模式之前,和大家介绍一个常见的设计模式的应用。

  上面这张图大家应该都很熟悉了,这就是我们在项目中常用到的mvc架构,mvc架构的设计本身就是多种设计模式的组合,主要通过组合模式、策略模式和观察者(或者发布订阅模式)这三种设计模式的应用而生成,下面简单的讲讲这三种设计模式在mvc架构下如何应用:

  mvcmodel(数据模型)、view(视图)、controller(控制器),mvc架构设计的提出目的就是将视图界面和业务逻辑分离,从而达到代码的可拓展性、复用性、可维护性等,降低相应的业务耦合度。

  view层,本身实现了组合模式,组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构,也就是说组合模式的结构层次是树形结构,而html页面结构本身就是树形结构,这应该就是组合模式的应用,只是我们无感知,浏览器帮我们做好了这件事情。在我们的业务中,组合模式也能解决些问题,如优化处理递归,大部分树结构问题都能用递归来处理,我们也可以尝试的思考是否能用组合模式来处理问题。

  在mvc中,viewmodel是观察者和被观察者的关系,也就是观察者模式中两个角色:观察者、被观察者。model发生变化,view会被通知做相应的视图更新,但是modelview不会产生直接关联,当model中的数据发生变化,不需要model主动将数据的变化的消息推送给view,而是通过controller来处理。通常model层的改变都是由用户的操作导致,用户的操作通过controller来处理,当controller改变model时,也是controller将更新的信息发送给view。在一定程度上将,这更多是在做发布订阅。发布订阅模式和观察者模式核心思想都是一样的。

  策略模式的应用主要是在viewcontroller中,viewcontroller是一对多的关系,controllerview的一个策略,controller对于view来说是可以替换的。

  在很多大型项目(包括很多web项目)中都会采用mvc架构设计,或者借鉴mvc架构思想。当你熟悉了设计模式的应用,你就会发现各种架构设计其实是多种设计模式的组合,架构设计其实并不是很神秘,也并不是触不可及的东西。设计模式在项目中的应用挺常见,有时候看不懂项目架构,或者不知道如何设计代码更合理,可能自己接触面太窄了,限制了自身的思考。

  写了这么多,还没具体的说设计模式的相关内容,上述的内容想表达的目的是我们可以尝试在实际业务中思考设计模式,mvc架构是一种很常见的项目架构方式,我们可能主要关心在其应用层面上:我们怎么使用这种架构,其架构能解决什么问题。可能不会从架构搭建的角度思考,这种架构是怎么搭建的,设计模式是如何应用在该架构中的。是的,我也没有思考过这些问题,现在我们需要思考这些问题,多思考架构背后的东西,八股文可能就不是八股文了。思考这种东西挺难的,我也不太会,多看多学习了。

设计模式定义

  什么是设计模式,专业解释是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。人话是,设计模式是在某种场合对某个问题的一种解决方案。它不是语言语法,是行业的大佬们对代码开发经验的总结,各语言都适用。我们可以把设计模式理解为解决某些特定问题的一系列模版,也就是说设计模式是能套用的。

  设计模式被总结出来的目的就是能够快速帮助我们解决程序设计中的某些问题,例如对象的创建、对象之间的通信,对象功能的拓展等等,这些问题的解决我们都能在被总结的设计模式中找到解决方案。但是在实际的开发中,我们也需要根据实际的需要做选择,如果是简单的程序,可能写一个简单的代码比引入某种设计模式更加容易,不要过度设计。对于大项目或者框架设计,例如mvc架构,用设计模式来组织代码显然更好。

面向对象

  设计模式被提出是基于面向对象,我们在使用设计模式时不要脱离了面向对象这个大前提,所以我们在了解各种设计模式前,我们应该了解面向对象的思想,设计模式的本质是面向对象的五大设计原则实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

  面向对象三大特性:封装、继承、多态。

  面向对象五大设计原则:开闭原则、里氏替换原则、依赖倒置原则、单一职责原则、接口隔离原则。

  大家对面向对象的概念应该都有了解以及应用,但是应该也有同学对面向对象的特性和设计原则不是很熟悉,我建议大家在接触设计模式之前,先对面向对象的特性和设计原则有充分的了解,会对设计模式的理解会有比较大的帮助,设计模式本身就是应用了面向对象的特性和设计原则中的某一个或者某几个,所以当你吃透了面向对象的特性和设计原则,设计模式也是很简单东西,并没有什么神奇的东西。

  在这里不会详细描述面向对象的特性和设计原则,建议大家去看看这些内容,我会在具体的设计模式中介绍该设计模式应用了哪些特性和设计原则。

  下面主要分享我常用到的设计模式:单例模式、工厂模式、策略模式、发布订阅模式。

单例模式

  全局变量,大家应该都使用过,很好用。定义全局变量,项目各个地方都能使用,虽然好用,但是缺点也非常的明显,javaScript这种弱类型语言,变量和方法都暴露在全局中,很容易出现变量冲突,变量就会有被覆盖的风险,应该尽量避免使用全局变量。单例模式能解决这种问题。

定义

  指一个类仅有一个实例,并提供一个访问它的全局访问点。简单的讲就是一个类只能创建一个实例,即使创建了多个实例,这些实例都是相同的。不会出现被覆盖的问题,保证只有一个实例,并提供全局访问。

  实例单例模式主要有两种方式,一是通过类的静态方法创建类实例,二是new操作符创建类实例。

静态方法

class Singleton {
    constructor() {
        this.name = 'singleton';
    }
    static getInstance() {
      if (!Singleton.instance) {
          Singleton.instance = new Singleton();
      }
      return Singleton.instance;
    }
}
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

  通过静态方法创建类实例,不够透明,因为是通过静态方法创建的类实例,约束了实例化的调用方式,对于使用者的来说增加了使用成本,需要阅读代码才知道该类是单例类,需要通过调用静态方法创建类实例,否则依然会通过new操作符创建类实例,这就失去了单例的价值,实际意义不大。

  耦合度过高,不符合面向对象的单一职责原则,单例的维护和对象的创建耦合在一起。

new操作符

class Singleton {
    constructor() {
        if (Singleton.instance) {
          return Singleton.instance;
        }
        Singleton.instance = this;
        this.name = 'singleton';
    }
}
let instance1 = new Singleton();
let instance2 = new Singleton();
console.log(instance1 === instance2); // true

  通过new操作符方式创建类实例,解决了静态方法关于不够透明的问题,但是依旧耦合度过高,不符合面向对象的单一职责原则,单例的维护和对象的创建耦合在一起。

代理方式

// 业务类
class CreateSingleton {
    constructor() {
        this.name = 'singleton';
    }
}
// 单例类
class Singleton {
    constructor(Target, ...args) {
        if (!Target.instance) {
            Target.instance = new Target(...args);
        }
        return Target.instance;
    }
}
let instance1 = new Singleton(CreateSingleton);
let instance2 = new Singleton(CreateSingleton);
console.log(instance1 === instance2); // true

  通过代理的方式解决了静态方法和直接new操作符创建单例的类实例的问题,一符合面向对象的“单一职责原则”,不同的对象承担独立职责,不过于紧密耦合,业务类只负责该类本身的职责,单例类也只负责创建业务类的单例对象,具体执行功能还是本体的对象。通过代理的方式,比较好的做到了业务解耦。

  同时代理方式常见实例对象也遵循了面向对象的“依赖倒置原则”,实例中CreateSingleton的实例的创建,通过依赖注入的方式创建实例对象,避免了单例类与单例类的强关联性,实现了单例类的通用性,可以创建不同业务类的单例对象,提高了代码的拓展性以及易于维护。

  代理方式是通过代理模式来实现单例模式,单例对象的创建是代理模式和单例模式的组合应用。

经典应用

  vuex,大家应该都比较熟悉了,vue的状态管理模式,采用集中式存储管理应用的所有组件的状态,利用一个全局的Store存储应用的状态,提供api供用户做读写操作。不同的页面都能访问该Store对象,获取的值也是一样的,实现了不同页面数据保持同步。其本质上的实现就是通过单例模式。vuex的实现是单例模式的经典应用。

  下面我们来看看vuex具体的实现:

// main.js
import Vue from 'vue';
import Vuex from 'vuex';
import store from './store';
Vue.use(Vuex); // 安装vuex插件
new Vue({
    store, // store注入到Vue实例中
    el: '#app',
});

  上面是vue的入口文件,通过Vue.use()方法安装Vuex插件(该方式符合依赖倒置原则,通过依赖注入的方式将vuex对象注入到vue中,降低了vue主体和vuex的耦合度),我们先看看Vue.use()的实现:

// vue/src/core/global-api/use.js
export function initUse (Vue: GlobalAPI) {
    // 接受一个plugin参数可以是Function或者Object
    Vue.use = function (plugin: Function | Object) {
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        // 判断该插件是否以及注册过,防止重复注册
        if (installedPlugins.indexOf(plugin) > -1) {
            return this
        }

        // additional parameters
        const args = toArray(arguments, 1)
        args.unshift(this)
        // 执行install方法,将Vue作为实例传入
        if (typeof plugin.install === 'function') { // plugin为对象,执行内置的install方法
            plugin.install.apply(plugin, args)
        } else if (typeof plugin === 'function') { // plugin为函数,将其作为install方法
            plugin.apply(null, args)
        }
        // 记录plugin已经注册过
        installedPlugins.push(plugin)
        return this
    }
}

  Vue.use()本质是执行插件的install方法,install方法由插件自定义,在Vue实例化前将插件提前安装完成。

  我们再来看看Vuex插件的install实现:

// vuex/src/store.js
let Vue; // bind on install

...

export function install (_Vue) {
  // 是否已经执行过了 Vue.use(Vuex),如果在非生产环境多次执行,则提示错误
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 如果是第一次执行 Vue.use(Vuex),则把传入的 _Vue 赋值给定义的变量 Vue
  Vue = _Vue
  // Vuex 初始化逻辑
  applyMixin(Vue)
}

  从上面的实现可以看到,Vuex内部定义了变量Vue,在执行Vue.use(VueX)时,本质上是执行install方法,Vue会被当参数传入,并用变量Vue缓存Vue对象,如果多次执行Vue.use(VueX),也只会生效一次,applyMixin(Vue)也只会被执行一次,也就是说Store全局只有一份。这也就是Vuex的单例模式的实现。

  单例模式虽然挺好用的,但是也别滥用,像Vuex是一个单例,不要在项目中滥用Vuex,因为Vuex能够被全局各个地方操作修改,如果项目是多人协作开发的话,Vuex中的数据很容易被多人同时修改,大家都有修改Vuex的权限,很容易带来问题,我们需要尽量避免使用全局操作的东西,如果必须要用全局操作的数据,需要制定好项目开发规范。

工厂模式

  工厂模式也是一种创建型设计模式,也比较常用,大多数人应该都用过或者听说过工厂模式,工厂模式能解决什么问题呢?大家有没有思考过这个问题,为什么使用工厂模式?

  现在可以思考这么一个场景:用户A对系统只有查看功能,用户B对系统有查看、新增、删除等功能,不同的角色用户用不同的权限功能,这种场景怎么处理?我们看看比较常见的一种处理方式:

class UserA {
    constructor(name){
        this.name = name;
        this.skills = ['查看'];
    }
    getName() {
        return this.name;
    }
}
class UserB {
    constructor(name){
        this.name = name;
        this.skills = ['查看', '新增', '删除'];
    }
    getName() {
        return this.name;
    }
}
let userA = new UserA('小a');
let userB = new UserB('小b');

  从上面的实例解释:对于只有查看功能一类的用户,我们定义了一个UserA类,对于有查看、新增、删除等功能的一类用户,我们定义类一个UserB类,当我们在业务应用的时候,如果判断某用户属于UserA类的用户时,我们可以通过实例化UserA创建该类用户对象,如果判断出某用户属于UserB类用户,实例化UserB创建该类用户对象。这样处理,明显也能实现功能,但是这种处理方式是否有没有不合理的地方???

  既然这么问了,当然是有不合理的地方,可以尝试的思考下,某System类中通过new的方式实例化了UserA类,System类和UserA类产生了之间的关联,这就业务产生了耦合,后期需要修改UserA类实例化方式,需要增加入参数量,那么UserA类要修改,同时System类也需要修改,既然UserA类需要修改,同样的UserB类也需要修改,如果多个类依赖了UserA类和UserB类,那么这个修改的工作量巨大,而且还容易出错,比较大的可能性出bug,这种代码可维护性就极差,耦合性也很强。

  UserA类和UserB类的功能行为极其的相似,只是它们的skills的值不同,其实也没有必要用多个类创建不同的角色用户,代码重复率太高了。

  我们需要将对象的调用和创建分离,System类中不直接创建User类实例对象,通过某种方式做隔离,对外抛出统一接口,内部完成各种角色对象的创建,业务侧不关心相关对象的创建,只负责调用即可。

  工厂模式能解决上述的问题,工厂管理对象的创建逻辑,调用方不用关心具体的创建过程,只负责调用,通过入参决定使用哪种对象。工厂模式比较好的做到了相关业务的解耦,对象的实例化工作集中在一处,降低代码的重复率和出错率。

  写到这里,大家应该比较清楚了工厂模式大致的应用场景是什么,也就知道了能解决什么样的问题,下面我们来看看工厂模式具体怎么定义和使用的:

定义

  从应用层来说,工厂就是一个函数,对外抛出使用,对象的创建逻辑都封装在这个函数中,不暴露创建对象的具体逻辑。简单的说,由工厂完成对象的创建,通过函数调用使用对象。目的就是为了把对象的创建和使用相分离。

  按照实例业务场景划分,工厂模式分为三种:简单工厂模式、工厂方法模式和抽象工厂模式。常用到的是简单工厂模式。

简单工厂模式

  简单工厂模式,又叫静态工厂模式,创建实例的方法通常是静态方法(也可以不用静态方法,只是一个形式而已)。简单工厂模式,有一个具体的工厂类,这个工厂类可以生成多个不同的产品,产品的创建由这个工厂类本身完成:

// 角色常量
const ROLE = {
    USER: 'user',
    ADMIN: 'admin',
}
// 用户用户类
class User {
    constructor(options) {
        let { role, name, skills } = options;
        this.role = role;
        this.name = name;
        this.skills = skills;
    }
    show() {
        console.log(`${this.name}用户拥有功能有:${this.skills.join(', ')}`);
    }
}
// 简单工厂类
class SimpleUserFactory {
    // 创建不同用户对象的静态的方法
    static create(options, Target) {
        let { role } = options;
        switch (role) {
            case ROLE.USER:
                return new Target({
                    skills: ['查看'],
                    ...options,
                });
                break;
            case ROLE.ADMIN:
                return new Target({
                    skills: ['查看', '新增', '删除'],
                    ...options,
                });
                break;
            default:
                break;
        }
    }
}
// 使用用户
let userA = SimpleUserFactory.create({role: 'user', name: '小白'}, User);
userA.show();
let userB = SimpleUserFactory.create({role: 'admin', name: '小李'}, User);
userB.show();

  上面的实例,就是一个简单工厂模式的应用,SimpleUserFactory就是一个简单工厂,两个用户对象拥有不同的功能,通过role来判断不同行为的用户,进而获取对象的实例对象。并且也可以通过传参的方式传入共有字段数据(例如name字段)。

  简单工厂模式也有不足的地方,如果需要新增产品时,需要修改工厂方法,代码会变的非常臃肿,违背来面向对象的“开闭原则”,而且工厂类单一,一个工厂类只能创建相同类型的产品,如果需要新增其他类型的产品需要新增一个具体的工厂类和对应的具体的产品类。

工厂方法模式

  工厂方法模式,其实就是对简单工厂模式进一步的抽象化,将实际创建对象工作推迟到子类中,核心类为抽象类,也就是说SimpleUserFactory只负责对象的创建,新增产品交给抽象类去做,SimpleUserFactory继承抽象类,这样就遵循了“开闭原则”。其本质就是利用了继承。

// 角色常量
const ROLE = {
    USER: 'user',
    ADMIN: 'admin',
}
// 用户产品类
class User {
    constructor(options) {
        let { role, name, skills } = options;
        this.role = role;
        this.name = name;
        this.skills = skills;
    }
    show() {
        console.log(`${this.name}用户拥有功能有:${this.skills.join(', ')}`);
    }
}
// 抽象工厂类
class AbstractSimpleUserFactory {
    constructor() {
        throw new Error('抽象类不能实例');
    }
    admin((options, Target) {
       return new Target({
           skills: ['查看', '新增', '删除'],
           ...options,
       });
    }
    user((options, Target) {
        return new Target({
           skills: ['查看'],
           ...options,
       });
    }
}
// 具体工厂类
class SimpleUserFactory extends AbstractSimpleUserFactory {
    constructor() {
        super();
    }
    static create(options, Target) {
        let { role } = options;
        this[role]();
    }
}
let userA = SimpleUserFactory.create({role: 'user', name: '小白'}, User);
userA.show();
let userB = SimpleUserFactory.create({role: 'admin', name: '小李'}, User);
userB.show();

  上面的实例就是对工厂方法模式的应用,SimpleUserFactory类只负责实际对象的创建,新增产品类交给AbstractSimpleUserFactory类做,在一定程度上SimpleUserFactory类遵循了“开闭原则”。

抽象工厂模式

  简单工厂模式和工厂方法模式本质上都是工厂直接生成实例对象,单个工厂类只能创建同类的产品,如果需要新的别类产品,需要新增工厂类创建新类的产品对象,不具备通用性,抽象工厂模式不直接生成实例,而是生成具体工厂,然后有具体工厂创建相应的产品类。在这里不对抽象工厂模式做具体的介绍,自己在业务中用的不多。

  工厂模式在一定程度上能解决部分对象的创建和使用的问题,但是不是所有的对象的创建都适合使用工厂模式,工厂模式比较适合复杂对象的创建,生成某一类的对象。但是很多对象相对来说都是简单的对象,不需要经过那么多的业务逻辑才创建出对象。简单对象在多个地方使用,如果后期需要修改构造函数的参数的话,依旧会带一开始说到问题。需要一种通用的解决方案来处理对象的创建和使用问题,下面会介绍一种常见的解决方案。

创建型模式总结

  单例模式、工厂模式都是创建型模式,创建型模式还有建造者模式和原型模式。创建型模式的主要关注点是“怎样创建对象”,主要的特点是“将对象的创建和使用分离”,进而降低业务的耦合度,使用者不用关心对象的创建细节。但是不是所有的对象的创建都适合用这些创建型设计模式去处理,我们需要思考哪种场景适合哪种创建方式,不要滥用设计模式。

  这里讲到创建型设计模式,有些非“设计模式”的东西在这里也可以和大家唠唠,也和对象有关,我觉得可以帮助大家更好的理解并使用创建型设计模式,大家在工作也有可能会遇到。

  创建、使用对象是程序中关键步骤,但是使用方式不合理的话,也可能会带来灾难性的修改。

  如果A类依赖B类,常用的使用方式是:A文件中importB类,A类中实例化B类。这种方式是一种比较常见的使用方式,如果业务不复杂的话,也不会有什么问题。但是随着业务的复杂化,项目中存在着大量的业务对象,它们之间有着复杂的依赖关系,如果通过上述说的A类直接依赖B类方式保持依赖关系,会导致模块之间很容易出现循环引用,也比较容易出现如果B类实例化参数有变化,B类需要修改,A类创建B类实例的地方需要修改,如果多个类依赖了B类,那么修改的地方很很多了,这显然这种设计很不合理。

  A类直接依赖B类,这种设计本身就没有遵循面向对象的“依赖倒置原则”,工厂模式在一定程度上解决上述的部分问题,但是工厂模式并不适合各类型的对象创建。当项目中拥有比较多的业务对象时,各个类之间紧密耦合,每个类除了之间依赖类,还会依赖这些依赖类的依赖类,照此往复,类之间的关系就会变的异常的复杂。并且创建对象的代码充斥在项目中各个地方,如果类的构造函数有变动,那么就需要修改用到该类的各个地方。

  自己所在的团队在项目中也遇到了上述问题,一旦项目复杂起来,就一定会遇到上述的问题。我们需要思考如何解决这种业务对象创建和使用的问题,需要一种方式管理业务对象,包括对象的创建和获取。也就是说通过某一方式,收起对象的创建和其使用的权限,通过统一的入口创建、引用对象,避免类之间的直接引用。行业内都这类问题也有实用的解决方案:IoC容器。

  IoC容器,增加了一层容器的概念,意思就是容器作为各个业务对象的托管地,容器负责对象的创建,如果需要使用某对象,从容器中取某对象,不要直接实例化某对象。容器为对象创建和引用的入口。IoC容器,本质上是利用了"依赖倒置"的思想,也就是控制反转,通过依赖注入的方式解决了类之间的直接引用的问题,类与类之间做了解耦。通过容器做统一管理,解决了某类的构造函数如果有变动,不需要修改用到该类的各个地方,只需要在容器这一个地方修改就可以了。

  IoC容器,在应用层上一定程度内解决了模块之间的循环引用的问题,但是不能从根本上解决循环引用,因为循环引用的出现是在代码设计层级出现了问题,代码写的有问题,就有可能出现模块之间的循环引用问题,利用IoC容器只能尽量避免循环引用的问题出现。

  利用IoC容器,能对项目的所有对象做统一管理,减少类之间的连接,能进一步提高代码的复用性,也增强了代码的可维护性,提高项目的代码组织结构。

  在这里就不对IoC容器做过多的描述了,内容也挺多的,感兴趣的同学可以自行了解,也是一种关于对象创建和使用的解决方案,前端应用的不多,在后端应用挺广泛,前端也能借用这种思想,设计符合自己业务场景的IoC容器。

策略模式

  从上面的工厂模式的实例代码中,大家应该能感受到代码的设计并不是很好,甚至会觉得臃肿,因为在静态方法crate中通过switch...case...或者if...else...这种方式处理创建对象的逻辑,所有的业务逻辑都耦合在一个function中,如果新增产品的话,还是继续在这个function中添加新的业务逻辑,这种行为在修改函数逻辑,这明显没有循序面向对象的“开闭原则”,修改代码就会带来出问题的风险,我们需要避免修改代码。是否有什么方式可以优化工厂模式的实例代码,遵循“开闭原则”?

  很明显,这里讲到策略模式,策略模式能解决上述问题,我们来看看策略模式:

定义

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

  上面官方的定义应该有点抽象,我们说人话解释下:

  策略模式的核心思想就是:寻找业务中的不变的部分和变化的部分,把不变的隔离,可变的封装起来。策略模式的目的就是将算法的使用和算法的实现分离开。这种就是策略模式。

  从上面的定义来看,一个基于策略模式的代码至少由两部分组成,一是策略类,策略类封装具体的算法,并负责具体的计算过程。二是环境类ContextContext接受客户的请求,随后把请求委托给某策略类做处理,Context需要维持对某策略对象的引用。

  也就是说策略模式将程序拆成两部分,由策略类将变化的封装起来,环境类不变,保持对策略类的引用,策略类做功能的增强,这样就避免了修改代码实现功能增强,遵循了“开闭原则”,下面我们看看如何使用策略模式优化工厂模式的实例:

const ROLE = {
    USER: 'user',
    ADMIN: 'admin',
}
// 产品类
class User {
    constructor(options) {
        let { role, name, skills } = options;
        this.role = role;
        this.name = name;
        this.skills = skills;
    }
    show() {
        console.log(`${this.name}用户拥有功能有:${this.skills.join(', ')}`);
    }
}
// 策略对象
let userStrategy = {
    [ROLE.USER]: (options, Target) => {
        return new Target({
           skills: ['查看'],
            ...options
        });
    },
    [ROLE.ADMIN]: (options, Target) => {
        return new Target({
           skills: ['查看', '新增', '删除'],
            ...options
        });
    },
}
// 抽象工厂类
class AbstractSimpleUserFactory {
    constructor(options, Target) {
        throw new Error('抽象类不能实例');
        let { role } = options;
        this.options = option;
        this.Target = Target;
        this.role = role;
    },
    // 保持对策略对象的引用
    [this.role]() {
        return userStrategy[this.role](options, Target);
    }
}
// 具体工厂类
class SimpleUserFactory extends AbstractSimpleUserFactory {
    constructor(options, Target) {
        super(options, Target);
        let { role } = options;
        return this[role]();
    }
}
let userA = new SimpleUserFactory({role: 'user', name: '小白'}, User);
userA.show();
let userB = new SimpleUserFactory({role: 'admin', name: '小李'}, User);
userB.show();

  上面的代码就是用策略模式对工厂方法模式的优化实现,通过userStrategy策略对象封装了不同的用户对象的创建,将用户对象的创建功能从业务主体中抽离出来,它们是变化的部分,如果有新增类型用户对象通过策略对象拓展,主体函数内保持对策略对象的应用,在SimpleUserFactory使用时,通过不同的参数使用策略对象中某一算法做处理。

  策略模式完美遵循“开闭原则”,将算法封装在策略类中,使得它们易于切换、拓展等,避免了将所有的业务逻辑都堆砌在Context中,减少了在Context中使用switch...case...或者if...else...,其本质是利用了面向对象的组合和委托的特性,策略类通过组合将不同的算法封装起来,环境类委托策略类做相应的业务逻辑处理。

  现在了解了策略模式,现在可以思考下在mvc架构中,viewcontroller的关系。

  和策略模式有相似的思想还有状态模式,但是意图上还是有很大的区别,感兴趣的同学可以了解状态模式。

发布订阅模式

  发布订阅模式应该是大家最熟悉的一种设计模式,应用太广泛了,应该不用做过多的解释。发布订阅模式主要是解决多个对象之间的通信问题,在实际业务中使用,需要对象并不是独立存在的,其中一个对象的行为发生变化可能会导致一个或者多个其他对象的行为也发生改变。

  例如,微信公众号,如果我们想查看某公众号的文章,我们通过会关注该公众号,当公众号发布了新的内容,会往订阅号消息推送,订阅号消息会及时通知我们该公众号有新的更新,这样我们就能阅读公众号发布的新内容。

  公众号的应用模式本质上就是一个发布订阅模式,公众号为发布者,用户为订阅者,订阅号消息为调度中心。用户将订阅公众号的时间注册到调用中心,公众号发布新的内容时,公众号将发布该事件到调度中心,由调度中心及时发消息通知用户。

  说到发布订阅模式,观察者模式就不得不提,两种设计模式本质上是一样的,区别在于,发布订阅者模式中发布者和订阅者之间存在第三者,也就是调度中心,发布者和订阅者之间没有耦合,发布者不用关心我有哪些订阅者,订阅者也不用关心我订阅了哪些发布者,统一由调度中心做信息内容的调度,发布者只负责往调度中心推送,订阅者只需要接受调度中心的通知获取信息,两者之间不用关心对方是谁。发布订阅模式做到了对象之间的解耦。

  而观察者模式,没有所有的第三者,也就是没有调度中心的存在,是观察者和被观察者的之间的直接接触,达到的目的和发布订阅模式一样,但是存在对象之间的关联性。灵活性不如发布订阅模式。

  发布订阅模式在Vue中有着比较强的应用,Vue2.x的响应式原理以及eventBus都是通过发布订阅模式实现的,下面我们来看看vueeventBus的实现:

// vue/src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
    const hookRE = /^hook:/
    // vm.$on实现 事件监听
    // event参数类型为字符串或者字符串数据 fn执行回调
    Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this
        // event类型为数组
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                // 递归并传入相应event事件的回调fn
                this.$on(event[i], fn)
            }
        } else { // event类型为字符串
            // 如有_events对象中有相应的event值,把fn添加到对应event值的缓存列表里
            // 如果对象中没有对应的event值,也就是说明没有订阅过,就给event值创建个缓存列表
            (vm._events[event] || (vm._events[event] = [])).push(fn)
            // optimize hook:event cost by using a boolean flag marked at registration
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true
            }
        }
        return vm
    }
    // vm.$once 事件监听一次
    Vue.prototype.$once = function (event: string, fn: Function): Component {
        const vm: Component = this
        // 先绑定,调用完成后删除
        function on () {
            vm.$off(event, on)
            fn.apply(vm, arguments)
        }
        on.fn = fn
        vm.$on(event, on)
        return vm
    }
    // vm.$off 事件取消订阅
    Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
        const vm: Component = this
        // all 未传参 清空所有的事件
        if (!arguments.length) {
            vm._events = Object.create(null)
            return vm
        }
        // event为数组,递归依次清空事件回调
        // array of events
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$off(event[i], fn)
            }
            return vm
        }

        // event为字符串,当前event的fn集合
        // specific event
        const cbs = vm._events[event]
        // 没有相应的fn返回vm实例
        if (!cbs) {
            return vm
        }
        // 如果没有传fn,清空event值对应的fn集合
        if (!fn) {
            vm._events[event] = null
            return vm
        }
        // fn存在 遍历对应的fn集合,找到与传入的fn相同的回调,从集合中删除
        if (fn) {
            // specific handler
            let cb
            let i = cbs.length
            while (i--) {
                cb = cbs[i]
                if (cb === fn || cb.fn === fn) {
                    cbs.splice(i, 1)
                    break
                }
            }
        }
        return vm
    }
    // vm.$emit 事件发布
    Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        if (process.env.NODE_ENV !== 'production') {
            const lowerCaseEvent = event.toLowerCase()
            if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                `Event "${lowerCaseEvent}" is emitted in component ` +
                `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
                `Note that HTML attributes are case-insensitive and you cannot use ` +
                `v-on to listen to camelCase events when using in-DOM templates. ` +
                `You should probably use "${hyphenate(event)}" instead of "${event}".`
                )
            }
        }
        // 当前event中的fn列表集合
        let cbs = vm._events[event]
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs
            const args = toArray(arguments, 1)
            // 依次执行传入的回调
            for (let i = 0, l = cbs.length; i < l; i++) {
                try {
                    cbs[i].apply(vm, args)
                } catch (e) {
                    handleError(e, vm, `event handler for "${event}"`)
                }
            }
        }
        return vm
    }
}

  上面就是vue中对eventBus的实现,可以参考上面的注释,实现其实也很简单,在vm实例的_events中缓存了各event的回调,通过$on方法收集event的回调,$emit做事件发布,触发回调执行,$off事件移除。

  发布订阅模式,优势很明显,能做到时间上解耦,能做异步编程。对象之间也能解耦,利用发布订阅模式做数据通信。我们再回过头看看一开始说的mvc架构中,modelview之间的通信就是利用了发布订阅模式,现在应该比较好理解了吧。像我们常用到的vue,实现基于mvvm架构,数据变化触发视图更新,底层也是通过发布订阅模式实现的,发布订阅模式在各种架构中是一个很重要的设计模式,几乎都是利用该模式做通信。

行为型模式总结

  策略模式和发布订阅模式都被归类为行为型设计模式,行为型模式就是用于描述程序在运行时的复杂的流程控制。简单的说就是描述多个类或者对象之间怎样相互协作共同完成单个对象无法单独完成的任务。主要是利用了继承或者组合等特性来处理问题,策略模式就是面向对象组合的经典应用,模版方法模式是继承的经典应用。行为型模式除了这篇文章写道的两种设计模式,还有很多其他的设计模式,大家可以了解,根据不同的业务场景,使用合适的设计模式。

总结

  本文只讲到了四种设计模式,包括两种设计模式类型:创建型、行为型模式,还有一种结构型设计模式本文没有讲到。大家可以自行了解。设计模式太多了,不太可能所有的设计模式都写一遍,也有很多设计模式我也没用过,写这篇文章的目的也不是写一篇关于设计模式的教程文章,有些东西是自己在接触设计模式的同时,额外学习到的东西,把内容以文字的形式做输出,帮助自己梳理相关内容。

  学习这种相对较偏理论的内容,在实际业务中学习效果会更明显些,多思考这种技术出现背后的的驱动力到底是什么,为什么会出现这个技术,能解决什么问题,是否有其他的不足的地方。像设计模式的本质就是面向对象,面向对象的本质又是什么,三大特性五基本原则,明白这些内容,设计模式的神秘感也就没了。纯理论的学习,可能会是看山是山,看水是水。

  设计模式就写到这了,还有很多内容自己也在进一步学习,多累积,多实践。