JavaScript设计模式笔记:3个设计原则、14个设计模式、9个技巧(干货)

4,982 阅读23分钟

介绍

软件开发的基础理论对于非科班出身的我来说一直是个弱项,前一段时间立了个flag,把 《JavaScript设计模式与开发实践》 这本书里的设计模式和设计原则整理成简单易懂的便签😂

一是自己加深理解,倒逼输出,二来可以请大家帮忙纠错,防止自己理解偏差;

喜欢刷沸点小伙伴们可能有看见过最近发的设计模式便签🔖, 由于每天早晚看娃,很少有整块的时间,只能一天抽几分钟整理一两个便签,还好这个flag算是勉勉强强的完成了,收获确实很多,下面就是整理的笔记,如有错误,恳请拍砖,em....,用力拍👋👋👋

可能很多优秀的模式模式是潜移默化在你的代码和实现思路里,只不过叫不上名字,如果粗略的理解一下设计模式,至少和别人讨论实现思路时可以喷出几个名词(开玩笑)😴,毕竟作为一个编程人员这些知识还是很很很重要的

3个设计原则

单一职责原则(SRP)

就一个类而言,应仅有一个引起它变化的原因。

单一职责原则(SRP)的职责被定义为“引起变化的原因”。 如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。

SRP原则体现为:一个对象/方法,只做一件事情。

SRP原则的应用难点是如何分离职责

SRP原则是所有原则中最简单也是最难正确运用的原则之一。 要明确的是,并不是所有的职责都应该一一分离。

  • 一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在ajax请求的时候,创建xhr对象和发送xhr请求几乎总是在一起的,那么创建xhr对象的职责和发送xhr请求的职责就没有必要分开。
  • 另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。

SRP原则的优缺点 SRP原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。

最明显的缺点是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

最少知识原则(LKP)

最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。

减少对象之间的联系

最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。

迪米特法则(Law of Demeter,LoD)

最少知识原则也叫迪米特法则(Law of Demeter,LoD),“迪米特”这个名字源自1987年美国东北大学一个名为“Demeter”的研究项目。 在实际开发中,是否选择让代码符合最少知识原则,要根据具体的环境来定。

开放-封闭原则

开放-封闭原则最早由Eiffel语言的设计者Bertrand Meyer在其著作Object-Oriented Software Construction 中提出。它的定义如下:

软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。

开放-封闭原则的思想:

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

实践方法

  • 用对象的多态性消除条件分支
  • 找出变化的地方 找出程序中将要发生变化的地方,把变化封装起来,稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,我们只需要替换那些容易变化的部分,变化的部分使用如下方法。
    1. 放置挂钩
    2. 使用回调函数

开放-封闭原则与设计模式

不管是具体的各种设计模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等,都是为了让程序遵守开放-封闭原则而出现的

堆砌设计模式与过度设计

让程序保持完全封闭是不容易做到的。就算技术上做得到,也需要花费太多的时间和精力。 下面这段话引自Bob大叔的《敏捷软件开发原则、模式与实践》:

有句古老的谚语说:“愚弄我一次,应该羞愧的是你。再次愚弄我,应该羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背着不必要的复杂性,我们会允许自己被愚弄一次。

这有点像星矢说的:“圣斗士不会被同样的招数击倒第二次。”

14个设计模式

单例模式

即保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

一个基于策略模式的程序至少由两部分组成。

  • 第一个部分是一组策略类,策略类封装了具体 的算法,并负责具体的计算过程。
  • 第二个部分是环境类 Context,Context 接受客户的请求,随后 把请求委托给某一个策略类。

代理模式

代理模式是为本体对象提供一个替身,以便控制对本体的访问。

代理模式的关键是,当我们不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,我们实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

可以帮助对象过滤掉一些不满足特定条件的请求,把一些开销很大的请求,延迟到真正需要它的时候才去执行等等。

迭代器模式

指提供一种方法顺序访问一个聚合对象中的各个元素而又不需要暴露该对象的内部表示。

迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素,如 jQuery 中的$.each 函数。

内部与外部迭代

内部迭代即调用一次循环所有元素,外部迭代需要手动触发下一个元素的迭代,如图:

可迭代特性

无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代。

迭代顺序

迭代器模式提供了循环访问一个聚合对象中每个元素的方法,但它没有规定我们以顺序、倒序还是中序来循环遍历聚合对象。

