前端必备10种设计模式

接手项目越来越复杂的时候,有时写完一段代码,总感觉代码还有优化的空间,却不知道从何处去下手。设计模式主要目的是提升代码可扩展性以及可阅读性。

本文主要以例子的方式展示设计模式应该如何使用!(例子主要来源于javascript设计模式一书,如果已经对这本书读得滚瓜烂熟的,可以划过,如果还未读,或者想了解一下可以收藏起来慢慢看~)
photo-1504691342899-4d92b50853e1 (1).jpg-119.8kB

设计原则

在使用设计模式前应该需要知道的几个原则(其中对应设计模式满足对应原则):

  • 单一职责原则(SRP): 一个对象(只做一件事)。

    • 代理模式,迭代器模式,单例模式,装饰者模式
  • 最少知识原则(LKP): 一个软件实体应当尽可能少地与其他实体发生相互作用。

    • 中介者模式
  • 开放-封闭原则(OCP):软件实体(类,模块,函数)应该都是可以扩展,但是不可修改

    • 发布-订阅模式,模板方法模式,策略模式,代理模式,职责链模式

代理模式

代理顾名思义,就是客服无法直接与本体进行的沟通通过第三方进行转述。

QQ五笔截图未命名.png-2.5kB

虚拟代理
作为创建开销大的对象的代表;虚拟代理经常直到我们真正需要一个对象的时候才创建它;当对象在创建前或创建中时,由虚拟代理来扮演对象的替身;对象创建后,代理就会将请求直接委托给对象;
图片预加载例子
const myImage = (function() {
    const imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
}())

// 代理容器
const proxyImage = (function() {
    let img = new Image();
    
    // 加载完之后将设置为添加的图片
    img.onload = function() {
        myImage.setSrc(this.src)
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('loading.gif');
            img.src = src;
        }
    }
}())

proxyImage.setSrc('file.jpg')

如上:代理容器控制了客户对MyImage的访问,并且在过程中加了一些额外的操作。

缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
计算乘积例子
// 求乘积函数(专注于自身职责,计算成绩,缓存由代理实现)
const mult = function() {
    let a = 1;
    for (let i = 0, l =arguments.length; i< l; i++){
        a = a * arguments[i];
    }
    return a;
}

// proxyMult 
const proxyMult = (function() {
    let cache = {};
    return function() {
        let args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        return cache[arg] = mult.apply(this, arguments);
    }
}())

proxyMult(1, 2, 3) // 6
proxyMult(1, 2, 3) // 6

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表达式。迭代器模式可以将迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即不用关心对象内部构造也可以按顺序访问其中的每个元素。

迭代器分为内部迭代器和外部迭代器,内部迭代器,是在函数内部已经定义好了迭代规则,外部只需要调用即可。但如果要修改需求,那就要去迭代函数内部去修改逻辑了。外部迭代器指的是必须显示的请求迭代下一个元素。

如现在有一个需求,判断两个函数是否完全相等,分别使用内部迭代器和外部迭代器去实现。

// 使用内部迭代的方式实现 compare
const compare = function (arr1, arr2) {
    try {
        if (arr1.length !== arr2.length) {
            throw 'arr1和arr2不相等'
        }
        // forEach 相当一于一个迭代器
        arr1.forEach((item, index) => {
            if (item !== arr2[index]) {
                throw 'arr1和arr2不相等'
            }
        })
        console.log('arr1等于arr2')
    } catch (e) {
        console.log(e)
    }
}

使用外部迭代器模式改写compare

// 迭代器
const iterator = function (obj) {
    let current = 0;
    let next = function () {
        current += 1;
    };
    let isDone = function () {
        return current >= obj.length;
    }
    let getCurrItem = function () {
        return obj[current];
    }
    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem,
        length: obj.length
    }
}
// 重写compare
const compare = function (iterator1, iterator2) {
    try {
        if (iterator1.length !== iterator2.length) {
            throw 'iterator1不等于iterator2'
        }
        while (!iterator1.isDone() && !iterator2.isDone()) {
            if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {

                throw 'iterator1不等于iterator2'
            }
            iterator1.next();
            iterator2.next();
        }
        console.log('iterator1 === iterator2')
    } catch (e) {
        console.log(e)
    }
}
const iterator1 = iterator([1, 2, 3]);
const iterator2 = iterator([1, 2, 3]);
compare(iterator1, iterator2)

