# 👉 设计模式

设计模式定义:在软件设计中针对不同场景问题的解决方案,使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。

设计模式的核心思想,就是“封装变化”。何为“封装变化”:

这一点相信大家不难理解——如果说我们写一个业务,这个业务是一潭死水,初始版本是 1.0,100 年后还是 1.0,不接受任何迭代和优化,那么这个业务几乎可以随便写。反正只要实现功能就行了,完全不需要考虑可维护性、可扩展性。但在实际开发中,不发生变化的代码可以说是不存在的。

我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码,它可以经得起变化的考验。而设计模式出现的意义,就是帮我们写出这样的代码。

设计模式主要有 23 种,分为三大类型为:创建型模式、结构型模式、行为型模式。

这里就不一一详细展开,主要用作简要记录常用的设计模式。

# 创建型模式

# 工厂模式

说到工厂模式之前,看看构造器是什么:

// 场景1:一个公司需要录入很多员工信息
function User(name, age, career) {
    this.name = name;
    this.age = age;
    this.caree = caree;
}

const user1 = new User(name, age, career);

User 就是一个类的构造器(构造函数),构造器将 name、age、career 赋值给实例对象的过程封装,确保了每个对象都具备这些属性,确保了共性的不变,同时将 name、age、career 各自的取值操作开放,确保了个性的灵活。

构造器模式的作用就是去抽象每个实例对象的变和不变;而工厂模式也有点类似意思,不过主要是抽象不同构造函数的变和不变。

那什么是工厂模式?我们往下看看应用场景

// 场景2:在场景1的基础上,需要对员工分类及其主要职责进一步分类赋值,程序员主要工作是敲代码,产品经历主要工作是催更,老板主要工作见客户
// 提炼不同类的共性
function User(name, age, career, work) {
    this.name = name;
    this.age = age;
    this.caree = caree;
    this.work = work;
}

// 对不同类的个性进行划分
function UserFactory(name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work =  ['写代码', '修Bug']
            break
        case 'product manager':
            work = ['写PRD', '催更']
            break
        case 'boss':
            work = ['看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
    }

    return new User(name, age, career, work)
}


const coder1 = userFactory(name, age, 'coder');
const boss1 = userFactory(name, age, 'boss');

工厂模式其实就是独立出一个大类,将创建实例对象的过程用大类的子类来实现。即将 new 操作单独封装,只对外提供接口,参数需要怎么区分来我可以不关心。

工厂模式的核心作用如下:

  1. 主要用于隐藏创建实例的复杂度,只需对外提供一个接口;
  2. 实现构造函数和创建者的分离,满足开放封闭的原则(对扩展开放和对修改封闭)。

工厂模式主要有三种细分模式:简单工厂模式、工厂方法模式、抽象工厂模式。

# 应用场景

平时遇到要创建实例的时候,就可以想想能否用工厂模式实现了。在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们也应该思考是不是可以掏出工厂模式重构我们的代码了。比如:手机工厂(抽象出操作系统、硬件芯片、屏幕...)

# 单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。

实现的方法:先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

class Modal {
    show() {
        console.log("打开弹窗");
    }

    hide() {
        console.log("关闭弹窗");
    }

    static createModal() {
        // 判断是否已经new过实例
        if (!Modal.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            Modal.instance = new Modal();
        }
        //若已经存在,则直接返回
        return Modal.instance;
    }
}

const modal1 = Modal.createModal();
const modal2 = Modal.createModal();

modal1 === modal2; // true

// createModal 的闭包实现
Modal.createModal = (function() {
    let instance = null;

    return function() {
        if (!instance) {
            instance = new Modal();
        }

        return instance;
    };
})();

# 应用场景

在日常业务场景中,我们经常会遇到需要单例模式的场景,比如最基本的弹窗,或是购物车等。因为不论是在单页面还是多页面应用程序中,我们都需要这些业务场景只会同时存在一个。

生产实践例子:vuex 状态管理中的 store

来自其他大佬博客的一篇参考文章 (opens new window)

// 安装vuex插件
Vue.use(Vuex);

// 将store注入到Vue实例中
new Vue({
    el: "#app",
    store,
});

// 通过调用 Vue.use()方法安装了 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到 Vue 实例里去。

// 在 install 方法里,有一段逻辑和我们楼上的 getInstance 非常相似的逻辑:
let Vue; // 这个Vue的作用和上述例子中的instance作用一样

export class Store {
    constructor(options = {}) {
        // Auto install if it is not done yet and `window` has `Vue`.
        // To allow users to avoid auto-installation in some cases,
        // this code should be placed here. See #731
        if (!Vue && typeof window !== "undefined" && window.Vue) {
            install(window.Vue);
        }
    }
    ....
}