终止迭代器

迭代器可以像普通 for 循环中的 break 一样,提供一种跳出循环的方法。

小结

迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器。

发布—订阅模式

发布—订阅模式和观察者模式类似,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。

作用

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

让两个对象松耦合地联系在一起,可以在不太清楚彼此的细节的情况下相互通信。

当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要约定的事件名没有变化,就可以自由地改变它们。

订阅实现的关键点

  • 发布者
  • 订阅列表
  • 订阅方法
  • 发布方法
  • 取消订阅方法

优缺点

发布—订阅模式的优点非常明显,时间、对象之间的解耦,从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与。

创建订阅者本身要消耗一定的时间和内存,订阅一个消息后,也许此消息最后都未发生,但订阅者始终存在内存中。另外,发布—订阅模式会弱化对象之间的联系,过度使用后,对象和对象之间的必要联系也将被深埋在背后,导致程序难以跟踪维护和理解。

命令模式

命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。

命令模式的由来其实是回调(callback)函数的一个面向对象的替代品,跟许多其他语言不同,JavaScript 可以用高阶函数方便地实现命令模式。

封装命令类

封装在普通函数

JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。运算块不一定要封装在命令类中,也可以封装在普通函数中。

撤销命令: 某个命令需要运行较长时间,可以增加撤销操作。

命令队列: 我们把命令存入一个队列,可以很简单的实现如“回放”、“后退”的功能。

宏命令 : 一组命令的集合,一次执行一组命令。

命令模式在 JavaScript 语言中是一种隐形的模式。

组合模式

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更 小的“孙对象”构成的。

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

优点

提供了一种遍历树形结构的方案,组合模式可以非常方便地描述对象的层次结构。

统一地使用组合结构中的所有对象,不需要关心它究竟是组合对象还是单个对象。

组合模式不是父子关系

有时候把上下级对象称为父子节点,但大家要知道,它们并非真正意义上的父子关系。

必要条件

只有用一致的方式对待列表中的每个叶对象的时候,才适合使用组合模式。

小结

我们可以把相同的操作应用在组合对象和单个对象上。大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。

模板方法模式

严重依赖抽象类,使用继承和重写父类的某些方法来实现功能的设计模式。

两部分组成

  • 抽象父类:封装了子类的算法框架、公共方法、以及子类中所有方法的执行顺序。
  • 子类:继承抽象类,按照整个算法结构选择重写父类的某些方法。

JavaScript 没有从语法层面提供对抽象类怎么办?

  • 接口检查,确保子类重写了父类的方法。
  • 未重写方法直接抛出异常。

小结

模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。子类的方法种类和执行顺序在抽象类中定义且不可变,新功能通过增加子类且不需要改动抽象父类及其他子类即可实现,这也符合开放-封闭原则。

享元模式

享元模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量细粒度的对象。

享元模式要求将对象的属性划分为内部状态与外部状态,目标是尽量减少共享对象的数量。

内部状态存储于对象内部

  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

享元模式适用的场景

  • 对象的大多数状态都可以变为外部状态。
  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

对象池

对象池维护一个装载空闲对象的池子,需要对象的时候,不是直接 new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取。

小结

享元模式是为解决性能问题而生的模式,大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

传统实现的缺点

传统实现就像一根环环相扣打了死结的链条,如果要增加、拆除或者移动一个节点,就必须得先砸烂这根链条。

职责链模式优点

职责链模式的最大优点就是解耦了请求发送者和 N 个接收者之间的复杂关系, 请求发送者只需要知道链中的第一个节点,弱化了发送者和一组接收者之间的强联系。

职责链模式缺点

不能保证某个请求一定会被链中的节点处理,大部分节点没有起到实质性的作用,仅是让请求传递下去。 从性能方面考虑,我们要避免过长的职责链带来的性能损耗。

小结

在 JavaScript 开发中,职责链模式是最容易被忽视的模式之一。只要运用得当,可以降低发起请求的对象和处理请求对象之间的耦合性,可以自由变化职责链中的节点数量和顺序

中介者模式

用来降低多个对象和类之间的通信复杂性,它提供了一个中介类,该类处理不同类之间的通信,并支持松耦合,使代码易于维护。

面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性。

中介者模式的作用