迭代器实际场景的应用

根据不同的浏览器获取相应上传组件对象
// 常规写法
const getUploadObj = function() {
    try {
        return new ActiveXObject('txftna')
    } catch (e) {
        if (supportFlash()) {
            let str = `<object type="application/x-shockwave-flast"></object>`
            return document.body.appendChild(str)
        } else {
            let str = `<input name="file" type="file"/>`;
            return document.body.appendChild(str);
        }
    }
}
// 迭代模式改写

// IE上传控件
const getActiveUploadObj = function () {
    try {
        return new ActiveXObject('TXFTNActiveX.FTNUPload')
    } catch (e) {
        return false;
    }
}

// flash上传控件
const getFlashUploadObj = function () {
    if (supportFlash()) {
        let str = `<object type="application/x-shockwave-flast"></object>`
        return document.body.appendChild(str)
    }
    return false
}
// 表单上传
const getFormUploadObj = function () {
    let str = `<input name="file" type="file"/>`;
    return document.body.appendChild(str);
}
// 使用迭器执行
const iteratorUploadObj = function () {
    for (let i = 0, fn; fn = arguments[i++];) {
        const uploadInstane = fn()
        if (uploadInstane !== false) {
            return uploadInstane
        }
    }
}

iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)

观察上面经过未使用迭代器模式,和使用迭代器模式后,发现不使用的话,如果后期想新增一个其他的模式,需要修改原来代码的逻辑,如果使用迭代器的模式的话只需要新增一个方法即可。虽然在写的时候代码多了几行,但总的来说,后期扩展以及可阅读性明显提高了~。

单例模式

定义:单例模式指的是保证一个类仅有一个实例,且提供一个访问它的全局访问点。全局缓存,window对象,都可以看作是一个单例。

目的: 解决一个全局使用的类频繁地创建与销毁

使用ES6 class 创建单例:
class Instance {
    static init () {
        if (!this.instance) {
            this.instance = new Instance()
        }
        return this.instance;
    }
}
const instance1 = Instance.init()
const instance2 = Instance.init()

console.log(instance1 === instance2) //true
使用闭包创建单例:
const instance = (function() {
    let instance = null;
    return function(name) {
        if (!instance) {
            instance = new Singleton(name)
        }
        return instance;
    }
}())
使用代理实现单例模式

以在页面上创建唯一的dom节点为例;

// 创建div类
class CreateDiv {
    constructor(html) {
        this.html = html
        this.init()
    }

    init() {
        let div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div)
    }
}

// 代理类
class ProxySingletonCreateDiv {
    static initInstance(html) {
        if (!this.instance) {
            this.instance = new CreateDiv(html)
        }
        return this.instance;
    }
}

ProxySingletonCreateDiv.initInstance('test1');
ProxySingletonCreateDiv.initInstance('test2');

测试上面这段代码会发现页面上只会显示test1,因为实例只创建了一次,也就是new CreateDiv只执行了第一次,第二次并没有执行。

惰性单例
惰性单例是指在需要的时候才创建对象实例。(在一定场景下,用户只有在需要的时候才创建)

例:instance实例总是在我们调用getInstance的时候才会被创建

Singleton.getInstance = (function () {
    let instance = null;
    return function (name) {
        if (!instance) {
            instance = new Singleton(name)
        }
        return instance;
    }
}())

const instance = Singleton.getInstance('hello')

实现通用惰性单例

const getSingle = function (fn) {
    let result;
    return function() {
        return result || (result = fn.apply(this, arguments))
    }
}

装饰者模式

装饰者模式指的是:可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
最基础的装饰者