export function install(_Vue) {
    // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
    if (Vue && _Vue === Vue) {
        if (__DEV__) {
            console.error(
                "[vuex] already installed. Vue.use(Vuex) should be called only once."
            );
        }
        return;
    }

    // 若没有,则为这个Vue实例对象install一个唯一的Vuex
    Vue = _Vue;

    // 将Vuex的初始化逻辑写进Vue的钩子函数里
    applyMixin(Vue);
}

# 结构型模式

# 装饰器模式

装饰器模式,又名装饰者模式。顾名思义,它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

因此其特征主要有两点:
1) 为对象添加新功能;
2)不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变。

class Circle {
    draw() {
        console.log("画一个圆形");
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }
    draw() {
        this.circle.draw();

        // “包装”了一层新逻辑
        this.setRedBorder(circle);
    }
    setRedBorder(circle) {
        console.log("画一个红色边框");
    }
}

let circle = new Circle();
let decorator = new Decorator(circle);
decorator.draw(); //画一个圆形,画一个红色边框

该例中,我们写了一个 Decorator 装饰器类,它重写了实例对象的 draw。

如果后续需要对 draw 这个行为添加更多详细对需求,可以直接在 Decorator 中添加。如此一来,我们就可以实现“只添加,不修改”的装饰器模式。

ES7 中也引入了装饰器语法插件,具体用法:

npm install babel-plugin-transform-decorators-legacy
npm install babel-cli -g

//.babelrc
// 浏览器和 Node 目前都不支持装饰器语法,需要大家安装 Babel 进行转码:
{
    "presets": ["es2015", "latest"],
    "plugins": ["transform-decorators-legacy"]
}

// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
    target.hasDecorator = true
  	return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
    // Button类的相关逻辑
}

// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)


// 以上简化:
@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

# 应用场景

# mixin
function mixins(...list) {
    return function(target) {
        Object.assign(target.prototype, ...list);
    };
}

const Foo = {
    foo() {
        console.log("in foo");
    },
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo(); // in foo
# 属性装饰器
// 装饰器
// 设置属性只读
function readonly(target, name, descriptor) {
    // descriptor 属性描述对象(Object.defineProperty 中会用到)
    /*
      {
          value: specifiedFunction,
          enumerable: false,
          configurable: true
          writable: true 是否可改
      }
    */

    descriptor.writable = false;
}

class Person {
    constructor() {
        this.first = "周";
        this.last = "杰伦";
    }

    @readonly
    name() {
        return `${this.first}${this.last}`;
    }
}

const p = new Person();
console.log(p.name()); // 打印成功 ,‘周杰伦’

// 试图修改name
p.name = function() {
    return "ccc";
};
// Uncaught TypeError:Cannot assign to read only property 'name' of object '#<Person>'
// 可见,再给属性添加了只读的装饰后,代码试图修改属性的命令将会报错。

# 适配器模式

适配器模式很好理解,在日常开发中其实不经意间就用到了。

适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成适应当前场景的另一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。所以,适配器模式必须包含目标(Target)、源(Adaptee)和适配器(Adapter)三个角色。

举个例子:

1)日常普通耳机插口是圆形的,换了个新手机 iphone12 手机耳机插口是扁平的 usb-c 接口,想要旧耳机在新手机也能兼容用得上。这个时候转换头就出现了,而转换头就充当着一个适配器的角色...

2)对项目中的 ajax 采用 fetch 替换(即适配旧请求格式)

export default class HttpUtils {
    // get方法
    static get(url) {
        return new Promise((resolve, reject) => {
            // 调用fetch
            fetch(url)
                .then((response) => response.json())
                .then((result) => {
                    resolve(result);
                })
                .catch((error) => {
                    reject(error);
                });
        });
    }

    // post方法,data以object形式传入
    static post(url, data) {
        return new Promise((resolve, reject) => {
            // 调用fetch
            fetch(url, {
                method: "POST",
                headers: {
                    Accept: "application/json",
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                // 将object类型的数据格式化为合法的body参数
                // 转换过程: {a: 1, b: 2} -> a=1&b=2 -> a%3D1%26b%3D2
                body: this.changeData(data),
            })
                .then((response) => response.json())
                .then((result) => {
                    resolve(result);
                })
                .catch((error) => {
                    reject(error);
                });
        });
    }
}

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase();
    let result;
    try {
        // 实际的请求全部由新接口发起
        if (type === "GET") {
            result = (await HttpUtils.get(url)) || {};
        } else if (type === "POST") {
            result = (await HttpUtils.post(url, data)) || {};
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success
            ? success(result)
            : failed(result.statusCode);
    } catch (error) {
        // 捕捉网络错误
        if (failed) {
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed);
}

# 应用场景

生产实践:axios 中的适配器

//  axios常用方式
axios.get('/user?ID=12345')
  .then(function (response) {
      ...
  })
  .catch(function (error) {
      ...
  })


axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    ...
  })
  .catch(function (error) {
    ...
  });

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})

