设计模式

从架构层面考虑设计,从上往下看,而不是只顾往下看
设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。
设计模式就是一种理念,通过一些设计思维来解决平时编写底层或业务代码时遇到的场景问题。比如早期业务中的一个封装类,同时带有一些封装方法。如果现在该类不能再满足全部业务场景,且不允许修改原方法,此时就需要装饰器或适配器模式来解决;又比如当设计一个场景,在调用一个固定对象时一定要先执行某些方法,比如验证登录、验证身份ID等场景,此时就应该用到代理模式。

介绍一下单一职责原则和开放封闭原则

单一职责原则: 一个类只负责一个功能领域中的相应职责
开放封闭原则: 软件实体(类、模块函数)是可扩展的但不可修改的

装饰者模式 Decorator

特征

在原来方法的基础上装饰一些针对特殊场景所适用的方法,即添加一些新功能,主要特征

  • 为对象添加新功能
  • 不改变原有的结构和功能,即原本的功能得继续用

实现 one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  class Circle {
draw() {
console.log('画一个圆')
}
}
class Decorator {
constructor(circle) {
this.circle = circle
}
draw() {
this.circle.draw()
this.setRedBorder()
}
setRedBorder() {
console.log('画一个红色的边框')
}
}

const circle = new Circle()
const decorator = new Decorator(circle) // 传入一个实例
decorator.draw()
// 画一个圆
// 画一个红色的边框

该例中,我们写了一个Decorator装饰器类,它重写了实例对象的draw方法,给其方法新增了一个setRedBorder(),因此最后为其输出结果进行了装饰。

实现 two

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 装饰器,在当前函数执行前先执行另一个函数
function DecoratorBefore(fn, beforeFn) {
return function() {
const res = beforeFn.apply(this, arguments);
// 在前一个函数中判断,不需要执行当前函数
if(res !== false) {
fn.apply(this.arguments)
}
}
}
function learn() {
console.log('learn 学习')
}
function shufa() {
console.log('书法')
}
function run() {
console.log('run')
}
var d = DecoratorBefore(shufa, run)
d = DecoratorBefore(d, learn)
d()
VM3126:12 learn 学习
VM3126:18 run
VM3126:15 书法

装饰器插件

ES7 中就存在了装饰器语法,需要安装相应的babel插件,一起看一下该插件如何用,首先安装一下插件,并做相关的语法配置:

1
2
3
4
5
6
7
npm i babel-plugin-transform-decorators-legacy 

//.babelrc
{
"presets": ["es2015", "latest"],
"plugins": ["transform-decorators-legacy"]
}

给一个Demo类上添加一个装饰器 testDec,此时 Demo类就具有了 装饰器赋予的属性:

1
2
3
4
5
6
7
8
@testDec
class Demo {}

function testDec(target) {
target.isDec = true;
}

alert(Demo.isDec) // true

得出结论

1
2
3
4
5
6
@decorator 
class A {}

// 等同于
class A {}
A = decorator(A) || A;

场景

  • Vue项目里的 vue-property-decorator
    1
    2
    3
    import { Component, Vue } from 'vue-property-decorator'
    @Component({})
    export default class MaskLayer extends Vue {}
  • vue中的 mixin 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function mixins(...list) {
    return function(target) {
    Object.assign(target.prototype, ...list)
    }
    }
    const Foo = {
    foo() {
    alert('foo');
    }
    }

    @mixins(Foo)
    class MyClass { }
    let obj = new MyClass();
    obj.foo();
    上例中,Foo作为target的实参,MyClass作为 list的实参,最终实现将Foo的所有原型方法(foo)装饰到 MyClass类上,成为了MyClass的方法。最终代码的运行结果是执行了foo()

拓展

ts里的装饰器, 更新中

发布-订阅模式(Observer Pattern)

发布-订阅模式又叫观察者模式,也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。JavaScript 本身也是一门基于事件驱动的语言,也利用了发布订阅模式

