以德服人
首发于以德服人

前端和设计模式

年前和朋友聊天,说起了程序设计原则,我说程序设计的原则就是「高内聚,低耦合」吧。


朋友说,「不是,程序设计的原则有六个。」
我说,「对啊,高、内、聚、低、耦、合,不就是六个么?」
朋友一声冷笑,「你们前端果然都不懂程序设计,麻烦你继续往下编。」
我说,你别着急,等我回去编好了再给你讲。

于是乎,我也开始思考这个问题:为什么前端普遍不熟悉设计原则和设计模式呢?

说起来几年前(做讲师的时候),别说是六大设计原则,哪怕是二十三种设计模式,我也能整着背,倒着背,毕竟是靠这个家伙吃饭。对于学生来说,这是经典的计算机理论,是最佳实践,是干货,也是面试时夸夸其谈的底气。但是随着几年的工作,我自己也把这些定义都忘记了,我猜当年的学生们恐怕更不会背得会。背不出来的原因只有一个,那就是这部分知识并没有经常被使用,但当你把23条设计模式列出来的时候,就会发现很多的概念在前端中其实是被经常使用的。所以我们就要来找一找,问题是出在了哪里。


比如 adapter,适配器模式。前一段时间玩了 bearychat 的机器人开发,用到了 hubot-bearychat,就是一个适配器。因为各个第三方服务商之间未必会提供一致的接口,需要有一个中间层适配。
类似的,比如 parse-server 对文件存储可以依赖 s3 或者 qiniu,都可以通过 adapter 实现。
adapter 在物理世界里面大概长这样:


而 proxy 和 iterator 就是 JS 原生功能的一部分,因为兼容的原因,proxy 没有被普遍使用。顺便说一下,虽然有 proxy-polyfill这个东西,但是基本等于没有用,ES5没法完整的模拟出 proxy。要是能用的话,vue 也不至于用 defineProperty 来实现劫持。


decorator 大家都说是 ES7 提出的特性,但是去查一下文档,发现都9102年了,还是个 stage2 draft。不过用 babel 抢跑基本没啥问题。


观察者模式,或者说发布订阅模式,有些人说是一回事,有些人说后者是前者的实现。对于前端来说,这个模式是学习 DOM 的第一课。前端不仅有事件的订阅,还有特殊的传播机制。而且因为有大量的异步场景,所以除了常见的维护订阅列表主动推送这种方式以外,前端也很常见惰性订阅的模式,即订阅方不会即时的对发布内容作出响应,只有在需要使用的时候才进行拉取。而rxjs 算是把观察者模式用到了极致,通过一个模式,解决了复杂的数据依赖问题。

惰性这个词,在设计模式里也有提到,singleton 单例模式就有 lazy 和 eager 两种实现。吐个槽,不知道是把 eager 翻译成了饿汉,所以 lazy 就变成了懒汉,不知道 lazy load 是不是要翻译成「懒汉读取」。

在 JS 中提单例往往也很 DT,因为在 JAVA 中只有类的实例,哪怕只有一个实例存在,也要先定义类,再实例化,而且要保证只有一个,还要线程安全。在 JS 中,反手一个字面量声明,哪个对象不是独一无二的。基于语言谈单例,意义不大,但是这个理念在开发的时候还是能用上的。
比如做一个弹窗组件,需要全局唯一,也就是说,同时最多只能有一个弹窗。你可以搞一个先写好 DOM,隐藏起来,需要的时候再展示的饿汉式弹窗,也可以写一端用 js 语句描述 DOM 的代码,在被调用时再展示的懒汉式弹窗。
饿汉弹窗易于调试,懒汉弹窗更加灵活易用。这算是前端对单例模式的另一种解读吧。

说了这么多,我们追根溯源来看。GoF 在1995年的著作中提出了设计模式,巧的是,布兰登艾克也是在1995年用十天的时间创造了 javascript。所以四巨头也无法预知到,互联网开发会在未来大行其道,况且,设计模式本来就是针对面向对象和静态语言提出的,把这个静态语言的最佳实践,套入动态语言和函数式编程中,难免会有很多水土不服。


我们可以看到,众多的设计模式已经成为了语言的一部分,也正因如此,我们才能把更多的精力投入到数据和业务中。另外说到传统软件业,软件是作为一种工具,既然是工具,和锤子扳手一样,工具都是实体的拓展和延伸,所以以面向对象来构建是恰到好处的。

另外传统软件业是进行一次统一交付,所以需求是封闭的,开发者往往通过构建精巧的架构去实现各种系统功能,架构的总体原则是要对协同开发友好,对版本迭代友好。这意味着别人搭的砖你最好不要动,你可以在基础上扩展。既然如此,那「架构」就十分重要了,因为所有的拓展都是依附于这个框架的,框架搭的好,同时大家遵循开闭原则,可以使得项目保持清晰。

对于前端来说,开闭是天然的,里氏代换原则其实就是 Duck Type。

而且因为交付方式的不同,对于互联网开发来说,只写「恰到好处」的代码往往不是最佳实践。接口数据要不要考虑冗余,大量的单例组件还需要遵循单一职责原则么?当你难以预期部分代码是否会被复用的时候,拆分可能意味着提前优化,而把组件拆得零散,又是违反了最小知识原则。说到最小知识,倡导只和朋友通信,可是父子通信的组件设计用起来非常的笨重。这么说起来,redux 好像是个反模式的东西。但 store.dispatch 很明显是个命令模式,state又可以说是状态模式。

所以你看,前端不是不用设计模式,而是已经把设计模式融入到了开发的基础当中。

至于说六条设计原则:
层模块不依赖底层模块,即为依赖倒转原则。
部修改关闭,外部扩展开放,即开闭原则。
合单一功能,即为单一职责原则。
知识要求,对外接口简单,即迪米特法则。
合多个接口,不如独立拆分,即接口隔离原则。
成复用,尽量不使用继承,即合成复用原则。

设计原则是通用的,但是要看其理念而不是形式,把传统软件开发时代的最佳实践放在前端的环境中,并不是完全适用的。至于说23条设计模式,大半都已经融入到日常开发之中了,作为语言的基础,并不是什么值得炫耀的难点知识。

这些模式和原则可以作为知识整合的脉络和代码优化的理念来看,但偏要以 JS 语法去实践一些设计模式,并没有什么实际的工程意义。


「好的,你要的解释我已经编好了」,前两天我把这些内容发给了朋友。「况且我觉得,恐怕也不会真的有做FE的朋友能把23条从头到尾都背一遍。」

「如果有这样基础扎实的人呢?」

「怕不是刚刚背好要去面试的。」

编辑于 2019-02-11

文章被以下专栏收录