中介者模式的作用就是解除对象与对象之间的紧耦合关系

增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。

中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

中介者模式的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

装饰者模式

在不改变对象自身的基础上,程序运行期间动态给对象添加职责的方式称为装饰者模式。

在程序开发中并不希望某个类天生就非常庞大,一次性包含许多职责; 装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响这个类派生的其他对象。

装饰者也是包装器

在《设计模式》成书之前,GoF原想把装饰者(decorator)模式称为包装器(wrapper)模式。

从功能上而言,decorator能很好地描述这个模式,但从结构上看,wrapper的说法更加贴切。装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会

装饰者模式和代理模式的区别

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的

两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。

代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供访问本体之前做一些额外的事情,或拒绝对它的访问, 代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,一开始就可以被确定。

装饰者模式的作用就是为对象动态加入行为,而装饰者模式用于一开始不能确定对象的全部功能时使用。代理模式通常只有一层代理,而装饰者模式经常会形成一条长长的装饰链。

状态模式

允许一个对象在内部状态改变时改变它的行为。

状态模式的关键是区分事物的内部状态,事物状态的改变会带来事物行为的改变,每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,当请求对象的某个行为时,把这个请求委托给当前的状态对象的行为即可。

优点

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。 Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
  • 状态模式的缺点是会在系统中定义许多状态类

策略模式与状态模式

  • 都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
  • 使用策略模式时,客户必须熟知这些策略类的作用,以便可以随时主动切换算法;
  • 状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。

javascript-state-machine 有限状态机

github.com/jakesgordon…

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。

当我们试图调用模块或者对象的某个接口时,发现这个接口的格式并不符合目前的需求时, 创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。 使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。

小结

适配器模式是一对相对简单的模式。有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式,这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。

代码重构技巧

原文是这么说的:

从某种角度来看,设计模式的目的就是为许多重构行为提供目标。

提炼函数

如果在函数中有一段代码可以被独立出来,最好把这些代码放进另外一个独立的函数中。

  • 避免出现超大函数。
  • 独立出来的函数有助于代码复用。
  • 独立出来的函数更容易被覆写。
  • 独立出来的函数如拥有良好的命名,本身就起到了注释作用。

合并重复的条件片段

如果一个函数体内有一些条件分支语句,而这些条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作。

把条件分支语句提炼成函数

在程序设计中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,而且容易导致一个庞大的函数。

合理使用循环

在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以完成同样的功能,还可以使代码量更少。

提前让函数退出代替嵌套条件分支

用《重构》里的话说:

嵌套的条件分支往往是由一些深信“每个函数只能有一个出口的”程序员写出的。但实际上,如果对函数的剩余部分不感兴趣,那就应该立即退出。引导阅读者去看一些没有用的else片段,只会妨碍他们对程序的理解。

传递对象参数代替过长的参数列表

一个函数接收的参数数量越多,函数就越难理解和使用。在使用的时候,还要防止少传了某个参数或者把两个参数搞反了位置。 使用对象就可以不用再关心参数的数量和顺序,只要保证参数对应的key值不变就可以了。

尽量减少参数数量

如果一个函数不需要传入任何参数就可以使用,这种函数是深受人们喜爱的。在实际开发中,向函数传递参数不可避免,但我们应该尽量减少函数接收的参数数量。

少用三目运算符

使用三目运算符和使用if、else代码循环一百万次,时间开销仍处在同一个级别里。 如果条件分支逻辑简单清晰可用三目运算符,如逻辑复杂建议还是使用if、else。

合理使用链式调用 链式调用带来的坏处是调试非常不方便,如果链条很容易发生变化,建议使用普通调用的形式。

用return退出多重循环

假设在函数体内有一个两重循环语句,使用控制标记变量或者设置循环标记这两种做法无疑都让人头晕目眩。 用return直接退出方法会带来一个问题,将来不能执行循环之后的代码,可以return函数。

结束

你要是能看到这里,真的是太厉害了,这是一篇罗列知识点的总结,本身就很枯燥,我在整理汇总的时候已经快失去耐心了🤧,能看完的一定是很厉害的大牛,也想请大家推荐几本关于软件设计的理论方面的书,感谢亲哒哒哒~~~,哦,对了🤗,撩骚一下,有收获的话就给个小心心吧😍😍😘。