它的作用是当一个对象的状态发生变化时,能够自动通知其他关联对象,自动刷新对象状态,或者说执行对应对象的方法。
这种设计模式可以大大降低程序模块之间的耦合度,便于更加灵活的扩展和维护。

核心

观察者模式包含两种角色:

观察者(订阅者)
被观察者(发布者)

核心思想:订阅者只要订阅了发布者的事件,那么当发布者的状态改变时,发布者会主动去通知观察者,而无需关心订阅者得到事件后要去做什么,实际程序中可能是执行订阅者中的回调函数。

特点

  1. 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
  2. 解耦, 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变
  3. 缺点:创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销,弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解(如果过多的使用发布订阅模式, 会增加维护的难度)

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Event {
constructor() {
this.watchers = {};
}
// 订阅主题和回调函数
subscribe(key, fn) {
this.watchers[key] = this.watchers[key] || []
this.watchers[key].push(fn)
}
// 主题发布以及传参
publish(key, data) {
if (this.watchers[key] && this.watchers[key].length) {
this.watchers[key].forEach(fn => {
fn(data);
})
}
}
// 取消主题或者指定事件退订
unsubscribe (key = '', fn = null) {
if (fn) {
// 如果只取消
if(this.watchers[key] && this.watchers[key].length) {
const index = this.watchers[key].findIndex(cb => Object.is(cb, fn))
this.watchers[key].splice(index, 1)
}
} else if (key) {
this.watchers[key] = []
} else {
this.watchers = {}
}
}
}

const event = new Event();
const eatFn = (data) => {
console.log(data, 'eat')
}
const studyFn = (data) => {
console.log(data, 'come on 加油')
}
// 根据主题 订阅
event.subscribe('eat', eatFn);
event.subscribe('study',studyFn);
// 根据主题进行发布
event.publish('study', 'study PUBLISH');
event.publish('eat', 'eat PUBLISH');

场景

  • JS中的事件就是经典的发布-订阅模式的实现
    1
    2
    3
    // 我们向某dom文档订阅了点击事件,当点击发生时,他会执行我们传入的callback
    element.addEventListener(‘click’, callback2, false)
    element.addEventListener(‘click’, callback2, false)
    或者Vue
    1
    2
    3
    4
    5
    事件发布者使用'vm.$emit、vm.$dispatch(vue1.0)、vm.$broadcast(vue1.0)发布事件
    // 接受方使用$on方法或组件监听器订阅事件,传递一个回调函数
    vm.$emit(event, […args]) // publish
    vm.$on(event, callback) // subscribe
    vm.$off([event, callback]) // unsubscribe

外观模式(Facade Pattern)

也叫门面模式,外观模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用
实现:
外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 三个处理函数
function start() {
console.log('start');
}
function doing() {
console.log('doing');
}
function end() {
console.log('end');
}
// 外观函数,将一些处理统一起来,方便调用
function execute() {
start();
doing();
end();
}
// 调用init开始执行
function init() {
// 此处直接调用了高层函数,也可以选择越过它直接调用相关的函数
execute();
}

init(); // start doing end

场景

我们可以应用外观模式封装一个统一的DOM元素事件绑定/取消方法,用于兼容不同版本的浏览器和更方便的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 绑定事件
function addEvent(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
} else {
element['on' + event] = fn;
}
}
// 取消绑定
function removeEvent(element, event, handler) {
if (element.removeEventListener) {
element.removeEventListener(event, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + event, handler);
} else {
element['on' + event] = null;
}
}

代理模式(Proxy Pattern)

核心

为一个对象提供一个代用品或占位符,以便控制对它的访问。
使用者、目标对象和代理者,使用者的目的是直接访问目标对象,但却不能直接访问,而是要先通过代理者。因此该模式非常像明星代理人的场景。其特征为:

  • 使用者无权访问目标对象;
  • 中间加代理,通过代理做授权和控制。

