美文网首页
js 中的 14 种设计模式(下)

js 中的 14 种设计模式(下)

作者: HoPGoldy | 来源:发表于2020-07-19 21:37 被阅读0次

    前言

    本文介绍了 js 中 14 种设计模式的后 6 个,如果你还没有读过上篇的话,推荐先点击 JS 中的 14 种设计模式(上)进行阅读。

    9、享元模式

    享元模式是一个用于性能优化的模式,它是指 当需要创建大量类似的对象时,通过复用一个真实对象来达到节省内存的目标。比如文件上传功能,你可以为列表中的两千个文件都创建对应的上传控件对象,也可以只创建一个上传控件对象,然后再用户点击上传按钮时替换其中的文件信息来实现相同的功能。

    接下来我们就以上传功能为例,介绍一下享元模式,首先我们来实现一个简单的上传控件,在实际开发中,这个上传控件可能会包含很多功能,所以需要消耗不小的资源:

    // 这个上传控件十分的简单,但这里我们假设它“相对来说比较耗费资源”
    const Upload = function() { }
    
    // 更新文件信息
    Upload.prototype.set = function(fileId, fileName) {
        this.fileId = fileId
        this.fileName = fileName
    }
    
    // 执行上传操作
    Upload.prototype.exec = function() {
        if (!this.fileId) throw new Error('未设置文件 id')
        console.log(`文件 ${this.fileName} 被上传,Id ${this.fileId}`)
    }
    

    然后我们来批量创建一个文件列表:

    /**
     * @param {file[]} files 要上传的文件列表
     *   @property {string} id 文件 id
     *   @property {string} name 文件名称
     */
    const createUploader = function (files) {
        uploaders = files.map(file => {
            // 创建上传控件
            const uploader = new Upload(file.id, file.name)
    
            // 创建上传文件
            const dom = document.createElement('div')
            dom.innerHTML = `
                <span>${file.name}</span>
                <button class="upload">上传</buton>
            `
    
            // 绑定上传按钮的点击事件
            dom.querySelector('.upload').onclick = () => uploader.exec()
            document.body.appendChild(dom)
    
            return uploader
        })
    }
    
    createUploader([
        { id: '001', name: '1.txt' },
        { id: '002', name: '2.doc' },
        { id: '003', name: '3.zip' },
        { id: '004', name: '4.txt' },
        { id: '005', name: '5.exe' }
    ])
    

    然后就能看到如下显示,点击上传按钮,就会在控制台输出“上传 xx 文件”。

    这个实现里我们为每个文件都创建了一个上传控件实例,当我们要同时绘制两千个文件时,就可能遭遇内存危机。接下来我们用享元模式来重写这个实现:

    // Upload 类没有变化
    
    // 首先使用单例模式创建唯一的上传控件
    // 单例模式的实现类
    const ProxySingleton = (function() {
        let instance
        
        return function(singleClass, ...args) {
            if (!instance) instance = new singleClass(...args)
    
            return instance
        }
    })()
    
    // 闭包保存未实例化的文件信息
    const uploaderManager = (function() {
        const uploadInfo = {}
    
        return {
            /**
             * 上传文件
             * @param {string} fileId 要执行上传的文件 id
             */
            exec(fileId) {
                // 获取并设置文件信息
                const uploader = new ProxySingleton(Upload)
                uploader.set(fileId, uploadInfo[fileId].name)
                // 执行上传
                uploader.exec()
            },
            create(files) {
                files.forEach(file => {
                    // 保存文件信息
                    uploadInfo[file.id] = { name: file.name }
    
                    // 创建上传文件
                    const dom = document.createElement('div')
                    dom.innerHTML = `
                        <span>${file.name}</span>
                        <button class="upload">上传</buton>
                    `
    
                    // 绑定上传按钮的点击事件
                    dom.querySelector('.upload').onclick = () => this.exec(file.id)
                    document.body.appendChild(dom)
                })
            }
        }
    })()
    
    uploaderManager.create([
        { id: '001', name: '1.txt' },
        { id: '002', name: '2.doc' },
        { id: '003', name: '3.zip' },
        { id: '004', name: '4.txt' },
        { id: '005', name: '5.exe' }
    ])
    

    然后就可以发现实现的效果和之前一样,这次无论我们要上传多少文件,实际都只创建了一个上传控件。

    什么使用使用享元模式

    • 一个程序中使用了大量类似的对象,并由此产生了很大的内存开销
    • 这些对象中的 不同属性都可以抽取出来。

    注意第二点,我们上面可以使用享元模式的一个主要因素就是,可以在上传控件创建之后更新其属性,当我们需要用到某个控件实例时,我们先更新了它的具体属性,然后再执行了上传操作。如果我们使用了 npm 上的第三方包(无法修改源码)且只能在创建对象时设置其属性(无法更新属性),那么就完全没办法使用享元模式了。

    内部状态与外部状态

    享元模式中,所有可以抽取出来的属性被称为外部状态,而所有无法抽取出来的属性就被称为内部状态。再上面的例子中,fileIdfileName 都是外部属性,并且没有内部属性。

    如果我们需要使用不同的控件进行上传(如 H5flash),那么就需要再上传控件创建时提供一个 uploadType 来对两种控件进行区分,这个 uploadType 就是内部状态,当所有的状态都是内部状态时,那享元模式就不存在了(无法进行复用,需要为所有状态组合创建实例 )。

    小结

    享元模式是为了解决性能问题而生的,这和大多数设计模式的诞生原因(更好的进行维护)都不同。当遇到大量类似对象时,就可以考虑使用享元模式进行开发。

    10、职责链模式

    职责链模式是指,对于传入的参数来说,若不同的参数组合存在不同的解决逻辑,那么就应该将这些不同的解决逻辑定义在不同的方法里,然后将这些方法连成链条。当被调用时,请求就会在这条链上依次传递,直到遇到可以处理它的方法,从而解决问题。在我看来,职责链模式非常适合处理大量嵌套 if-else 的代码。接下来来看一个坏例子:

    假如我们有一个电商平台正在售卖手机,该手机接受过两次预定,对于预定的用户来说,不会存在手机没货,而对于普通购买的用户来说,可能会抢不到手机。

    /**
     * 购买手机
     * @param {number} orderType 0 代表没有预购,1 代表参与了第一次预购,2 代表参与了第二次预购
     * @param {boolean} hasPay 是否支付了定金
     * @param {number} stock 剩余手机库存
     */
    const order = function(orderType, hasPay, stock) {
        // 如果参加了第一次预购
        if (orderType === 1) {
            // 如果支付了定金
            if (hasPay) console.log('已支付定金,得到 100 元优惠券')
            // 没有支付定金等于没有预购
            else {
                if (stock > 0) console.log('普通购买')
                else console.log('库存不足')
            }
        }
        // 如果参加了第二次预购
        if (orderType === 2) {
            // 如果支付了定金
            if (hasPay) console.log('已支付定金,得到 50 元优惠券')
            // 没有支付定金等于没有预购
            else {
                if (stock > 0) console.log('普通购买')
                else console.log('库存不足')
            }
        }
        // 没有预购
        else {
            // 有库存就可以购买
            if (stock > 0) console.log('普通购买')
            // 没有库存就买不了
            else console.log('库存不足')
        }
    }
    

    虽然我们得到了意料中的运行结果,但是这远远算不上一段值得夸奖的代码(书中原话 )。可以看到就算是注释完整,但是过多的 if-else 嵌套依旧会让后期维护变得压力巨大,并且如果有新的需求,例如根据用户等级发放不同的优惠劵,那么就需要对所有可能涉及到的分支进行修改。接下来我们尝试解耦一下这段代码:

    // 第一次预购订单
    const firstOrder = function(orderType, hasPay, stock) {
        if (orderType === 1 && hasPay) console.log('已支付定金,得到 100 元优惠券')
        else secondOrder(orderType, hasPay, stock)
    }
    
    // 第二次预购订单
    const secondOrder = function(orderType, hasPay, stock) {
        if (orderType === 2 && hasPay) console.log('已支付定金,得到 50 元优惠券')
        else normalOrder(orderType, hasPay, stock)
    }
    
    // 普通订单
    const normalOrder = function(orderType, hasPay, stock) {
        if (stock) console.log('普通购买')
        else console.log('库存不足')
    }
    

    我们把不同订单涉及到的业务逻辑进行了拆分,消除掉了重复的代码。但是新版本的代码依旧非常“僵硬”,请求传递的代码和业务代码还是耦合在一起,就像一条环环相扣的链条,如果要在中间新增节点,就要先砸烂这根链条。

    为了解决这个问题,接下来我们用真正的职责链模式来重写这段代码:

    // 第一次预购订单
    const firstOrder = function(orderType, hasPay, stock) {
        if (orderType === 1 && hasPay) console.log('已支付定金,得到 100 元优惠券')
        else return false
    
        return true
    }
    
    // 第二次预购订单
    const secondOrder = function(orderType, hasPay, stock) {
        if (orderType === 2 && hasPay) console.log('已支付定金,得到 50 元优惠券')
        else return false
    
        return true
    }
    
    // 普通订单
    const normalOrder = function(orderType, hasPay, stock) {
        if (stock) console.log('普通购买')
        else console.log('库存不足')
    
        return true
    }
    

    现在的代码和刚才最大的区别就是节点之间没有了联系,如果本节点无法处理的话,那么他就会返回 false,接下来实现链条节点:

    // 链条节点类
    const Chain = function(fn) {
        this.fn = fn
        // 他的下一个节点
        this.successor = null
    }
    
    // 设置下个节点
    Chain.prototype.setSuccessor = function(successor) {
        return this.successor = successor
    }
    
    // 执行请求
    Chain.prototype.exec = function(...args) {
        const result = this.fn(...args)
        // 如果本节点处理不了,就执行下个节点
        if (!result) return this.successor && this.successor.exec.apply(this.successor, args)
    
        return result
    }
    

    然后我们把上面三个方法组装成链条:

    const chainFirstOrder = new Chain(firstOrder)
    const chainSecondOrder = new Chain(secondOrder)
    const chainNormalOrder = new Chain(normalOrder)
    
    chainFirstOrder.setSuccessor(chainSecondOrder)
    chainSecondOrder.setSuccessor(chainNormalOrder)
    
    chainFirstOrder.exec(2, false, 1) // 普通购买
    

    可以看到,业务代码和请求传递代码完全解耦开来,并且只要有需要,完全可以在不深入内部实现的情况下在链条上新增和删除节点。然而这么实现依旧有点小瑕疵,那就是我们在执行请求时调用的是 chainFirstOrder,但是它却可以处理所有情况,这多少有点“文不对题”的感觉,在后期维护时可能会让人困惑。

    AOP 实现职责链

    接下来我们使用 AOP(面向切面编程)来实现职责链的功能:

    // 给方法定义下一个节点
    Function.prototype.after = function(fn) {
        // 返回一个可执行的方法
        return (...args) => {
            const result = this(...args)
            // 如果处理不了就执行下个节点
            if (!result) return fn(...args)
    
            return result
        }
    }
    
    // 生成职责链
    const order = firstOrder.after(secondOrder).after(normalOrder)
    
    order(2, true, 0) // 已支付定金,得到 50 元优惠券
    

    可以看到,用 AOP 来实现职责链不仅简单而且巧妙,最后的链式调用更是简明的描述了“链条”的前后关系。通过在原型上闭包保存下一个要执行的方法来实现链式调用。

    小结

    职责链模式对于解耦大型函数非常有帮助,只要运用得当,职责链就能很好的帮助我们管理和组织代码。正因为其普适性,作用域链、原型链、事件冒泡等非常基础的机制中都存在职责链的影子。所以这个设计模式非常有必要好好理解一下。

    11、中介者模式

    当对象的数量过多时,对象与对象之间的联系会变得千丝万缕越来越复杂。而中介者模式就是把对象与对象之间的网状关系转换成对象与中介者之间的一对多关系。从而减轻了对象之间的耦合。

    接下来我们以机场日常为例,飞机在降落时需要向塔台请求降落,塔台允许后才能进行降落,这里的塔台就是中介者,现在我们来假设没有塔台时会是什么样子的:

    // 空中所有的飞机
    let airplanes = []
    
    // 飞机类
    const Airplane = function(name) {
        this.name = name
        // 是否正在着陆
        this.landing = false
    
        airplanes.push(this)
    }
    
    // 请求着陆,需要遍历所有飞机,检查是否有人正在着陆
    Airplane.prototype.requireLand = function() {
        return !airplanes.find(airplane => airplane.landing)
    }
    
    // 开始着陆
    Airplane.prototype.land = function() {
        console.log(`${this.name} 已着陆`)
        this.landing = true
    }
    

    飞机需要询问机场边上的每一架飞机,之后才能决定要不要降落,这对于每架飞机来说都是不小的压力,现在我们把塔台加回来。

    // 飞机类
    const Airplane = function(name) {
        this.name = name
        // 是否正在着陆
        this.landing = false
    }
    
    // 请求着陆,需要遍历所有飞机,检查是否有人正在着陆
    Airplane.prototype.requireLand = function() {
        AirportTower.requireLand(this)
    }
    
    // 开始着陆
    Airplane.prototype.land = function() {
        console.log(`${this.name} 已着陆`)
        this.landing = true
    }
    
    // 多飞一会
    Airplane.prototype.keep = function() {
        console.log(`${this.name} 继续盘旋`)
    }
    
    // 机场塔台
    const AirportTower = (function() {
        // 监控空中所有的飞机
        let airplanes = []
        // 当前是否有飞机正在降落
        let cantLand = false
    
        return {
            // 请求降落
            requireLand(airplane) {
                // 如果已经有飞机在降落了就不去检查其他飞机了
                if (cantLand) return airplane.keep()
    
                // 塔台确认没有降落中的飞机
                const hasAirplaneLand = airplanes.find(other => {
                    if (other == airplane) return false
                    return other.landing
                })
    
                // 引导飞机进行降落
                if (hasAirplaneLand) airplane.keep()
                
                airplane.land()
                cantLand = true
            },
            // 注册驶入范围的飞机
            addAirplane(airplane) {
                airplanes.push(airplane)
            }
        }
    })()
    

    机场塔台管理着所有的飞机,并且会引导飞机进行后续行动,现在每架飞机都只需要和塔台通讯即可,并且由于塔台掌握着所有飞机的动向,所以他也可以通过保存一些状态(cantLand)来提高机场的运作效率,接下来测试一下:

    const airplaneA = new Airplane('A')
    AirportTower.addAirplane(airplaneA)
    const airplaneB = new Airplane('B')
    AirportTower.addAirplane(airplaneB)
    
    airplaneA.requireLand() // A 已着陆
    airplaneB.requireLand() // B 继续盘旋
    

    小结

    在大多数情况下,中介者模式和观察者模式(订阅-发布模式)很像,他们都提供了一个中间对象来服务于其他对象。都用于和其他对象相解耦。只是两者的侧重点并不相同,中介者模式着力于解决多个对象之间的复杂网状关系,而观察者模式则解决了 pull 模型中信息获取效率差的问题。

    虽然中介者模式可以让多个对象之间相互解耦,但是这只是把对象之间的复杂度转移到了对象和中介者之间的复杂度,随着需求的逐渐增加。中介者依旧会变得越来越难以维护。所以要先看清对象之间的耦合度是否合理,然后再决定是否使用中介者模式。

    12、装饰者模式

    装饰者模式是指在 不改变原有功能 的基础上为其增加新的功能。这个模式有点类似于继承,但是要比继承更加轻量且更加黑盒。你可以定义多个装饰器,并以“组装”的形式快速生成不同种类的“子类”。如果使用继承的话,就需要僵硬的为每种组合形式都定义一个子类。

    首先以一架游戏里的战斗机为例,为其添加新的攻击手段:

    // 定义一架飞机
    const plane = {
        fire: () => console.log('发射子弹')
    }
    
    
    // 飞机攻击模块的装饰器
    const missileDecorator = () => console.log('发射导弹')
    
    // 备份原来的功能
    const fire1 = plane.fire
    // 添加新的功能
    plane.fire = () => {
        fire1()
        missileDecorator()
    }
    
    plane.fire() // 发射子弹 发射导弹
    

    可以看到这种写法 在保留原有功能的基础上为其增加新的功能,确实是很有 js 风格的装饰器模式,但是这种实现并不好看,它有下面这两个问题:

    • 存在中间变量,我们要新建 fire1 来保存原来的方法,当需要进行多次装饰时会导致这种中间变量越来越多。
    • this 可能被劫持,这里没有遇到这个问题,但是当原有的函数里使用了 this 的话,随着你中间变量的赋值,原有函数中的 this 就可能被指向新的作用域导致代码的崩溃出错。

    接下来使用上节中提到的 AOP 对上面的功能进行重写:

    Function.prototype.after = function(fn) {
        // 保存原始引用
        const self = this
        // 返回新函数
        return function(...args) {
            // 保证 this 指向正确
            const result = self.apply(this, args)
            fn.apply(this, args)
            return result
        }
    }
    
    // 添加装饰器
    plane.fire = plane.fire.after(missileDecorator)
    

    之后再执行 plane.fire() 就可以看到完全一样的输出。其实这里我对其中 this 和 self 的指向问题产生了一些困惑,具体的解释见 使用 AOP 实现装饰器模式中的 this 指向探究

    通过上面这种形式,我们可以在保证原有功能不变且黑箱的情况下为其添加新的功能。下面是两个应用场景:

    • 数据统计:在网页中很多功能都需要进行统计工作,例如某个按钮点击了多少次,如果要把这些统计代码放在业务代码里就会显得很臃肿且难以维护,这时我们就可以通过装饰器为业务代码所在的函数添加统计功能,这种装饰器模式还可以让我们批量给很多功能添加相同类型的统计功能,并且如果有新的统计需求的话,不必改写之前的代码,直接添加新的装饰器即可:
    const login = () => console.log('登陆功能')
    const stats = () => console.log('统计数据')
    
    login = login.after(stats)
    
    document.getElementById('loginButton').onclick = login
    
    • 修改对象的值:在项目中有一个方法封装了所有的请求,当我们需要给每个请求中添加新的参数时,就可以使用装饰器:
    let ajax = function(type, url, param) {
        console.log(`向 ${url} 发送 ${type} 请求,参数为 ${JSON.stringify(param)}`)
        // 请求代码略
    }
    
    // 获取新参数的函数
    const getToken = () => 'token'
    
    // 在发送前先添加新参数, before 和 after 的唯一区别就是 before 会先指向装饰器函数
    ajax = ajax.before((type, url, param) => {
        param.token = getToken()
    })
    
    ajax('GET', 'baidu.com', { a: 1 }) // 向 baidu.com 发送 GET 请求,参数为 {"a":1,"token":"token"}
    

    装饰器模式和代理模式的区别

    装饰器模式和代理模式都保存了对原始对象的引用,并且会把请求转发过去。他们两者的区别在于他们的意图和目的。代理模式是介于调用者和处理者之间的一个中间人,负责对请求做一些“检查”或者“升级”操作,并在必要的时候拦截请求,总而言之,代理模式是服务于处理者的。而装饰器模式则是为原始的处理者增添新的功能,这些新功能和原先的功能是“平级”的。

    从目的上来看,代理模式最终返回的结果和本体是一致的,例如在本体没加载好之前先暂存请求并返回友好一点的信息。而装饰器模式则是实打实的在原先的功能上增添新的职责和行为。

    小结

    装饰器提供了一个方法让我们可以在不侵入原先功能的基础上增加新的功能,这对于提高复用性和可移植性是很有帮助的,因为无论再怎么修改,只要需要,都可以随时访问到未装饰时的功能。并且,合理的使用装饰器,也可以快速增加一些个性化需求,从而避免为了预测用户需求而使用了过多的 if-else。

    13、状态模式

    状态机想必大家都不陌生,当事物内部的状态改变时,该事物的行为就会对应的发生改变。例如电灯开关,当我们按下开关时,电灯会亮弱光;再次按下时,电灯会亮强光;第三次按下时,电灯会被关闭。这个电灯开关就是一个典型的状态机,接下来我们实现一个这个电灯:

    反例

    const Light = function() {
        this.state = 'off'
    }
    
    Light.prototype.click = function() {
        if (this.state === 'off') {
            console.log('弱光')
            this.state = 'weak'
        }
        else if (this.state === 'weak') {
            console.log('强光')
            this.state = 'strong'
        }
        else if (this.state === 'strong') {
            console.log('关灯')
            this.state = 'off'
        }
    }
    
    const light = new Light()
    
    light.click() // 弱光
    light.click() // 强光
    light.click() // 关灯
    light.click() // 弱光
    

    这段代码很好的满足了我们的要求,但是它称不上一段漂亮的代码,我们来分析一下它的缺点

    • 所有的分支条件都包含在 click 方法中并通过 if-else 分割,当我们需要添加新的状态(例如超强光和究极闪光)时,我们就需要深入这段代码然后在对应位置添加新的 if-else。最终这个方法会膨胀到难以维护。
    • 状态的不明确性,除非我们通读整段代码,不然我们很难清晰的看到 有多少种状态 以及 状态的切换关系 / 顺序,并且用字符串标注状态切换条件也比较危险。
    • 没办法高效的进行复用,当我们又有了一种新的电灯,并且电灯的开关顺序为:“关灯 > 强光 > 弱光 > 关灯”时,我们就需要把这一大堆代码完整的复制粘贴一遍,然后再深入 click 修改其对应的切换顺序。

    好一点的实现

    const LightFSM = {
        off: {
            click() {
                console.log('弱光')
                this.state = LightFSM.weak
            }
        },
        weak: {
            click() {
                console.log('强光')
                this.state = LightFSM.strong
            }
        },
        strong: {
            click() {
                console.log('关灯')
                this.state = LightFSM.off
            }
        }
    }
    
    const Light = function() {
        this.state = LightFSM.off
    }
    
    Light.prototype.click = function() {
        this.state.click.call(this)
    }
    
    const light = new Light()
    
    light.click() // 弱光
    light.click() // 强光
    light.click() // 关灯
    light.click() // 弱光
    

    可以看到我们把所有的状态都抽象到了 LightFSM 里,然后再初始化 Light 对象时指定一个起始状态,最后在 click 方法中把请求转发给对应的状态即可。注意在转发时要绑定 this,因为状态机内部需要访问到 Light 对象来修改其当前状态 this.state

    舒服多了,抽象之后状态的边界更加清晰了,这让我们对状态的维护和增删更加的轻松。但是这种实现依旧有点问题,首先在 LightFSM 中的方法修改了外部对象 Light 的状态,这增加了两者之前的耦合度,并且会对不了解项目的维护人员带来困惑(明明 LightFSM 里没有定义 state 属性也没有相关的应用,为什么这里进行了修改?)。并且状态的切换条件和状态耦合在了一起,当我们想创建两个状态相同但切换顺序不同的电灯按钮时依旧需要复制粘贴。

    现在我们来解决上面提到的问题:

    更好的实现

    // 设置下一个切换条件
    Object.prototype.setNext = function(next) {
        this.next = next
        return next
    }
    
    // 将状态机改造为类
    const LightFSM = function() {
        return {
            off: {
                click() {
                    console.log('弱光')
                }
            },
            weak: {
                click() {
                    console.log('强光')
                }
            },
            strong: {
                click() {
                    console.log('关灯')
                }
            }
        }
    }
    
    const Light = function() {
        // 新建状态机并设置其顺序
        const fsm = new LightFSM()
        this.state = fsm.off.setNext(fsm.weak).setNext(fsm.strong).setNext(fsm.off)
    }
    
    Light.prototype.click = function() {
        this.state.click.call(this)
        // 设置完成后切换顺序
        this.state = this.state.next
    }
    
    const light = new Light()
    
    light.click() // 弱光
    light.click() // 强光
    light.click() // 关灯
    light.click() // 弱光
    

    可以看到,LightFSM 中的状态完全不包含任何切换关系,具体的切换关系变成了在 Light 构造器里指定,这种实现不仅更加清晰,让人一眼就能看到状态的切换顺序,并且在新建不同的电灯时,我们也可以通过调整 setNext 的顺序轻松的进行完成工作。

    需要注意的是,我们这里把 LightFSM 改造成了一个类,并在 Light 初始化同步创建一个 fsm 实例,这样是为了避免原型污染,比如下面的例子,我们从 LightA 创建的实例就错误的被修改了开关的执行顺序:

    // 全局唯一的状态机
    const LightFSM = {
        off: {
            click() { console.log('弱光') }
        },
        weak: {
            click() { console.log('强光') }
        },
        strong: {
            click() { console.log('关灯') }
        }
    }
    
    // 新增两个切换顺序不同的类
    const LightA = function() {
        this.state = LightFSM.off.setNext(LightFSM.strong).setNext(LightFSM.off)
    }
    const LightB = function() {
        this.state = LightFSM.off.setNext(LightFSM.weak).setNext(LightFSM.off)
    }
    
    // 初始化实例
    const light1 = new LightA()
    const light2 = new LightB() // 注意这行代码污染了 `LightFSM`,导致 light1 的切换顺序被修改了
    
    // light1 执行了 light2 应该执行的切换顺序
    light1.click() // 弱光
    light1.click() // 强光
    light1.click() // 弱光
    light1.click() // 强光
    

    小结

    状态模式可以有效的拆分状态机中各个状态的代码,让原本杂乱无章的逻辑更容易维护和拓展。在设计状态机时应首先考虑用这种办法进行实现,此处并未涉及表驱动的状态机

    14、适配器模式

    适配器模式很简单,就是用来解决不同数据源或者不同模块的兼容问题,下面是两个例子:

    不同模块间的兼容

    假如项目中已经实现了谷歌地图的接入,并且同事也已经完成了地图渲染的逻辑封装。突然有一天老板让你加入对百度地图的支持,但是百度地图的 api 和谷歌地图有些出入,没办法直接传递给同事完成的渲染方法。在两边都没办法修改的情况下,我们就可以使用适配器对百度地图进行封装:

    // 来自第三方模块
    const googleMap = {
        show: () => console.log('开始渲染谷歌地图')
    }
    const baiduMap = {
        display: () => console.log('开始渲染百度地图')
    }
    
    // 来自同事封装好的模块
    const renderMap = function(map) {
        map.show()
    }
    
    // 我们自己写的适配器
    const baiduMapAdapter = {
        show: () => baiduMap.display()
    }
    
    // 渲染地图
    renderMap(googleMap) // 开始渲染谷歌地图
    renderMap(baiduMapAdapter) // 开始渲染百度地图
    

    不同数据源的兼容

    和上面情况相同,当我们突然拿到了新的数据源,但是现有的系统非常依赖于原先数据源的结构。这时我们为了避免对现有系统进行大刀阔斧的修改,就可以使用适配器对新的数据源进行改造:

    // 两个数据源
    const oldData = [
        { key: 'itemA', value: 1 },
        { key: 'itemB', value: 2 },
        { key: 'itemC', value: 3 }
    ]
    const newData = {
        itemD: 4,
        itemE: 5,
        itemF: 6
    }
    
    // 使用数据
    const showData = function(dataSource) {
        console.log(dataSource.map(d => `键 ${d.key} 值 ${d.value}`).join('\n'))
    }
    
    // 新数据的适配器
    const dataAdapter = function(dataSource) {
        return Object.keys(dataSource).map(key => ({ key, value: dataSource[key] }))
    }
    
    showData(oldData)
    showData(dataAdapter(newData))
    // 输出:
    // 键 itemA 值 1
    // 键 itemB 值 2
    // 键 itemC 值 3
    // 键 itemD 值 4
    // 键 itemE 值 5
    // 键 itemF 值 6
    

    小结

    适配器模式是一种“亡羊补牢”式的设计模式,没有人会在一开始设计系统的时候使用这种模式(也没地方用),当我们出现了新的需求但是不方便修改原有的代码时,不妨使用适配器让新增的需求去适应已经存在的系统。

    总结

    以上就是《JavaScript 设计模式与开发实践》这本书中提到的全部 14 种设计模式,其实就 js 而言,其特别的原型链设计导致了和其他多数面向对象语言的设计模式实现都不太相同,特别是一些需要继承的设计模式,在书中详细论述了 js 和其他语言在实现中的区别,而本文为了更专注于如何实现,特别精简掉了这些内容。其实就算用 js 去模仿这些语言进行实现,也远没有直接用 js 的特性进行实现要来的简洁轻巧。

    这些设计模式中,闭包和原型链的使用非常的常见,而 this 作用域的重定向也很精髓,虽然有些难以理解,但是作为 js 雷打不动的特性。这些内容还是很有必要学习的,而深入学习设计模式,更有助于了解这些特性。

    相关文章

      网友评论

          本文标题:js 中的 14 种设计模式(下)

          本文链接:https://www.haomeiwen.com/subject/secgfktx.html