以编写一个飞机大战的游戏为例,随着等级的增加最开始我们只能发送普通的子弹,二级可以发送导弹,三级可以发送原子弹。

// 飞机对象
let plane = {
  fire: function () {
      console.log('发射普通子弹')
  }
};
// 发送导弹的方法,实际内容略
let missileDecorator = function () {
    console.log('发射导弹');
};
// 发送原子弹的方法
let atomDecorator = function () {
    console.log('发射原子弹');
};
// 将发送子弹方法存起来,
let fire1 = plane.fire;
// 装饰发送普通子弹的方法
plane.fire = function () {
    fire1();
    missileDecorator()
};

let fire2 = plane.fire;
plane.fire = function () {
    fire2();
    atomDecorator()
};

plane.fire() // 发射普通子弹,发射导弹,发射原子弹

我们有时候在维护代码的时候,可能会遇到这样的需求。比如给window绑定onload事件,但又不太确定这个事件是否被其他人绑定过了,为了避免覆盖掉之前的window.onload的函数的行为,我们一般会向上面的例子一样,将之前的行为保存在一个变量内,然后再给绑定的window.onload函数添加这个变量执行从而满足需求。
以上方法的缺陷

  1. 需要多维护了中间变量,如上面的例子fire1fire2,如果链越来越长,那么维护的就越来越多。
  2. 还会遇到this劫持问题,如上fire函数被变量存起来的时候plane.fire执行时this指向global(node环境)
AOP装饰函数

为解决上面this的劫持问题,延伸实现Function.prototype.beforeFunction.prototype.after方法:

Function.prototype.before = function (beforeFn) {
    let that = this; // 保存原函数的引用
    return function () {
        beforeFn.apply(this, arguments);
        return that.apply(this, arguments); //执行原函数,且保证this不被劫持
    }
};

Function.prototype.after = function (afterFn) {
    let that = this;
    return function () {
        let ret = that.apply(this, arguments);
        afterFn.apply(this, arguments);
        return ret;
    }
};

改写上面飞机的例子:

let plane = {
    fire: function () {
        console.log('发射普通子弹!')
    }
};
plane.fire.after(function () {
    console.log('发射导弹!')
}).after(function () {
    console.log('发射原子弹!')
})()

很明显的看见解决了上面的两个缺陷。

AOP应用实例之数据上报

做前端开发,主要提升用户体验,所以有时候在项目结尾为了能更多的收集到用户的操作数据不得不加入一些埋点数据在业务中。如:点击上报多少人点击登录按钮来显示登录浮窗。

// 常规做法 bad
let log = function () {
    console.log('上报')// 实际内容略
}
let showLogin = function () {
    console.log('打开登录浮窗');
    log();
}
// 上面做法,showLogin既要做显示弹窗的操作,又要负责数据上报,违反了单一职责原则
// 使用装饰者方式改写 good
let showLogin1 = function () {
  console.log('显示弹窗')
}

let showLogin = showLogin.after(log);
document.getElement('button').onclick = showLogin;

装饰者模式与代码模式的区别:
代理模式强调的是代理与它的实体之间的关系(这种关系在一开始就可以被确定),装饰者模式用于一开始无法确定对象的全部功能场景。代理模式通常只有一层代理。而装饰者会形成一条长长的装饰链。

中介者模式

中介者指的是解除对象与对象之间的紧耦关系,增加中介者之后,所有对象通过中介者来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。

现实生活中很的中介者场景,如:快递物流公司,快递员将负责的区域拿到用户的快递之后将快递送到中转场,然后中转场再进行整理之后输出分好类的区域,再由快递员送到指定区域。在设计模式中,中转场就扮演者,中介者的角色。
QQ五笔截图未命名.jpg-12.1kB
如图将 A,B,C,D互相关联的东西使用一个中介者进行管理,减少A,B,C,D内的互相引用。

const createAgent = (function () {
    return {
        add() => {
            //添加一个东西 代码略
        },
        send () => {
            // 发送一个东西 代码略
        }
    }
}())

