从设计原则,程序实现目标谈谈前端常用的10种设计模式和应用场景
时间:2023-10-07 16:18:01 | 来源:网站运营
时间:2023-10-07 16:18:01 来源:网站运营
从设计原则,程序实现目标谈谈前端常用的10种设计模式和应用场景:
总括:
近几年前端技术迭代迅速,除了ES6,Vue,React,Angular,各种企业级框架也层出不穷,比如egg,umi,nuxt,next,koa等等,还有redux, vuex, Mobx, Flux,dva等状态管理库,这些给前端开发工作带来了极大的简化,同时也给开发者很大的学习挑战。但是想要在前端架构层面有一定的高度,这些都是必须要掌握的。除了这些,还要掌握工程化,自动化,服务端等。那么这些类库或者框架剥离业务之后,其背后的设计思想以及遵循的底层程序原理是怎样的呢?
很多开发者对于知识的掌握,框架的应用都很熟悉,但是总有一些角落感觉没有深入进去,这可能是因为对于程序本身没有一个全局的认识。举个例子(不识庐山真面目,只缘身在此山中),好比在一座山之中,很多人可以很清楚的知道山里的路,但可能不会知道路为什么是这个走向,要想了解山势,就要走出这座山,从更高的地方观看一目了然。
所以做程序不仅要走得进来,更要能够走得出去,用最简单的方式思考问题,抓住程序的底层思维,设计出程序路径,至于用什么方法实现反而不那么重要了。
一年前和一位后端高级架构专家讨论前端如何能够快速上手开发后端需求,当时我觉得是先要学好java或者其他语言,因为这是开发的基础,如果基础语法和知识都不清楚的话,怎么开发呢。不过他说真正的程序开发,不管是前端和后端,和语言的关联程度并不是排在第一位得,真正排在第一位的是程序实现目标和实现模式。事实上也是如此,语言和框架有很多,但是这些都是开发的工具,我们要掌握的是开发的思想。体会并深入掌握这一点后,能少走很多弯路。
什么是程序
计算机程序是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。
系统的三大因素:结构化的数据,操作数据的逻辑(增删改查),展示数据(ui层)
程序实现目标(OOP)
程序执行时通过顺序,判断,循环实现增删改查,这就要求数据有一定的结构,才方便操作。
所以程序实现的目标是将宏观事物抽象为结构化的数据,并覆以操作数据的方法。
数据结构
常用的数据结构有Set, Map, Tree, Array,链表,图等等,而操作这些就要有对应的方法,这些方法基于一定的实现准则,就是我们所说的设计原则。
设计原则
何为设计
- 按照哪一种思路或者标准实现功能
- 功能相同或,可以用不同设计方案来实现
- 伴随着需求增加,设计的作用才能体现出来
程序设计原则举例(参考UNIX/LINUX设计哲学)
- 小即是美
- 让每个程序只做一件事情
- 快速建立原型
- 舍弃高效率而取可移植性
- 采用纯文本存储数据
- 充分利用软件的杠杆效应(软件复用)
- 使用shell脚本提高杠杆效应和可移植性
- 避免强制性的用户界面
- 让每个程序都成为过滤器
实际开发中设计的五大原则(SOLID)
- 单一职责原则(SRP) 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中
- 开放封闭原则(OCP) 实体应该对扩展是开放的,对修改是封闭的
- 里氏替换原则(LSP) 一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误
- 接口隔离原则(ISP) 接口隔离原则表明客户端不应该被强迫实现一些他们不会使用的接口
- 依赖倒置原则(DIP) 抽象不应该依赖于细节,细节应当依赖于抽象
说明:单一职责原则(SRP),开放封闭原则(OCP) 在javascript开发中应用非常广泛, 里氏替换原则(LSP),接口隔离原则(ISP),依赖倒置原则(DIP) 在javascript种应用较少,在TypeScript中可以体现。1,单一职责原则
- 一个程序只针对一件事情
- 如果功能过于复杂,每个部分保持独立
2,开放封闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
- 这是软件设计的最终目标
3,李氏置换原则
子类能覆盖父类
父类出现的地方子类就能出线
4,接口独立原则
保持接口的单一独立,避免出现旁接口(不是100%,像外观模式,后面详细介绍)
类似于单一职责原则,但是这里更注重于接口
5,依赖倒置原则
面向接口编程,依赖于抽象而不依赖于具体
使用方法只关注接口而不关注具体类的实现
对比分析
开闭原则(OCP)是面向对象设计原则的基础也是整个设计的一个终极目标,而依赖倒置原则(DIP )则是实现OCP原则的一个基础,换句话说开闭原则(OCP)是你盖一栋大楼的设计蓝图,那么依赖倒置原则就是盖这栋大楼的一个钢构框架。
从设计到模式
设计和模式是分开的,有了设计才产出了模式。设计是指导思想,模式是根据指导思想抽象出来的一些模板。
设计模式分类
从功能上划分为三类:
创建型,组合型,行为型具体到每个类型又细分如下, 如下图:
UML类图
在讲设计模式之前,介绍下UML类图:详细参见UML类图
在UML的静态机制中类图是一个重点,它不但是设计人员关心的核心,更是实现人员关注的核心。建模工具也主要根据类图来产生代码。
- 类是具有相似结构、行为和关系的一组对象的描述符。
- 类是面向对象系统中最重要的构造块。
- 类图显示了一组类、接口、协作以及他们之间的关系。
- 类的属性、操作中的可见性使用+、#、-分别表示public、protected、private。
常见的设计模式和应用场景
1,工厂模式
- 将new操作单独封装
- 遇到new时,就要考虑是否该使用工厂模式
宏观例子:购买汉堡时直接取餐点餐,不需要做,商店封装了做汉堡的工作
UML类图应用场景:jQuery $('div')、React.createElement、vue异步组件工厂模式构造函数和创建者分离,符合开放封闭原则2,单例模式
比如登录框和购物车
UML类图:
应用场景:jquery只有一个$
模拟登录框
vuex和redux的store
单例模式符合单一职责原则,只有一个实例化对象。3,适配器模式
应用场景:封装接口,vue中的computed
适配器模式就接口和使用者进行了分离,符合开放封闭原则4,装饰器模式
应用场景:ES7装饰器, core-decorators(第三方库)
也可以去装饰类的方法
装饰器不能用于函数,因为函数存在提升装饰器模式将现有对象和装饰器分离,两者独立存在,符合开放封闭原则5,代理模式
代理模式提供限制后一摸一样的接口
举例:科学上网,访问github
明星经纪人
应用场景 : 网页事件代理,es6 proxy
代理模式原始对象和代理对象独立,符合开放封闭原则代理模式,适配器模式,装饰器模式对比分析:
代理模式:经限制之后提供一模一样的接口,代理功能适配器模式:原有的方法不能用,转换功能装饰器模式:原有的方法还要使用,扩展功能6,外观模式
- 为子系统中的一组接口提供了一个高层接口
- 使用者使用高层接口
比如去一家医院,挂号,取药,划价等集中到一个部门,病人只需要到部门即可
应用场景:参数不同时,使用一样
外观模式不符合单一职责原则,不符合开放封闭原则,不能滥用7,观察者模式
举例:订牛奶
应用场景:- 网页事件绑定
- Promise
- Jquery callbacks
- node.js自定义事件
- redux
- vue、React生命周期触发
- vue watch
- node多进程通讯
这里不在写具体代码,观察者模式是前端开发中最常见的设计模式,应用比较广泛,核心是发布订阅。
观察者模式主题和观察者分离,符合开放封闭原则8,迭代器模式
- 顺序访问一个集合
- 使用者无需知道集合的内部结构(封装)
说明:迭代器模式生成了一种访问机制,只要符合这种访问机制的就可以被该迭代器遍历。
应用场景: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}
迭代器模式中迭代器对象和目标对象分离,迭代器将使用者和目标对象隔离开,符合开放封闭原则。9,原型模式
从原型创建对象(clone自己,生成一个新对象)
和js中的prototype不同
应用场景:Object.create(prototype)(比较像原型模式,java中的clone是原型模式)
10,状态模式
- 一个对象有状态变化
- 每次状态变化都会触发一个逻辑
- 不能总用if...else...控制
例子:红绿灯的变化
应用场景:有限状态机, Promise
javascript-state-machine
其他设计模式
除了以上常用的10种设计模式,还有一些不常见的设计模式(在前端中应用较少),这里不再具体介绍
总结:
设计模式在一些库和框架中应用是很广泛的,比如工厂模式,观察者模式,单例模式,适配器模式,外观模式,代理模式等等,掌握设计模式对于一些库源码的理解 以及他人代码的理解是很有帮助的。设计模式是从原则而来,是大量开发者通过大量实践得出的较为友好的程序实现方式,像es6中的很多新语法其实是设计模式的具体体现。
在实际开发过程中,刻意的使用和模仿某种设计模式,对于代码的可读性,性能都是很有帮助的,掌握设计模式之后,我们也可以更深入的理解es6中一些语法的含义,比如iterator, generator, decorator, proxy等。而如果在没有了解设计模式的情况下,在对es6的理解上会相对困难。
可以通过阅读一些经典的lib,从设计模式方向了解其设计思路,进而提升设计能力。