[读书笔记] JavaScript设计原则

578 阅读10分钟

本文是基于《JavaScript设计模式》所做的读书笔记, 摘取其中比较重点知识点, 形成此文, 方便日后查阅。

1. 单一职责原则

单一职责原则(SRP)的职责是: "引起变化的原因".

如果我们有两个动机去写一个方法, 那么这个方法就具有两个职责. 每个职责都是变化的一个轴线. 如果一个方法承担了过多的职责, 那么在需求的变迁过程中, 需要改写这个方法的可能性就越大.

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

SRP原则在很多设计模式中都有着广泛的应用, 例如: 代理模式, 迭代器模式, 单例模式和装饰器模式.

1) 何时应该分离职责

SRP原则是所有原则中最简单也是最难正确运用的原则之一.

要明确的是, 并不是所有的职责都应该是一一分离的.

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

2) 违反SRP原则

在人的常规思维中, 总是习惯性地把一组相关的行为放到一起, 如何正确地分离职责不是一件容易的事情. 一方面, 我们受设计原则的指导.
另一方面, 我们未必要在任何时候都一成不变地遵守原则. 在实际开发中, 因为种种原因, 违反SRP原则的情况并不少见. 比如jQuery的attr等方法, 就是明显违反SRP原则的做法. jQuery的attr是个非常庞大的方法, 既负责赋值, 又负责取值, 这对于jQuery的维护者来说, 会带来一些困难, 但对于jQuery的用户来说, 却简化了用户的使用.

在方便性与稳定性之间有一些取舍. 具体是选择方便性还是稳定性, 并没有标准的答案, 而要取决于具体的应用环境.

3) SRP原则的优缺点

  • 优点, 是降低了单个类或对象的复杂度, 按照职责把对象分解成更小的粒度, 这有助于代码的复用, 也有利于进行单元测试, 当一个职责需要变更的时候, 不会影响到其他的职责.
  • 缺点, 最明显的缺点是会增加编写代码的复杂度, 当我们按照职责把对象分解成更小的粒度后, 实际上也增大了这些对象之间相互联系的难度.

2. 最少知识原则

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

1) 减少对象之间的联系

单一职责原则指导我们把对象划分成较小的粒度, 这可能提高对象的可复用性, 但越来越多的对象之间可能会产生错综复杂的联系, 如果修改了其中一个对象, 很可能会影响到跟它相互引用的其他对象, 对象和对象耦合在一起, 有可能会降低它们的可复用性.

最少知识原则, 要求我们在设计程序时, 应当 尽量减少对象之间的交互.
如果两个对象之间不必彼此直接通信, 那么这两个对象就不要发生直接的相互联系. 常见的做法是引入一个第三者对象, 来承担这些对象之间的通信作用. 如果一些对象需要向另一些对象发起请求, 可以通过第三者对象来转发这些请求.

最少知识原则, 在设计模式中体现得最多的地方是中介者模式和外观模式.

2) 封装在最少知识原则中的体现

封装在很大程度上表达的是数据的隐藏, 一个模块或者对象可以将内部的数据或者实现细节隐藏起来, 只暴露必要的接口API供外界访问, 对象之间难免产生联系, 当一个对象必须引用另外一个对象的时候, 我们可以让对象只暴露必要的接口, 让对象之间的联系限制在最小的范围之内.

同时, 封装也用来限制变量的作用域, 在JavaScript中对变量作用域的规定是:

  • 变量在全局声明, 或者在代码的任何位置隐匿声明(不用var), 则该变量在全局可见.
  • 变量在函数内显式声明(使用var), 则在函数内可见
  • 把变量的可见性限制在一个尽可能小的范围内, 这个变量对其他不相关的模块的影响就越小, 变量被改写和发生冲突的机会也越小, 这也是广义的最少知识原则的一种体现.

3. 开放-封闭原则

开放-封闭原则的思想:
当需要改变一个程序的功能或者给这个程序增加新功能的时候, 可以使用增加代码的方式, 但是不允许改动程序的源代码.

1) 找出变化的地方

