JavaScript设计模式返璞归真

闲话

本文写于2月初,原分享在有道云笔记上,现在转移到此。现阶段对于设计模式的理解还没有能力对本文的框架做出大的翻新,所以暂且原样搬过来了。

本文不从名目众多的设计模式谈起,而是为语言能力、代码优化、交互关系三个方面寻找解决方案,从而引出设计模式的使用和作用。覆盖了《 javascript设计模式与开发实践 》提到的全部15种javascript内常用的设计模式,具体设计模式的讲解可以翻阅此书。

一、语言特性

动态语言:程序运行,变量被赋值时才具有了类型
高阶函数:函数的参数和返回值,都可以是函数
原型继承:无严格类,通过this和原型实现赋值和继承

优点:天然面向接口,无需向上转型。创建单例无需声明类。调用函数无需创建对象。
缺点:无类别,错误容易隐藏。添加对象的初始化约束(如不按指定参数创建 throw error),繁复且不容易。

二、编程原则

设计模式的存在意义,是服务于“最佳编程实践”的思想

  1. 单一职责(SRP):一个对象(方法)只做一件事
  2. 最少知识原则(LKP 又称迪米特法则):如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的联系
  3. 开放-封闭原则:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改

三、场景分类

设计模式的应用场景可大致分为3类:

语言能力:语言层面的能力,能实现什么样的功能
Ⅰ:单一对象 | ①功能性用法 ② 工具性用法
Ⅱ:功能增强 | ① 平面(直接扩展) ② 切面(AOP) ③ 时间轴(多对象复用)

代码优化:抽离 “可变” 与 “不变”,提炼函数,优化代码,使维护容易
Ⅰ:提炼函数
Ⅱ:分支优化 | ① 单一因素 ② 多因素 ③ 涉及不同状态
Ⅲ:享元与模板

交互关系:简化各个对象的交互耦合模型,解耦或者降低耦合

四、解决方案

1、语言能力:(单一职责原则、开放-封闭原则)

开发中,通常遵循单一职责原则,一个对象(方法)只做一件事。
这引出了两个重要的话题:

  • Ⅰ:单一对象的功能结构
  • Ⅱ:如何对一个对象实现“功能增强”

Ⅰ:单一对象

单一对象的功能结构分两类:常规(不做讨论)、闭包

闭包:(闭包实现的常规方式都能实现。闭包的作用是保存变量的状态,但不会污染环境)

JavaScript设计模式返璞归真_第1张图片

图一:功能性用法,用途体现在函数体本身。对需要记录状态的功能使用闭包(如 命令模式 的撤销undo)

图二:工具性用法,作为其他函数或对象的辅助工具。(自执行为单例,手动执行为工厂。图三为面向对象式)

① 宏命令
命令集合,一并执行。本质为 cache、add、iterator 的组合。
配合 “命令模式”,如表单验证的规则,可依次添加,打包进行

发布-订阅模式(观察者模式)
本质为 cache、add、remove、iterator 的组合,多了事件名指定 cache={“click”: [fn1,fn2…], “move”: [fn1] …… }
通常对应名字 listen、remove、trigger

组合模式
本质为 cache、add、iterator 的组合。推入堆栈的是组合对象,全部都实现 iterator 接口,执行时调用所有对象的 iterator 进行深度遍历。
叶节点需单独定义,到此终止。可增加指定 parent 的功能,实现自我栈出、删除等功能

Ⅱ:功能增强

利用特性:闭包、高阶函数

① 平面(代理、工具式包装)

JavaScript设计模式返璞归真_第2张图片

图一,典型的 代理模式,以基本功能函数为框架,实现细节上的功能补充。
比如“只允许函数产生一个实例(单例模式)”、“每300毫秒只触发一次(函数节流)”、“函数支持json格式(内部转为顺序参数)”等

代理模式的理想情况:为了减少使用者的记忆负担,proxyFn 与 fn(可以是对象) 的接口和需要传入参数一致。(— fn 的 平稳升级 —)
增强版:接口的第一个参数设置为 Context 环境。(比如原函数是 fn1.fn , 可手动指定以 fn1 调用代理函数)

图二,变体, 被代理者位置从 “参数” 换到 “调用者”

JavaScript设计模式返璞归真_第3张图片

图三(对象池)、图四(外部迭代器,如游标),工具式包装,针对性提供一套接口体系。
“适配器模式” 也比较类似一个简化的工具式包装

② 切面(AOP)

JavaScript设计模式返璞归真_第4张图片

当逻辑的扩充,出现在原函数的切面,而非本身时
图一,“装饰者模式”,实现AOP切面编程。还可用“职责链模式”,参照下面的分支优化②,可构造 “面向切面编程” 及相关逻辑,实现 开放-封闭原则。(单一职责,不要糅合其他作用)
可以链式调用,return值加入判断,则可实现分支判断、终止链条。也可实现为 after(fn, nextFn)这种形式。

“职责链模式”是基于引用,”装饰者模式”是基于封装。因此”职责链模式”还能够实现异步AOP,而”装饰者模式”不行。