const createA = function () {
    createAgent.add()
    setTimeout(() => {
        createAgent.send();
        
        // 代码略
    }, 3000)
}

const createB = function () {
    //同上面方法类似
}

代码只是说明中介者的意图,内容不要在意。

当关联的东西越来越多的时候中介者模式会变得越来越大,虽然会带来这个缺点,但取舍一下还是会比相互之间引用会更好。

发布-订阅模式

发布-订阅模式:定义对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知!

常见的javascript事件指令,也是一种发布订阅模式,

document.body.addEventListener('click', function () {
    // 一堆操作
}, false)

// 模拟用户点击
document.body.click()

这里监控用户点击document.body的动作,但我们并不知道用户什么时候会点击,所以我们订阅document.body上的click事件,当body被点击后,body节点会向订阅者发布这个消息。

发布与订阅模式的实现步骤:

  • 首先要指定谁充当发布者。
  • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
  • 最后发布消息的时候,发布者会便利缓存列表依次触发里面存放的订阅者回调函数

以售楼处去购买房子为例:A去售楼部查看了一下房源,并告知了销售者小姐姐自己的电话以及自己需要的房源类型(这里A充当订阅者),售楼处将用户的电话以及需要房源类型记录在小本子上(这个小本子相当于缓存列表),售楼处充当发布者!下面以一段代码实现这段描述:

// 定义售楼处
let saleOffices = {
    
    // 存放订阅者的回调函数,即用户的电话以及需求
    clientList: {},
    
    // 订阅消息
    listen: function (key, fn) {
        // 如果没有订阅过此类消息,则创建一个缓存列表
        if (!this.clientList[key]) { 
            this.clientList[key] = [];
        }
        // 将订阅的消息添加进消息缓存列表
        this.clientList[key].push(fn);
    },

    // 发布消息
    trigger: function () {
        let key = Array.prototype.shift.call(arguments );
        let fns = this.clientList[key];

        // 如果没订阅此消息则返回
        if (!fns || fns.length === 0) return false;


        for (let i = 0, fn; fn = fns[i++];) {
            // arguments是发布消息的附送参数
            fn.apply(this, arguments);
        }
    },
    
    // 取消订阅
    remove: function (key, fn) {
        let fns = this.clientList[key];

        if (!fns) return false;

        // 如果没有传入回调函数,则取消key所有的订阅
        if (!fn) {
            fns && (fns.length = 0)
        } else {
            for (let l = fns.length - 1; l >= 0; l--) {
                let _fn = fns[l];
                if (_fn === fn) {
                    // 删除订阅的回调函数
                    fns.splice(l, 1)
                }
            }
        }
    }
}; 

let fn1 = function (price) {
    console.log('价格=', price)
}
saleOffices.listen('listen100', fn1)

saleOffices.remove('listen100', fn1)
saleOffices.trigger('listen100', 20000)

看了发布与订阅模式和中介者模式,发现两者之间有着很多相似之处。

发布-订阅中介者之间的区别:

中介者目的是为了减少对象之间的耦合,而且类里的内容可能存在着不同对象之间需要的一些东西存储,后期可能是某个对象自己去取。发布-订阅主要也是解决对象之间的耦合,不同的是发布订阅是取决用户关注什么东西后发布者在有了这个东西之后主动推送给订阅者~

模板方法模式

模板模式指的是一种只需要使用继承就可以实现的非常简单的模式 ,比较依赖于抽象类的一种设计模式,主要由抽象父类和具体实现子类组成!

抽象类可以表示一种契约,继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为了它子类定义这些公共接口

咖啡与茶的例子

泡茶与冲咖啡:
首先可以将茶和咖啡抽象成饮料。

两个在泡和冲都有相似的步骤:

  1. 把水煮沸
  2. 用沸水冲泡饮料
  3. 把饮料倒进杯子
  4. 加调料

在类的继承情况下,有时会存在子类未实现父类里已经调用过的一些方法,常见解决可以在父类里添加对象的方法并提示一个错误如:

Beverage.prototype.brew = function () {
    throw new Error('子类必须重写brew方法');
}

实现冲咖啡与泡茶的例子:

// 抽象类
class Beverage {
    boilWater () {
        console.log('把水煮沸');
    }
    brew () {
        throw new Error('子类必须实现此方法!')
    }
    pourInCup () {
        throw new Error('子类必须实现此方法!')
    }
    addCondiments () {
        throw new Error('子类必须实现此方法!')
    }
    // 构子方法是否需要添加调料
    customerWantsCondiments() {
        return true
    }
    init () {
        this.boilWater();
        this.brew();
        this.pourInCup();
        if (this.customerWantsCondiments()) {
            this.addCondiments();
        }
    }
}
// 咖啡类
class Coffee extends Beverage{
    constructor(props) {
        super(props)
    }

    brew() {
        console.log('用沸水煮咖啡')
    }
    pourInCup() {
        console.log('把咖啡倒进杯子')
    }
    addCondiments() {
        console.log('加糖和牛奶')
    }
    customerWantsCondiments () {
        return false;
    }
}
// 泡茶类
class Tea extends Beverage {
    constructor(props) {
        super(props);
    }
    brew () {
        console.log('用沸水浸泡茶叶')
    }
    pourInCup () {
        console.log('将茶水倒进杯子')
    }
    addCondiments () {
        console.log('加对应配料')
    }
}
let coffee = new Coffee()
coffee.init() // 把水煮沸 用沸水煮咖啡 把咖啡倒进杯子

let tea = new Tea()
tea.init(); // 把水煮沸 用沸水浸泡茶叶 将茶水倒进杯子 加对应配料
好莱坞原则
许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待尽管。有时候演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来差我,我会给你打电话。”,这就是好莱坞原则。

好莱坞原则,允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候,什么方式使用这些底层组件,与好莱坞原则一样。

在javascript中,我们很多时候不需要用类这样繁琐的方式实现模板方法,使用高阶函数更好。我们用高阶函数改写上面的例子。

    const Beverage = function(param) {
        let boilWater = function() {
            console.log('把水煮沸')
        }
        let brew = param.brew || function() {
            throw new Error('必须传递brew方法')
        }
        let pourInCup = param.pourInCup || function() {
            throw new Error('必须传递pourInCup方法')
        }
        let addCondiments = param.addCondiments || function() {
            throw new Error('必须传递addCondiments')
        }
        let F = function () {};
        F.prototype.init = function() {
            boilWater();
            brew();
            pourInCup();
            addCondiments()
        }
        return F;
    }
    const Coffee = Beverage({
        brew:function() {
            console.log('用沸水冲泡咖啡')
        },
        pourInCup: function() {
            console.log('把咖啡倒进杯子')
        },
        addCondiments: function() {
            console.log('加糖和牛奶')
        }
    })

策略模式

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

策略模式优点:

  • 策略模式例用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句
  • 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
表单校验例子

校验逻辑:

  • 用户名不能为空,用户名长度不能小于10位
  • 密码长度不能少于6位
  • 手机号必须符合格式。

为了下面的javascript代码更少的编写html代码,我这里将html代码提取出来

// html 代码
<html>
    <body>
        <form action="http://xxx./register" id="registerForm" method="post">
        请输入用户名:<input type="text" name="userName">
        请输入密码:<input type="password" name="password">
        请输入手机号:<input type="text" name="phoneNumber">
        <button>提交</button>
        </form>
    </body>
</html>

不使用策略模式我们正常的实现

 const registerForm = document.getElementById('registerForm');
 registerForm.onsubmit = function() {
     const userName = registerForm.userName.value
     if(userName === '' && userName.length >= 10) {
         console.log('用户名不能为空')
        return false;
     }
     if (registerForm.password.value.length < 6) {
         console.log('密码不能为空')
         return false;
     }

     if (!/(^1[3|5|8][0|9]{9}$)/.test(registerForm.phoneNumber.value)) {
         console.log('手机号输入不正确')
         return false;
     }
 }

这样的代码会导致校验的函数越来越庞大,在系统变化的时候缺乏弹性。

使用策略模式重构上面的表单校验:

策略模式的组成部分:

  • 策略类:封装具体算法,并负责具体计算过程。
  • 环境类:环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。
// 封装算法
const strategies = {
    isNonEmpty: function (value, errorMsg) {
        if (value === '') {
            return errorMsg;
        }
    },
    minLength: function (value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg;
        }
    },
    isMobile: function (value, errorMsg) {
        if (!/(^1[3|5|8][0|9]{9}$)/.test(value)) {
            return errorMsg;
        }
    }
}

// 实现Context环境类
const Validator = function () {
    this.cache = [];
}
Validator.prototype = {
    add (dom, rules) {
        let self = this;
        for (let i = 0, rule; rule = rules[i++];) {
            let strategyAry = rule.strategy.split(':');
            let errorMsg = rule.errorMsg;

            self.cache.push(function () {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg);
                return strategies[strategy].apply(dom, strategyAry)
            })
        }
    },
    start () {
        for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
            let errorMsg = validatorFunc();
            if (errorMsg) {
                return errorMsg;
            }
        }
    }
}

// 客户端使用
let registerForm = document.getElementById('registerForm');
let validataFunc = function () {
    let validator = new Validator();
    validator.add(registerForm.userName, [{
        strategy: 'isNonEmpty',
        errorMsg: '用户名不能为空'
    }, {
        strategy: 'minLength:10',
        errorMsg: '用户名长度不能小于10位'
    }])

    validator.add(registerForm.password, [{
        strategy: 'minLength:6',
        errorMsg: '密码长度不能小于6位'
    }])
    
    validator.add(registerForm.phoneNumber, [{
        strategy: 'isMobile',
        errorMsg: '手机号码格式不正确'
    }])
    let errorMsg = validator.start();
    return errorMsg;
}

registerForm.onsubmit = function () {
    let errorMsg = validataFunc();
    if (errorMsg) {
        console.log('errorMsg');
        return false;
    }
}

虽然看起来代码多了很多,但对于以后的维护和扩展方法,复用方法,这种方式明显会好很多。

职责链模式

职责链模式指的是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一个链条,并沿着链条传递这些请求,直到有一个对象处理它为止。

职责链生活例子:

中学时期末考试,如果你平时不老实考试时就会被安排到第一排的位置,遇到不会答的题就会把题写在纸条上传给后排的同学,如果后排同样不会继续往后排传值到会为止。

实际开发中职责链模式使用场景:

需求是这样的:

商场在预定手机时,分别缴纳500和200的定金,到的购买阶段后交付500定金的可以收到100优惠券,200的可以收到50优惠券,没有支付定金没有优惠券且只能保证购买时库存有货时能购买成功!

平常我们拿到这样的需求后可能会写下面这样的代码:

const fn = function(stock) {
    if (stock > 0) {
        console.log('普通购买,无优惠券')
    } else {
        console.log('手机库存不足')
    }
}
/**
 * @param {number} orderType 订单类型1,2,3
 * @param {boolean} pay 是否支付定金 true | false
 * @param {number} stock 库存数量 
 */
let order = function(orderType, pay, stock) {
    if (orderType === 1) {
        if (pay === true) {
            console.log('500元定金预购,得到100优惠券。')
        } else {
            fn(stock);
        }
    } else if (olderType === 2) {
        if (pay === true) {
            console.log('200元,50优惠券')
        } else {
            fn(stock)
        }
    } else if (orderType === 3) {
        fn(stock)
    }
}

看上面的代码,逻辑上也没什么问题,但相信我们在写的时候一般也不会这样去写,因为这样在后期维护的时候order函数会变得越来越庞大,而且要新增一些其他的逻辑也是比较困难。