axios 强大的地方在于,它不仅仅是一个局限于浏览器端的库。在 Node 环境下,我们尝试调用上面的 api,会发现它照样好使 —— axios 完美地抹平了两种环境下 api 的调用差异,靠的正是对适配器模式的灵活运用。

在 axios 的核心逻辑中,我们可以注意到实际上派发请求的是 dispatchRequest 方法。该方法内部其实主要做了两件事:
1)数据转换,转换请求体/响应体,可以理解为数据层面的适配;
2)调用适配器。

调用适配器的逻辑如下:

// 若用户未手动配置适配器,则使用默认的适配器
// 手动配置适配器允许我们自定义处理请求,主要目的是为了使测试更轻松。
var adapter = config.adapter || defaults.adapter;

// dispatchRequest方法的末尾调用的是适配器方法
return adapter(config).then(
    // 请求成功的回调
    function onAdapterResolution(response) {
        throwIfCancellationRequested(config);

        // 转换响应体
        response.data = transformData(
            response.data,
            response.headers,
            config.transformResponse
        );

        return response;
    },

    // 请求失败的回调
    function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
            throwIfCancellationRequested(config);

            // 转换响应体
            if (reason && reason.response) {
                reason.response.data = transformData(
                    reason.response.data,
                    reason.response.headers,
                    config.transformResponse
                );
            }
        }

        return Promise.reject(reason);
    }
);

// 实际开发中,我们使用默认适配器的频率更高。默认适配器在axios/lib/default.js里是通过getDefaultAdapter方法来获取的:
function getDefaultAdapter() {
    var adapter;

    // 判断当前是否是node环境
    if (
        typeof process !== "undefined" &&
        Object.prototype.toString.call(process) === "[object process]"
    ) {
        // 如果是node环境,调用node专属的http适配器
        adapter = require("./adapters/http");
    } else if (typeof XMLHttpRequest !== "undefined") {
        // 如果是浏览器环境,调用基于xhr的适配器
        adapter = require("./adapters/xhr");
    }
    return adapter;
}

一个好的适配器的自我修养——把变化留给自己,把统一留给用户。在此处,所有关于 http 模块、关于 xhr 的实现细节,全部被 Adapter 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。

# 行为型模式

# 策略模式

策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。

策略模式的目的就是将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。
第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类 Context(不变),Context 接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明 Context 中要维持对某个策略对象的引用。

/*策略类*/
var levelOBJ = {
    A: function(money) {
        return money * 4;
    },
    B: function(money) {
        return money * 3;
    },
    C: function(money) {
        return money * 2;
    },
};

/*环境类*/
var calculateBouns = function(level, money) {
    return levelOBJ[level](money);
};

console.log(calculateBouns("A", 10000)); // 40000

# 观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象(1 对 N),使它们能够自动更新。

观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素——“发布者”与“订阅者”。

例子:一个个开发被产品经理拉群,等需求文档整理好了以后,@所有人 通知各位开发开始“996”。

// 发布者
class Publisher {
    constructor() {
        this.watchers = [];
        console.log("publisher created, watchers is: ", this.watchers);
    }

    // 添加订阅者
    add(watcher) {
        this.watchers.push(watcher);
    }

    // 删除订阅者
    remove(watcher) {
        this.watchers.forEach((item, index) => {
            if (item === watcher) {
                this.watchers.splice(index, 1);
            }
        });
    }

    // 通知订阅者
    notify() {
        this.watchers.forEach((watcher) => {
            watcher.update(this);
        });
    }
}

// 订阅者
class Watcher {
    constructor() {
        console.log("watcher created");
    }

    update() {
        console.log("Watcher.update invoked");
    }
}

// 在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。
class PrdPublisher extends Publisher {
    constructor() {
        super();

        // 初始化需求文档
        this.prdFile = null;

        // 初始化群
        this.watchers = [];

        console.log("PrdPublisher created");
    }

    setState(state) {
        console.log("PrdPublisher.setState invoked");
        // 需求文档发生改变
        this.prdFile = state;

        // 立刻通知所有开发者
        this.notify();
    }

    getState() {
        console.log("PrdPublisher.getState invoked");
        return this.prdFile;
    }
}

class DeveloperWatcher extends Watcher {
    constructor() {
        super();

        // 还没获取最新需求文档
        this.prdFile = null;

        console.log("DeveloperWatcher created");
    }

