设计模式
# 设计模式
首先如果你只是单纯的为了写一个页面的话,不介意那种强行追求设计模式。强扭的瓜不甜,设计模式使用的场景,是一些大型的项目,比如你要开发一个站点。
什么是设计模式?
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
我们来通过一个例子,了解理解一下上面这段非人类的话 。
在人类起源开始进步发展的过程中,有一个东西必不可少,那就是房子,一个私有的领域、空间,但是在最开始的时候,人类会建造房子吗? 肯定是不会的。都是一代人不断的摸索出来的。
假设我们第一个建造成功的是一件草房,我们要干什么? 是不是要先找到房子的支点,然后有个草制的屋顶。一间简陋的 草屋制成了,但是在使用中发现,这个屋子只能遮挡小雨,且风一吹就倒了,于是,房屋的支点不再随意摆放,开始嵌入地面,成为地基。
这些是什么,是不是建造房子的时候总结的经验。
那这些经验放到同领域其他地方还有用吗?
肯定是能用的。
比方说,我们现在不建草屋了,改成泥瓦房,同样的是不是需要用到地基,屋顶,围墙?
这就是前人总结的经验。
但是要注意,每个领域都有其起源,巅峰。
我们称这个为 黑匣子
这是什么呢?
比如说,建房子,一开始我们就是建草屋对吧,然后有一个需求,突然让我们整个摩天大厦。 这能做吗?
再比如说,手机,我们日常使用现在已经是不可或缺的东西,但是在古代,是没有手机的,如果我们让 古人做一部手机,他肯定是做不出来的。很困难 。首先他要先了解电路,然后是通信的原理,然后学习如何制作屏幕 等等,需要攻克的技术难关太多了。但是我们现在就不同了。你想做手机对吧,行 ,我在三星订购一个屏幕,然后淘宝买主板、后盖,等一切资源。我们都能得到。 而我们需要做的是什么? 是不是只需要拼装就可以了。
这就是设计模式的初衷,在日常开发,和项目应用中能够,更加高效的开发,更大可能的复用逻辑代码。
设计模式都是前人总结好的我们直接拿来用就行。
说白了,设计模式就相当于 套路,我们要想出一个套路来针对我们已知的需求问题。
为什么说设计模式像套路呢?
比如,一个三角形,我们知道两条边,如何确定第三条边呢?
我们学过数学肯定知道 勾股定理 这个公式对吧。但是这个公式是怎么来的? 是不是通过不断的实验,不断的总结,才得出的这种结论,然后我们现在,只需要知道我们根据这个数学公式,能够推导出第三条边,这就是我们的目的,对前人经验的使用。以前可能要 1 天才能试出来,但是现在,我们只需要调用这个公式,经过计算,很快就能得到。大大的加快了我们的效率。
设计模式的目的是什么?
为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编写真正工程化
设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式的原则是什么?
首先,有一个小问题要问下自己,你认为对人类约束力最大的是什么?
多花点时间好好思考。
ok。
我这里有两种答案,第一:法律约束了我们自身,第二:我们的思想约束了自身。
如果你的想法是法律的话,那和答案的偏差还是有点距离的。为什么这么说呢?
法律应该是一种不得已的情况,就是觉得我没有办法觉得你做的事情是对的错的,因为你已经超过了人类的底线,有点难懂,我们举个例子。
假设我们是新手,开始直接敲代码。但是我们忽略了一个问题,新手对于代码的规则一窍不通,如果我们违反了这个语言规则,如 我们未声明变量,直接取用。最终的结果是什么?
是不是报错了呀? 但是为什么报错,因为我们 js 引擎不得已,没有办法解析出你写的这是什么。对不对? 所以他动用法律来惩罚你,报错了,让你焦头烂额。
我们得出了一个结论。 法律是底线(守护和平的红线)。对应我们程序中来说就是 bug红线。 我们的程序不能报错对吧,因为报错说明代码已经执行不下去了,不可信的。
第二,我们的思想约束自身。不完全正确。为什么呢?
我们要明确,我们的思想是怎么来的? “古人云:人之初,性本善”,我们相信每个新生命都是善良的,那为什么 还会有坏人呢? 坏人又是怎么来的呢?
这取决于我们的思想,但是我们的思想同时又被其他的因素影响。 环境、人文属性。
儒家思想确立 仁义礼智信 ,贯穿古今,我们至今也被这种属性约束自身。仁义礼智信 是我们的一种对 伦理道德的理解, 等同于我们做人的原则。是这种伦理道德的观念时时刻刻的影响着我们。
但是原则最大的影响的地方,是让你如何做好一个人,对吧? 归根究底,是在权衡利弊。
假设,小王同学,想做个好人,那么他会怎么样?扶老奶奶过马路,捡到钱交给警察叔叔,坐公交车给老人让座。对吧 等等等等··· 这些都是能让你成为好人的途径。在这个过程中,由于你想成为好人,你在权衡之下,决定做出这些事情,虽然付出了一些代价,但是,你赢得了他人对你的看法,印象。 你想成为坏人同理是吧,你权衡之下觉得名声并不重要···。
再比如,菜市场,如果你的菜又好又便宜是不是都愿意去你家买,但是,如果你的菜都是那种快要变质的,还贵,还给人家缺斤短两。还会有人去吗?
其实,越到后面,就会发现,设计模式更像是一种另类的人文,只不过,是人和机器之间的交流。我们也在时时刻刻的思考权衡利弊的问题。
所以我们要干什么? 在守住底线,坚持原则的基础上,考虑我们的问题。
总结来说就是
没有套路做事情没效率,笨拙,重用性小,情况 复杂的时候,所需要攻克的问题太大,成本太高。有了套路,我们就能更容易的解决一些复杂的问题,让开发变得高效、简单。
普及一下知识。
设计模式最早出现是为了解决后端的需求问题,因为早期前端需求并没有这么复杂,他的初衷就是为了解决一些面向对象的问题,虽然 ES6 已经在往面向对象靠拢。
设计模式研究的就是 对象。
比如我们使用的话首先要创建一个对象,就可以用到创建型的工厂模式,有了对象之后我们是不是要描绘他的结构啊,这时候可以从结构型模式中找到适用的 - 代理模式,有了结构之后,还要描述他的行为,行为类型 - 观察者模式。 等等···
谈一下设计模式的六大原则
减少耦合,增强复用,降低代码的开发维护扩展成本。
什么是耦合呢 ?
耦合关系是指两个事物之间如果存在一种相互作用、相互影响的关系,那么这种关系叫做“耦合关系”。
举个例子:
假设有一个男生和一个女生,他们在一起了,这时候他们就叫情侣是吧,然后他们每天吃饭睡觉都在一起,男生打游戏的时候女生捣乱,女生化妆的时候男生捣乱,这种相互捣乱相互影响的关系。
对比于我们前端来说
假设有一个函数 A ,这个函数 A 需要做一些操作,但是发现功能不够用,又调用了函数 B。这种直接的关系。
再假设有一个 对象,这个对象呢 有一个行为影响了另外一个对象。 这样的直接的关系我们称之为耦合。
我们说,只要是两个对象也好、函数也好,他们之间有这种相互作用的联系。这就叫做耦合性。像是有一根绳子在牵着他们。
但是这种情况不叫耦合
假设我们有一个函数,这个函数呢有一个功能,会使全局中某个状态发生改变,然后巧合的是存在另外一个函数,这个函数的功能呢是监听这个全局状态,当这个状态发生改变时进行一系列的操作。
这是耦合吗?
虽然他确实是通过一些手段使得两个函数存在了关系,但是里面是不是有个第三者啊,就是这个全局的状态对吧。也就是说,他们两个之间的关系不够存粹。
这不是耦合!
耦合度和复杂度的危害
什么是复杂度呢?
大家都知道书法对吧? 什么宋体、隶书、楷书、草书。有的时候你就会发现有的书法,你能看明白,知道是什么字,但是有的字体,哇哦···· 称之为天数不为过吧,格式杂乱,字体飞舞。这个时候你看起来是不是特别费劲?
再比如, ES6之前,有一个有意思的东西就是 “嵌套地狱”,你就发现一个函数里面,TM 全是 if ··· else ···。
假设你有一个可爱的同事,平时啥事没有,就一个乐趣,写 if else,然后有一天他离职了,然后呢老板把他的需求转给了你,然后你一看代码,人傻了。想改个代码,发现 找括号就找了半天,还不包括看逻辑。那最后怎么办呢? 是不是只能重构了。
这种代码,就是那种复杂度很高,一个脚本就一个函数,剩下全是 if else。所有的逻辑都放在一个函数里面。 几千行啊··· 你瞅着那个括号眼睛疼不。
复杂度:
复杂度高,表示代码质量不高,可维护性差,复用性差,不易扩展。
耦合度:
耦合度无,是不可能的···,耦合度低,程度合理,过高则不容易维护。但是扩展性、复用性好。
# 所以我们开发时的流程是什么
本着优先降低复杂度的思想,尽量的去减少耦合度。但是有一点你要注意,你的复杂度和耦合度是息息相关的,你的复杂度降低,自然而然的你的耦合度就会上升。
- 通过单一职责原则,开闭原则,里氏替换原则,来降低复杂度(但是会提升耦合度)
- 通过迪米特法则来减少耦合度
- 通过依赖倒置原则消除可以没有的耦合度
读到这里,我们就会发现,权衡贯穿了整个设计模式的全部。而降低复杂度,减少耦合度就是我们的原则。
应用:
-- 使用设计模式六大原则 - 单一职责原则
单一职责原则总体思想就是,各司其职,各做各的。不再同一函数中做其他的事情。避免功能混淆,导致无法复用、扩展。
这种就是典型的通过增加耦合度来降低复杂度的例子,下面我们通过伪代码来模拟一下。
假设我们有一个 ul
<ul id="chess-list"></ul>
现在的需求是让你通过网络请求,获取列表中的内容,并将其渲染到页面上。 粗略一看很简单是吧
function getView(url,data,container){
// 网络资源请求数据
$.ajax({
url:url,
data:data,
success:function (json){
// 在这里我们能够拿到 一个 json 数据对吧 假设格式是[{name:'zhang san'},{name:'li si'}]
// 渲染到页面
let renderStr = json.reduce((prev,item) => prev + '<li>' + item.name + '</li>','')
container.innerHTML = renderStr;
}
})
}
看着实现起来挺简单的对吧,但是某一天,领导说,诶 这个需求变更一下,说首次请求太浪费请求资源了,添加一个缓存机制。 我们缓存怎么弄? 是不是整一个副本给他存放到本地,然后当我进来的时候如果有缓存值就取本地的,没有就进行网络请求。 我们把之前的代码 copy 过来。然后继续添加,为了方便观看 会给注释添加序号。
function getView(url,data,container){
// 2 - 添加缓存
let catchStr = localStorage.getItem('list')
if(catchStr){
// 2 - 缓存值存在,取缓存数据进行渲染
let json = JSON.parse(catchStr);
// 2-1 - 在这里我们能够拿到 一个 json 数据对吧 假设格式是[{name:'zhang san'},{name:'li si'}]
// 2-1 - 渲染到页面
let renderStr = json.reduce((prev,item) => prev + '<li>' + item.name + '</li>','')
container.innerHTML = renderStr;
} else {
// 2 - 没有缓存值,则进行网络请求
// 1 - 网络资源请求数据
$.ajax({
url:url,
data:data,
success:function (json){
// 2 - 也就是说我们在取得网络资源时,先将其存储到 localStorage 中。
localStorage.setItem('list',JSON.stringify(json));
// 1 - 在这里我们能够拿到 一个 json 数据对吧 假设格式是[{name:'zhang san'},{name:'li si'}]
// 1 - 渲染到页面
let renderStr = json.reduce((prev,item) => prev + '<li>' + item.name + '</li>','')
container.innerHTML = renderStr;
}
})
}
}
嗯···我们发现 getView 函数,变得复杂了一些,但是还好对吧。一些逻辑还是能够分的很清楚的,但实际上,我们可以看出,有些代码,他就是重复的比如渲染那块,就直接 control + C , control + V, 这··是不是有点了对吧。
我们发现,这个函数的规模会随着我们的需求更迭不断的变大,复杂度越来越大,当有一天,就加到我们自己都看不懂的时候,就没有办法了,所以,这时候单一职责原则派上了用场。 各司其职。 假设我们以函数的形式,
// 请求网络资源
function getSource(){}
// 渲染
function render(){}
// 缓存
function cache(){}
// 如果再有新的需求,比如离线缓存
function onlineCache(){}
··· 等等。
函数之间相互调用,存然增加了耦合度,但是相应的,我们的复杂度也减少了。 但是这样看起来还是不够直观对吧?,我们可以试一下使用对象的方式。
function WebSource(callbackArray,url,params){
this.callbackArray = callbackArray;
this.url = url;
this.params = params;
this.do = function (){
$.ajax({
url:this.url,
data:this.params,
success: function (json){
// pass 占位
}
})
}
}
写到 success 后面不知道取了数据之后干什么了对吧,我们先进行其他的
function Cache(key){
// do 函数要做的是将传入的数据缓存起来对吧
this.do = function (data){
localStorage.setItem(key,JSON.stringify(data));
}
// 然后我们还要能够取到这个缓存值
this.getCacheValue = function (){
localStorage.getItem(key);
}
}
缓存的构造函数就 OK 啦。 然后是渲染
function Render(container){
this.container = container;
this.do = function (data){
// 这里我就把之前的 copy 过来了
let renderStr = data.reduce((prev,item) => prev + '<li>' + item.name + '</li>','')
this.container.innerHTML = renderStr;
}
}
好的 前置工作准备完了,我们看下数据获取那里 如何使用。(再 copy 一份过来)
function WebSource(callbackArray,url,params){
this.callbackArray = callbackArray;
this.url = url;
this.params = params;
this.do = function (){
$.ajax({
url:this.url,
data:this.params,
success: function (json){
// pass 占位
}
})
}
}
首先我们要明确,网络资源请求完毕要做什么? 是不是要进行数据缓存、视图渲染啊? 那么我们的 callbackArray 传入的应该是什么呢? 首先要保证能够进行缓存,然后还要保证能够进行视图的渲染。 我们发现这些功能都存在于他们各自的 do 方法上。 也就是说,我要传入一个能够调用 do 方法的实例对吧。
const cacheObj = new Cache('list')
const renderObj = new Render(xxx) // xxx 代表你传入的这个容器
// 然后呢? 实例化我们的 WebSource
// const sourceObj = new WebSource([],'xxx',{xxx:'xxx'}) 那么这个数组里面放什么?是他们的实例
const sourceObj = new WebSource([cacheObj,renderObj],'xxx',{xxx:'xxx'})
相互的一个调用关系就形成了对吧? 然后我们回到 WebSource 的构造函数中,完成剩余的部分
function WebSource(callbackArray,url,params){
this.callbackArray = callbackArray;
this.url = url;
this.params = params;
this.do = function (){
$.ajax({
url:this.url,
data:this.params,
success: function (json){
// 拿到数据后进行缓存与渲染
callbackArray.forEach(obj => obj.do(json))
}
})
}
}
OK ~ 就是这么的简洁,你会发现,当你有其他的需求增加的时候,你只需要再构造函数中扩展,或者修改某个构造函数里面的方法,使其满足你的需求。这样你就能直接定位你的问题位置,不再需要去找逻辑了对吧?
使用设计模式的六大原则二 -- 开闭原则OCP
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
面向扩展开放,面向修改关闭 (jquery extend),同时增强代码复用性。
开闭原则呢其实很像 “腾讯“,为什么这么说呢?不知道大家有没有听过,腾讯对于账号的所属权问题,大致的意思就是,你只保留有账号的使用权,但是这个账号属于腾讯,只是借给你用。如果你想体验那种奇幻的功能是吧 OK啊,充钱呗,冲就完事了。
我们的开闭原则的核心思想就是,希望你通过扩展的方式来丰富其他功能,原有的功能呢不支持你修改。
还记的我们的开发流程吗,使用开闭原则可以降低复杂度。
这个和开闭原则的核心思想是相关联的,以 jQuery 为例,假设,jQuery 这个作者发布的时候,所有的方法等等都能够修改,你可以想象一下,你的项目会变成什么样子。 这个地方改一下,那边又改一下,最后你自己都不知道这个函数到底是什么功能了。大大的增加了开发的复杂程度。所以,你可以凭借你的经验,和开发过程,来分析有没有哪些必要的,又是插件里面没有的,然后通过他提供的扩展方法 如 jQuery extend 扩展出来。不允许你修改其内容。
使用设计模式的六大原则三 -- 里氏替换原则 L Subsituation Principle
任何基类可以出现的地方,子类一定可以出现。通俗来说就是:子类可以扩展父类的 功能,但不能改变父类原有的功能。 这句话什么意思呢? 就是让你使用继承的方式来扩展父类,但是不让你重写父类的方法。 比方说我们有这样两个构造函数,Son 继承 Father,然后重写了 Father 的 eat 方法。
function Father() {
this.eat = function () {
console.log('i like eat egg!');
}
}
let f = new Father()
Son.prototype = f;
function Son() {
this.eat = function () {
console.log('i like eat apple');
}
}
let s = new Son()
f.eat()
s.eat()
console.log(s);
打印出来结果应该是 // i like eat egg! 和 // i like eat apple 对吧?这时候把父类的方法重写了,可能会造成一些不可预测的结果。 里氏替换原则的核心思想就是,希望你通过继承去扩展的方式降低复杂度,最最最重要的一点就是,他希望你继承的是一个动作,而不是一个行为。 比方说,刚刚的 Father 和 Son 对吧,如果父类 Father 提供的 eat 只是一个吃的动作,那么他们自身就可以通过扩展其他的方法来实现喜欢吃什么。
使用设计模式的六大原则四 -- 迪米特法则(最小知道原则) LD
一个接口和一个方法,传入的参数越少越好。降低耦合度的同时也会让复杂度降低。 形成的关联越少越好,依赖越少越好。 这个引用一个我听过的例子,就是说谍战大家都知道是吧,假如你抓到了一个间谍,是不是要逼供,让他说出其他同伙的位置啊,但是假设,一共就俩间谍,然后他们还互相不知道,是不是你再怎么问,都没有用,本着那种要死死一个的精神···。
使用设计模式的六大原则五 -- 依赖倒置原则 DIP
最常用的原则,依赖接口,不依赖方法,底层的东西不用了解,我们只需要知道表现就可以。降低耦合度
前端应用中可以理解成多者之间依赖状态,而不依赖彼此。
JS 中没有接口概念。 这个依赖倒置原则是怎么回事呢? 不知道各位还记不记得我们之间说过,如果你依赖第三者,依赖其他的状态,而不是和这个函数或者对象形成直接依赖,我们说他是耦合嘛?不是耦合对吧,这是不是就消除了耦合度? 这种原则应用再发布订阅模式中, 大致是这样的 假设我们有两个函数 A、B ,这个 AB函数 本来是相互依赖的,然后呢,我们通过依赖倒置原则,创建了一个全局的状态 C,然后 A 不再依赖 B, 我们切断与 B 的联系,改为修改 全局状态C 的状态。 然后我们的 B 呢监听这个全局的状态C,只要你的值发生改变,那就执行原有操作。这样之后,虽然 A、B 之间还存在联系,但这个联系是通过 C 来传递的,他们相互都只知道 C 这个状态的变化。
使用设计模式的六大原则六 -- 接口分离原则 LSP
把大接口拆分为小接口,不能一个接口全部实现增删改查的功能。
这个呢就是属于服务端范畴了,大致的意思就是,如果你把功能通过一个接口全部实现,势必会造成这个接口的复杂度上升,肯定要接收更多的参数和判断,而且,假设你修改的时候这个接口挂了,完蛋,凉了,因为是同一个接口,导致查询也查询不了,删除也删除不了。
所以要将接口分离,保证你在其他功能挂掉的时候,至少你这个查询还是正常的,能够查回来数据的对吧。
设计模式六大原则的使用 根据我们开发流程 首先是我们的代码块 S, S 可以经过单一职责原则,开闭原则,里氏替换原则,通过增加耦合度的方式,来降低复杂度。
S --> 单一职责原则、开闭原则、里氏替换原则 --> A、B、C、D 通过这三个原则我们得到了 A、B、C、D 四个依赖的代码块。 他们的依赖关系可能是: A 依赖 B ,A 依赖 C ,A 依赖 D B 依赖 C ,B 依赖 D, B 依赖 A C 依赖 D ,D 依赖 B。 都有可能,这里是随便举例。 然后可以使用迪米特法则,降低耦合度。 这个过程,有可能把 B 对 C、B 对 D 、 C 对 D 、 B 对 A 的依赖消除了, 此时,经过 A 的改变,对其依赖改变,然后反馈到页面上。
A --> | --> B -----------> --> C -----------> 页面 --> D ----------->
最后,我们使用依赖倒置原则,消除可以不存在的耦合度。
A --> | | --> 全局状态 E | |(通过监听 E 的状态) | |--> B -----------> |--> C -----------> 页面 |--> D ----------->
再上面的过程,通过监听“状态 E”,触发 B、C、D 功能。
这便是设计模式六大原则的应用。
创建型 - 研究高效的创建对象
- 单例模式
- 抽象工厂模式
- 建造者模式
- 工厂模式
- 原型模式
结构型模式 - 设计对象的结构和关系
- 适配器模式
- 桥接模式
- 装饰模式
- 组合模式
- 外观模式
- 亨元模式
- 代理模式
行为型模式 - 设计对象的行为
- 模板方法模式
- 命令模式
- 迭代器模式
- 观察者模式
- 中介者模式
- 备忘录模式
- 解释器模式
- 状态模式
- 策略模式
- 职责链模式
- 访问者模式