开放-封闭原则就一个看起来比较虚幻的原则, 并没有实际的模板教导我们怎样实现它, 但我们还是能找到一些让程序尽量遵守开放-封闭原则的规律, 最明显的就是找到程序中将要发生变化的地方, 然后把变化封装起来. 通过封装变化的方式, 可以把系统中稳定不变的部分和容易变化的部分隔离开来, 在系统的演变过程中, 我们只需要替换那些容易变化的部分. 如果这些部分是已经封装好的, 那么替换起来也相对容易, 而变化的部分之外的就是稳定的部分, 在系统的演变过程中, 稳定的部分是不需要改变的.

2) 用对象的多态性消除条件分支

过多的条件分支语句, 是造成程序违反开放-封闭原则的一个常见原因.
每当需要增加一个新的if语句时, 都要被迫改动原函数, 把if换成switch (case)是没有用的, 这是一种换汤不换药的做法. 实际上, 每当我们看到一大片的if或者switch语句时, 第一时间应该考虑, 能否复用对象的多态性来重构它们.

利用对象的多态性, 让程序遵守开放-封闭原则, 是一个常用的技巧.

3) 放置挂钩

放置挂钩也是分离变化的一种方式, 我们在程序有可能发生变化的地方放置一个挂钩. 挂钩的返回结果决定了程序的下一步走向.
这样一来, 原本的代码执行路径上就出现了一个分叉路口, 程序未来的执行方向被预埋下多种可能性.

由于子类的数量是无限制的, 总会有一些"个性化"的子类迫使我们不得已不去改变已经封装好的算法骨架. 于是我们可以在父类中的某个容易变化的地方放置挂钩, 挂钩的返回结果由具体的子类决定. 这样一来, 程序就拥有了变化的可能.

4) 使用回调函数

在JavaScript中, 函数可以作为参数传递给另外一个参数, 这是高阶函数的意义之一. 在这种情况下, 我们通常会把这个函数称为回调函数. 在JavaScript版本的设计模式中, 策略模式和命令模式等都可以用回调函数轻松实现.

回调函数是一种特殊的挂钩, 我们可以把一部分易起变化的逻辑封装在回调函数里, 然后把回调函数当作参数传入一个稳定和封闭的函数中, 当回调函数被执行的时候, 程序就可以因为回调函数的内部逻辑不同, 而产生不同的结果.

比如, 我们通过ajax异步请求用户信息之后, 要做一些事情, 请求用户信息的过程是不变的, 而获取到用户信息之后要做什么事情, 则是可能变化的.

4. 设计模式中的 开放-封闭原则

1) 发布-订阅模式

发布-订阅模式用来降低多个对象之间的依赖关系, 它可以取代对象之间硬编码的通知机制. 一个对象不用再显式地调用另外一个对象的某个接口, 当有新的订阅者出现时, 发布者的代码不需要进行任何修改, 同样当发布者需要改变时, 也不会影响到之前的订阅者

2) 模板方式模式

模板方法模式, 是一种典型的通过封装变化来提高系统扩展性的设计模式.
在一个运用了模板方法模式的程序中, 子类的方法生执行顺序都是不变的. 所以我们把这部分逻辑抽出来, 放到父类的模板方法里面. 而子类的方法具体怎么实现是可变的, 于是把这部分变化的逻辑封装到子类中, 通过增加新的子类, 便能给系统增加新的功能, 并不需要改动抽象父类以及其他的子类, 这也是符合开放-封闭原则的

3) 策略模式

策略模式和模板方法模式, 是一对竞争者. 在大多数情况下, 它们可以相互替换作用.

模板方法模式基于继承的思想, 而策略模式则偏重于组合和委托.

策略模式将各种算法都封装成单独的策略类, 这些策略类可以被交换使用. 策略和使用策略的客户代码可以分别独立进行修改而互不影响, 我们增加一个新的策略也非常方便, 完全不用修改之前的代码.

4) 代理模式

预加载图片的功能和给图片设置src的功能被隔离在两个函数里, 它们可以单独改变而互不影响.

5. 相关链接