使用职责链模式重写上面的例子:

  1. 拆分条件语句,将每个条件提取成一个函数。
  2. 约定一个字符串'nextSuccess'是否需要向后传递
  3. 包装职责链chain
const order500 = function(orderType, pay, stock) {
    if(orderType === 1 && pay === true) {
        console.log('500定金,100优惠券')
    } else {
        return 'nextSuccess'
    }
}

const order200 = function(orderType, pay, stock) {
    if (orderType === 2 && pay === true) {
        console.log('200定金,返50优惠券')
    } else {
        return 'nextSuccess'
    }
}

const orderNormal = function(orderType, pay, stock) {
    if (stock > 0) {
        console.log('普通购买')
    } else {
        console.log('手机库存不足')
    }
}

const Chain = function(fn) {
    this.fn = fn;
    this.successor = null;
}

Chain.prototype = {
    setNextSuccessor: function(successor) {
        return this.successor = successor;
    },
    passRequest: function() {
        let ret = this.fn.apply(this, arguments);
        
        if (ret === 'nextSuccessor') {
            return this.successor && this.successor.passRequest.apply(this.successor, arguments)
        }

        return ret;
    }
}
// 包装职责链节点
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);
// 指定职责链顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal)
// 应用
chainOrder500.passRequest(1, true, 500); // 500定金,100优惠券
chainOrder500.passRequest(1, false, 0) // 库存不足

职责链的缺点:

职责链使程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点没有起到实质性的作用,它们的作用仅仅让请求传递下去,从性能方面考虑我们应该避免过长的职责链带来的性能损耗。

在之前我们使用了AOP装饰函数,实现装饰者的模式。
同样 这里我们可以使用AOP实现职责链

改写之前的Function.prototype.after函数

Function.prototype.after = function(fn) {
    let self = this;
    return function() {
        let ret = self.apply(this, arguments);
        
        if (ret === 'nextSuccessor') {
            return fn.apply(this, arguments)
        }
        return ret;
    }
}
// 指定顺序
let order = order500.after(order200).after(orderNormal);
order(1, true,  50) // 500定金,100优惠券

去掉了chain类,整个逻辑也变得更加清晰了,同样这种方式也不适合太长的链条。

状态模式

状态模式指的是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

状态模式的优点:

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原来过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。

使用javascript版本的状态机,实现开灯与关灯例子:

let delegate = function(client, delegation) {
    return {
        buttonWasPressed: function() {
            // 将客户端操作委托给delegation对象
            return delegation.buttonWasPressed.apply(client, arguments)
        }
    }
}
let FSM = {
    off: {
        buttonWasPressed: function() {
            console.log('关灯')
            this.button.innerHTML = '下次按我是开灯';
            this.currState = this.onState;
        }
    }, 
    on: {
        buttonWasPressed: function() {
            console.log('开灯');
            this.button.innerHTML = '下次按我是关灯'
            this.currState = this.offState;
        }
    }
}

let Light = function() {
    this.offState = delegate(this, FSM.off);
    this.onState = delegate(this, FSM.on);
    this.currState = this.offState; // 设置初始状态为关闭状态
    this.button = null;
}

Light.prototype = {
    init () {
        let button = document.getElementById('button');
        let self = this;
        button.innerHTML = '已关灯';
        this.button = document.body.appendChild(button);
        this.button.onclick = function () {
            self.currState.buttonWasPressed()
        }
    }
}
let light = new Light()
light.init()

结语

写这篇文章主要意图在于以前也看过javascript设计模式这本书,但可能在写代码的时候一般只会想到文章开头的三个原则,但具体如何通过比较优雅的代码去满足三个原则是比较困难的,最近又重新看了一遍这本书,为了加深自己的印象,将一些比较常用的模式通过自己去描述的方式呈现出来,也能分享给广大的搬砖同学~, 如果觉得读完文章后有所收获,请点赞和收藏给予一丢丢鼓励,haha

阅读 639

推荐阅读
骑着大象兜风
用户专栏

分享有趣和有用的内容

0 人关注
1 篇文章
专栏主页