    update(publisher) {
        console.log("DeveloperWatcher.update invoked");

        // 更新需求文档
        this.prdFile = publisher.getState();

        // 开始搬砖
        this.work();
    }

    // 专门搬砖的方法
    work() {
        // 获取需求文档
        const prd = this.prdFile;

        // 开始基于需求文档提供的信息搬砖。。。
        console.log("996 begins...");
    }
}

// 开始了
// 发布者-项目群
const project1 = new PrdPublisher();

// 订阅者
const chieminchan = new DeveloperWatcher("chieminchan");
const kate = new DeveloperWatcher("kate");
const toby = new DeveloperWatcher("toby");

// 项目群拉人
project1.add(chieminchan);
project1.add(kate);
project1.add(toby);

// 更新需求文档
const file = { day1: "需求评审", day2: "梳理需求详细流程" };
project1.setState(file);

# 应用场景

Vue 的双向绑定 、 EventBus/EventEmitter

vue 的双向绑定可以参考另一篇文章,这里展开一下 EventEmitter 的实现。

先来看看应用方式:

const emitter = new EventEmitter();

// 监听sendMsg事件
emitter.on("sendMsg", function(text1) {
    console.log("我接收到了消息: ", text1);
});
// 触发emitter事件
emitter.emit("sendMsg", "send test");

console.log("----------------------");

// 一次性监听 forOnce事件
emitter.once("forOnce", function(text1, text2) {
    console.log("我接收到了消息: ", text1, text2);
});
emitter.emit("forOnce", "test1", "tets2");

/**
on invoked
我接收到了消息:  send test
----------------------
on invoked
我接收到了消息:  test1 tets2
off invoked
*/

ES6 实现方式

class EventEmitter {
    constructor() {
        // 事件与回调函数的映射表
        this.handlers = {};
    }

    // 安装事件监听器
    on(event, handler) {
        // 先检查一下目标事件名有没有对应的监听函数队列
        if (!this.handlers[event]) {
            this.handlers[event] = [];
        }

        // 把回调函数推入目标事件的监听函数队列里去
        this.handlers[event].push(handler);
        console.log("on invoked");
    }

    // 移除某个事件回调队列里的指定回调函数
    off(event, handler) {
        const callbacks = this.handlers[event];
        const index = callbacks.indexOf(handler);

        if (index > -1) {
            callbacks.splice(index, 1);
            console.log("off invoked");
        }
    }

    // emit方法用于触发目标事件
    emit(evnet, ...args) {
        if (this.handlers[evnet]) {
            this.handlers[evnet].forEach((handler) => {
                handler(...args);
            });
        }
    }

    // 为事件注册单次监听器
    // 需对回调函数进行包装,使其执行完毕自动被移除
    once(event, handler) {
        const wrapper = (...args) => {
            handler.call(handler, ...args);
            this.off(event, wrapper);
        };

        this.on(event, wrapper);
    }
}

ES5 实现方式

function EventEmitter() {
    this.handlers = {};
}

EventEmitter.prototype.on = function(event, handler) {
    if (!this.handlers[event]) {
        this.handlers[event] = [];
    }

    this.handlers[event].push(handler);
    console.log("on invoked");
};

EventEmitter.prototype.off = function(event, handler) {
    const callbacks = this.handlers[event];
    const index = callbacks.indexOf(handler);

    if (index > -1) {
        callbacks.splice(index, 1);
        console.log("off invoked");
    }
};

EventEmitter.prototype.emit = function(event, ...args) {
    if (this.handlers[event]) {
        this.handlers[event].forEach((handler) => {
            handler(...args);
        });
    }
};

EventEmitter.prototype.once = function(event, handler) {
    const wrapper = (...args) => {
        handler(...args);
        this.off(event, wrapper);
    };

    this.on(event, wrapper);
};

# 观察者模式与发布-订阅模式的区别是什么

两种的核心思路是一样的,只是规模不同所采取的方案不同而已。

在发布订阅模式里,发布者并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识,通过第三者,也就是在消息队列里面。

有点类似于快递员上门送货,后来快递太多了,为了增加效率,分工更明确一点,现在多了个中间站,菜鸟驿站,快递员方便了,这是在规模起来以后自然而然的选择。现在是人主动去拿快递,如果以后连这也嫌弃效率不高,怎么办?再加一层,菜鸟驿站派出机器人送..

从表面上看:

  1. 观察者模式里,只有两个角色 —— 观察者 + 被观察者
  2. 发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人 Broker

往更深层次讲:

  1. 观察者和被观察者,是松耦合的关系
  2. 发布者和订阅者,则完全不存在耦合

从使用层面上讲:

  1. 观察者模式,多用于单个应用内部
  2. 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件

参考文章: