近几年前端技术迭代迅速,除了ES6,Vue,React,Angular,各种企业级框架也层出不穷,比如egg,umi,nuxt,next,koa等等,还有redux, vuex, Mobx, Flux,dva等状态管理库,这些给前端开发工作带来了极大的简化,同时也给开发者很大的学习挑战。但是想要在前端架构层面有一定的高度,这些都是必须要掌握的。除了这些,还要掌握工程化,自动化,服务端等。那么这些类库或者框架剥离业务之后,其背后的设计思想以及遵循的底层程序原理是怎样的呢?
很多开发者对于知识的掌握,框架的应用都很熟悉,但是总有一些角落感觉没有深入进去,这可能是因为对于程序本身没有一个全局的认识。举个例子(不识庐山真面目,只缘身在此山中),好比在一座山之中,很多人可以很清楚的知道山里的路,但可能不会知道路为什么是这个走向,要想了解山势,就要走出这座山,从更高的地方观看一目了然。
所以做程序不仅要走得进来,更要能够走得出去,用最简单的方式思考问题,抓住程序的底层思维,设计出程序路径,至于用什么方法实现反而不那么重要了。
一年前和一位后端高级架构专家讨论前端如何能够快速上手开发后端需求,当时我觉得是先要学好java或者其他语言,因为这是开发的基础,如果基础语法和知识都不清楚的话,怎么开发呢。不过他说真正的程序开发,不管是前端和后端,和语言的关联程度并不是排在第一位得,真正排在第一位的是程序实现目标和实现模式。事实上也是如此,语言和框架有很多,但是这些都是开发的工具,我们要掌握的是开发的思想。体会并深入掌握这一点后,能少走很多弯路。
计算机程序是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。
系统的三大因素:结构化的数据,操作数据的逻辑(增删改查),展示数据(ui层)
程序执行时通过顺序,判断,循环实现增删改查,这就要求数据有一定的结构,才方便操作。
所以程序实现的目标是将宏观事物抽象为结构化的数据,并覆以操作数据的方法。
常用的数据结构有Set, Map, Tree, Array,链表,图等等,而操作这些就要有对应的方法,这些方法基于一定的实现准则,就是我们所说的设计原则。
说明:单一职责原则(SRP),开放封闭原则(OCP) 在javascript开发中应用非常广泛,
里氏替换原则(LSP),接口隔离原则(ISP),依赖倒置原则(DIP) 在javascript种应用较少,在TypeScript中可以体现。
子类能覆盖父类
父类出现的地方子类就能出线
保持接口的单一独立,避免出现旁接口(不是100%,像外观模式,后面详细介绍)
类似于单一职责原则,但是这里更注重于接口
面向接口编程,依赖于抽象而不依赖于具体
使用方法只关注接口而不关注具体类的实现
开闭原则(OCP)是面向对象设计原则的基础也是整个设计的一个终极目标,而依赖倒置原则(DIP )则是实现OCP原则的一个基础,换句话说开闭原则(OCP)是你盖一栋大楼的设计蓝图,那么依赖倒置原则就是盖这栋大楼的一个钢构框架。
设计和模式是分开的,有了设计才产出了模式。设计是指导思想,模式是根据指导思想抽象出来的一些模板。
从功能上划分为三类:
创建型,组合型,行为型
具体到每个类型又细分如下, 如下图:
在讲设计模式之前,介绍下UML类图:详细参见UML类图
在UML的静态机制中类图是一个重点,它不但是设计人员关心的核心,更是实现人员关注的核心。建模工具也主要根据类图来产生代码。
宏观例子:购买汉堡时直接取餐点餐,不需要做,商店封装了做汉堡的工作
UML类图
应用场景:jQuery $('div')、React.createElement、vue异步组件
工厂模式构造函数和创建者分离,符合开放封闭原则
比如登录框和购物车
UML类图:
应用场景:
jquery只有一个$
模拟登录框
vuex和redux的store
单例模式符合单一职责原则,只有一个实例化对象。
应用场景:封装接口,vue中的computed
适配器模式就接口和使用者进行了分离,符合开放封闭原则
应用场景:ES7装饰器, core-decorators(第三方库)
也可以去装饰类的方法
装饰器不能用于函数,因为函数存在提升
装饰器模式将现有对象和装饰器分离,两者独立存在,符合开放封闭原则
代理模式提供限制后一摸一样的接口
举例:
科学上网,访问github
明星经纪人
应用场景 : 网页事件代理,es6 proxy
代理模式原始对象和代理对象独立,符合开放封闭原则
代理模式:经限制之后提供一模一样的接口,代理功能
适配器模式:原有的方法不能用,转换功能
装饰器模式:原有的方法还要使用,扩展功能
比如去一家医院,挂号,取药,划价等集中到一个部门,病人只需要到部门即可
应用场景:参数不同时,使用一样
外观模式不符合单一职责原则,不符合开放封闭原则,不能滥用
举例:订牛奶
应用场景:
这里不在写具体代码,观察者模式是前端开发中最常见的设计模式,应用比较广泛,核心是发布订阅。
观察者模式主题和观察者分离,符合开放封闭原则
说明:迭代器模式生成了一种访问机制,只要符合这种访问机制的就可以被该迭代器遍历。
应用场景:
ES6的iterator
es6为什么要设计iterator
由于es6中有序集合的数据类型已经很多,比如:
Array, Map, Set, String, TypeArray, arguments, NodeList
这些数据类型都具有Symbol.iterator属性,基于这个共同属性,ES6提供了一个iterator迭代器。
手动实现这类数据的遍历:
function each(data){
let iterator = data[Symbol.iterator]
let item = {done:false}
while(!item.done){
item = iterator.next()
}
if(!item.done){
console.log(item.value)
}
}
es6提供了for...of...来遍历这种数据集合
和generator的关系
如果不了解迭代器这种设计模式,对于generator和iterator是不太好理解的,比较抽象。
对比generator的使用
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
foo()执行过后生成了可供for...of...遍历的数据集合,说明generator是用来生成具有Symbol.iterator属性的数据集合,generator内部的yield就是该数据集合的每一项,而这个数据项还可以是同类数据集合的嵌套。
任何数据结构只要有 Iterator 接口,就可以被yield*
遍历
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
迭代器模式中迭代器对象和目标对象分离,迭代器将使用者和目标对象隔离开,符合开放封闭原则。
从原型创建对象(clone自己,生成一个新对象)
和js中的prototype不同
应用场景:
Object.create(prototype)(比较像原型模式,java中的clone是原型模式)
例子:红绿灯的变化
应用场景:有限状态机, Promise
javascript-state-machine
除了以上常用的10种设计模式,还有一些不常见的设计模式(在前端中应用较少),这里不再具体介绍
设计模式在一些库和框架中应用是很广泛的,比如工厂模式,观察者模式,单例模式,适配器模式,外观模式,代理模式等等,掌握设计模式对于一些库源码的理解 以及他人代码的理解是很有帮助的。设计模式是从原则而来,是大量开发者通过大量实践得出的较为友好的程序实现方式,像es6中的很多新语法其实是设计模式的具体体现。
在实际开发过程中,刻意的使用和模仿某种设计模式,对于代码的可读性,性能都是很有帮助的,掌握设计模式之后,我们也可以更深入的理解es6中一些语法的含义,比如iterator, generator, decorator, proxy等。而如果在没有了解设计模式的情况下,在对es6的理解上会相对困难。
可以通过阅读一些经典的lib,从设计模式方向了解其设计思路,进而提升设计能力。