代理模式确实很方便,通常如果面临一些很大开销的操作,就可以并采用虚拟代理的方式延迟到需要它的时候再去创建,比如懒加载操作。或者一些前置条件较多的操作,比如目标操作实现的前提必须是已登录,且Id符合一定特征,此时也可以将这些前置判断写到代理器中。举个加载图片的例子:

场景

  • 事件冒泡与事件捕获应用:事件代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <body>
    <ul class="color_list">
    <li>red</li>
    <li>orange</li>
    <li>yellow</li>
    <li>green</li>
    <li>blue</li>
    <li>purple</li>
    </ul>
    <script>
    function colorChange(e) {
    var e = e || window.event;
    if (e.target.nodeName.toLowerCase === 'li') {
    box.innerHTML="该颜色为 "+e.target.innerHTML;
    }
    }
    color_list.addEventListener("click", colorChange, false)
    </script>
    </body>

    我们并未直接在元素上定义点击事件,而是通过监听元素点击事件,并通过定位元素节点名称来代理到

  • 标签的点击,最终利用捕获事件来实现相应的点击效果。

  • 缓存代理 可以为一些开销大的运算结果提供暂时的缓存,提升效率

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function memo(fn){
    const cache = new Map();
    return function(){
    const key = [].join.call(arguments);
    if(cache.has(key)){
    return cache.get(key);
    }
    const res = fn.apply(this, arguments);
    cache.set(key, res);
    return res;
    }
    }
    var getMemor = memo(function slow(x,y,z) {
    console.log(x + y + z, 'total')
    return x + y + z
    })
    var test1 = getMemor(1,2,3);
    var test2 = getMemor(1,2,3);
    console.log(test1)
    console.log(test2)
    // 6 "total" 只会执行一次
    // 6
    // 6
  • ES6 proxy
    ES6的 Proxy 相信大家都不会陌生,Vue 3.0 的双向绑定原理就是依赖 ES6 的 Proxy 来实现,给一个简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // proxy
    var obj = {
    name: 'cpp',
    age: 30
    }
    new Proxy(
    obj,
    {
    get(target,key) {
    return target[key]
    },
    set(target, key, val) {
    target[key] = val;
    return true
    }
    }
    )

工厂模式(Factory Pattern)

定义

当构造函数过多不方便管理,且需要创建的对象之间存在某些关联(有同一个父类、实现同一个接口,有同一个属性等)时,不妨使用工厂模式。工厂模式提供一种集中化、统一化的方式,避免了分散创建对象导致的代码重复、灵活性差/实例化重复的问题。

核心

  • 主要用于隐藏创建实例的复杂度,只需对外提供一个接口;
  • 实现构造函数和创建者的分离,满足开放封闭的原则;

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Suzuki汽车构造函数
function SuzukiCar(color) {
this.color = color;
this.brand = 'Suzuki';
}
// BMW汽车构造函数
function BMWCar(color) {
this.color = color;
this.brand = 'BMW';
}
// 汽车品牌枚举
const BRANDS = {
suzuki: 1,
honda: 2,
bmw: 3
}
/**
* 汽车工厂 颜色一致 brand不一样
*/
function CarFactory() {
this.create = function (brand, color) {
switch (brand) {
case BRANDS.suzuki:
return new SuzukiCar(color);
case BRANDS.bmw:
return new BMWCar(color);
default:
break;
}
}
}
const carFactory = new CarFactory();
const cars = [];
cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));
// SuzukiCar {color: "brown", brand: "Suzuki"}
// BMWCar {color: "red", brand: "BMW"}

使用工厂模式之后,不再需要重复引入一个个构造函数,只需要引入工厂对象就可以方便的创建各类对象。

场景

  • jQuery的选择器$(selector),$内置的实现机制是工厂模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class jQuery {
    constructor(selector) {
    super(selector)
    }
    // ...
    }
    window.$ = function(selector) {
    return new jQuery(selector)
    }
  • Vue 异步组件
    1
    2
    3
    4
    5
    6
    7
    Vue.component('async-example' , (resolve , reject) => {
    setTimeout(function() {
    resolve({
    template: `<div>I am async!</div>`
    })
    }, 1000)
    })