③ 时间轴(每次函数执行的数据都能被存储、索引、操作)
有序保存 fn 每次运行时产生的数据,需要利用 “堆栈”(数组或对象)

JavaScript设计模式返璞归真_第5张图片

两种方式:
1. 图一,fn能返回所需数据时,只需把所有需要的数据推入堆栈(如实现 “命令模式” 的撤销N步、重演等)
2. 图二,fn只能操作数据时,需构建create方法提供API操作 fn返回的接口,并把API集合挂载在外层堆栈上(闭包,复用活动对象。活动对象被API的[[scope]]引用,不会销毁)
如:对基本的 “发布-订阅模式” 增加 namespace 寻址。由于 fn 返回的接口只能操作数据,无法把数据推入外部堆栈,必须复用活动对象(方法2)

2、代码优化:(开放-封闭原则、提炼函数)

书写代码通常的原则是:分离 “可变” 与 “不变”。
姑且分为三类:提炼函数分支优化享元与模板

Ⅰ:提炼函数

有时候函数显得肿胀不堪,导致主逻辑看不分明,此时也许需要提炼了

JavaScript设计模式返璞归真_第6张图片

图一,可以分离组件,“命令模式” 封装成命令抽离出来,当做一个开关使用
某个 if 中的判断条件比较复杂,不直观时也可抽离封装,如季节月份判断:isSummer()。多次出现的代码片段也可以抽离封装起来。

Ⅱ:分支优化

当需求经常变化,对主逻辑内分支的修改可能会使代码难以维护,而且庞大的分支也影响了业务逻辑的意图表达
① “单一因素” 形成的 if-else 分支

JavaScript设计模式返璞归真_第7张图片

1、 由自己指定单一变量:图一,使用 “策略模式” 抽离成若干策略,如图二。

JavaScript设计模式返璞归真_第8张图片

2、 自主判断(浏览器兼容等)的分支,可用两种方式抽离:
懒加载(图三)、预加载(图四);⑵ “策略模式” + “迭代器模式”(仅当需个性化指定尝试顺序时,用 return “success” 终止)

② “多因素” 较复杂判断(if-else 或 多if)
例如:是否预约、是否预付款、是否是vip等导致优惠(结果)不同这样的复杂判断,可以考虑抽离(按结果分,结果1、2、3),使用面向切面编程串成链来

JavaScript设计模式返璞归真_第9张图片

“职责链模式” 先抽离分支(如图一),构造Chain(图二)串起,通过 return “next” 向下进行,否则中断。既可以 if-else式、if式,还可以异步调用下一个。
属于面向切面编程,跟 AOP(after()、before())的不同是 AOP 是逐层封装,过多层影响性能。AOP不能异步调用下一个。

③ 对象有 “若干状态”, 执行相同操作产生不同反应(多处if-else)
例如:云产品有扫描中、正在上传、暂停中、已成功、失败等状态,每种状态下的取消、暂停等按钮反馈不一,可能充斥大量if-else,不易修改。

JavaScript设计模式返璞归真_第10张图片 JavaScript设计模式返璞归真_第11张图片

图一、图二是典型的“状态模式”。主逻辑是添加按钮,这属于 “不变” 。每种状态下点击事件的相应不同,这属于 “变化”
把变化抽离,每个状态一个单独的对象,让主框架来委托给状态,执行可变的逻辑。
实现的关键在于两者间能够相互引用,相互切换。活用call、apply,或者面向对象式的 this.xxx =xxx

Ⅲ:享元与模板

① “享元模式 ”
有时候,对象太多造成性能灾难,而其中性能耗费严重的属性却相同。(如试穿50件女装只需要1个女模特)

JavaScript设计模式返璞归真_第12张图片

“享元模式”,分离”内部状态”、”外部状态”,每次动作之前需要先找到外部状态对象setState,可写在prototype中
注:主逻辑都在add中,创建对象使用单例代理。(一样的创建对象,一样的执行对象方法,逻辑上与处理前有很好的一致性)

② “模板方法模式”
有时候,创建了很多函数工厂,它们都要求有规定的方法、初始化规则。

JavaScript设计模式返璞归真_第13张图片

“模板方法模式” 使用 init() 定义了类的基本框架,通过 “钩子” 和 throw error 使继承更稳健
上例 “状态模式” 的状态类可以用此种方式优化

3、交互关系:(最小知识原则)

多组件间的耦合关系,有时可以考虑 引入第三方,组件只需要发一个指令,由第三方来通知、改变各组件的状态
通常使用 “中介者模式” (指挥塔,是一种思想)。这非常像 “发布-订阅模式” 的特征

JavaScript设计模式返璞归真_第14张图片

图一, “中介者模式”,若无中介者,每个玩家都要保存一份玩家名单、队友名单,吃药水、死亡等都要遍历名单然后调用其他玩家的接口。
中介者统一保存玩家、状态等,负责调用玩家的接口,玩家只需向其发送信息,使逻辑大大简化。(缺点:中介者可能非常庞大,逻辑复杂)
如果图一,如果增加了动态添加 operations 的方法的API,就是 “发布-订阅模式” 的模型了。

你可能感兴趣的:(JavaScript,设计模式)