单例模式(Singleton Pattern)

顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。

  • 确保只有一个实例
  • 可以全局访问

我们一般通过实现以下两点来解决上述问题:

  • 隐藏Class的构造函数,避免多次实例化
  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例

    实现 one

    Javascript中单例模式可以通过以下方式实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 单例构造器
    const FooServiceSingleton = (function () {
    // 隐藏的Class的构造函数
    function FooService() {}
    // 未初始化的单例对象
    let fooService;
    return {
    // 创建/获取单例对象的函数
    getInstance: function () {
    if (!fooService) {
    fooService = new FooService();
    }
    return fooService;
    }
    }
    })();
    实现的关键点有:
  1. 使用 IIFE 创建局部作用域并即时执行;
  2. getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回

测试

1
2
3
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true

实现 two

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const singleton = function(fn) {
let result = null;
return function() {
return result || (result = fn.apply(this, arguments));
};
};

const getScript = singleton(function() {
return document.createElement('script');
});

const script1 = getScript();
const script2 = getScript();
console.log(script1 === script2); // true

场景

  • 因为 JavaScript 是无类的语言, 而且 JS 中的全局对象符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用,
  • Vue和React中的store

策略模式

解释: 对象有某个行为,但是在不同的场景中,该行为有不同的实现策略(根据不同参数可以命中不同的策略)
案例: 模式的场景如登录鉴权,鉴权算法取决于用户的登录方式是手机、邮箱或者第三方的微信登录等等,而且登录方式也只有在运行时才能获取,获取到登录方式后再动态的配置鉴权策略。所有这些策略应该实现统一的接口,或者说有统一的行为模式。
示例:
登录鉴权的例子我们仿照 passport.js 的思路通过代码来理解策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 登录控制器
*/
function LoginController() {
this.strategy = undefined;
this.setStrategy = function (strategy) {
this.strategy = strategy;
this.login = this.strategy.login;
}
}
/**
* 用户名、密码登录策略
*/
function LocalStragegy() {
this.login = ({ username, password }) => {
console.log(username, password);
// authenticating with username and password...
}
}
/**
* 手机号、验证码登录策略
*/
function PhoneStragety() {
this.login = ({ phone, verifyCode }) => {
console.log(phone, verifyCode);
// authenticating with hone and verifyCode...
}
}
/**
* 第三方社交登录策略
*/
function SocialStragety() {
this.login = ({ id, secret }) => {
console.log(id, secret);
// authenticating with id and secret...
}
}

const loginController = new LoginController();
// 调用用户名、密码登录接口,使用LocalStrategy
app.use('/login/local', function (req, res) {
loginController.setStrategy(new LocalStragegy());
loginController.login(req.body);
});

// 调用手机、验证码登录接口,使用PhoneStrategy
app.use('/login/phone', function (req, res) {
loginController.setStrategy(new PhoneStragety());
loginController.login(req.body);
});

// 调用社交登录接口,使用SocialStrategy
app.use('/login/social', function (req, res) {
loginController.setStrategy(new SocialStragety());
loginController.login(req.body);
});

特点:

  • 在运行时切换算法和策略
  • 更简洁,避免使用大量的条件判断
  • 分离,每个strategy类控制自己的算法逻辑,strategy和其使用者之间也相互独立

迭代(遍历器Iterator pattern)

ES6中的迭代器 Iterator 相信大家都不陌生,迭代器用于遍历容器(集合)并访问容器中的元素,而且无论容器的数据结构是什么(Array、Set、Map等),迭代器的接口都应该是一样的,都需要遵循 迭代器协议。
迭代器模式解决了以下问题:
一种机制,各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

ES6提供了更简单的迭代循环语法 for…of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。
比如我们实现一个 Range 类用于在某个数字区间进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return { value: start++, done: false };
}
return { done: true, value: end };
}
}
}
}
}
for (num of Range(1, 5)) {
console.log(num); //1 2 3 4
}

参考链接