1

写在前面

  • 书籍介绍:本书在尊重《设计模式》原意的同时,针对JavaScript语言特性全面介绍了更适合JavaScript程序员的了16个常用的设计模式,讲解了JavaScript面向对象和函数式编程方面的基础知识,介绍了面向对象的设计原则及其在设计模式中的体现,还分享了面向对象编程技巧和日常开发中的代码重构。本书将教会你如何把经典的设计模式应用到JavaScript语言中,编写出优美高效、结构化和可维护的代码。
  • 我的简评:这本书主要围绕JavaScript中的一些设计模式和设计原则,每种模式的讲解都带有生活实例,恰当贴切,比较容易懂。不过,在此特意建议一下,有一定的编程经验和项目经历后再读设计模式方面的书。
  • !!文末有pdf书籍、笔记思维导图、随书代码打包下载地址,需要请自取!阅读「书籍精读系列」所有笔记,请移步:推荐收藏-JavaScript书籍精读笔记系列导航

第一章 面向对象的JavaScript

  • JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。JavaScript也没有在语言层面提供对抽象类和接口的支持

1.1.动态类型语言和鸭子类型

  • 静态类型语言在编译时便确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型
  • 鸭子类型的通俗说法是:如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子
  • 鸭子类型指导我们只关注对象的行为,而不关注对象本身,也就是关注HAS-A,而不是IS-A
  • 在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:面向接口编程,而不是面向实现编程

1.2.多态

  • 多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈
  • 一段”多态“的JavaScript代码:多态背后的思想是将“做什么”和“谁去做以及怎么去做”分离开,也就是将“不变的事物”与“可能改变的事物”分离开来
  • 类型检查和多态:静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类
  • JavaScript的多态:多态的思想实际上是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底先要消除类型之间的耦合关系;在JavaScript中,并不需要诸如向上转型之类的技术取得多态的结果;
  • 多态在面向对象程序设计中的作用:多态最根本的作用就是通过把过程化的条件分支语句转换为对象的多态性,从而消除这些条件分支语句
  • 设计模式与多态:GoF所著的《设计模式》,完全是从面向对象设计的角度出发的,通过对封装、继承、多态组合等技术的反复使用,提炼出一些可重复使用的面向对象设计技巧

1.3.封装

  • 封装的目的是将信息隐藏
  • 封装数据:但JavaScript并没有提供对private、protected、public这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出public和private这两种封装性
  • 封装实现:封装的目的是将信息隐藏。封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等;封装使得对象之间的耦合变松散,对象之间只通过暴露的API接口来通信;
  • 封装类型:封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口类进行的;JavaScript本身也是一门类型模糊的语言。在封装类型方面,JavaScript没有能力,也没有必要做得更多;
  • 封装变化:《设计模式》一书中共归纳总结了23种设计模式。从意图上区分,这23种设计模式分别被划分为创建型模式、结构型模式和行为型模式

1.4.原型模式和基于原型继承的JavaScript对象

  • 原型模式不单是一种设计模式,也被称为一种编程范性
  • 使用克隆的原型模式:原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript5提供了Object.create方法,可以用来克隆对象
  • 克隆是创建对象的手段:但原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段
  • 体验Io语言:在JavaScript语言中不存在类的概念,对象也并非从类中创建出来的,所有的JavaScript对象都是从某个对象上克隆而来的;JavaScript基于原型的面向对象系统参考了Self语言和Smalltalk语言;
  • 原型编程范性的一些规则:Io语言和JavaScript语言一样,基于原型链的委托机制就是原型链继承的本质;原型编程中的一个重要特性,即当对象无法响应某个请求时,会把该请求委托给它自己的原型;原型编程范型至少包括以下基本规则(所有的数据都是对象;要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它;对象会记住它的原型;如果对象无法响应某个请求,它会把这个请求委托给它自己的原型)
  • JavaScript中的原型继承:事实上,JavaScript中的根对象是Object.prototype对象。Object.prototype对象是一个空的对象;JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new运算符来调用函数时,此时的函数就是一个构造器。用new运算符来创建对象的过程,实际上也只是先克隆Object.prototype对象,再进行一些其他额外操作的过程;就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给它的构造器的原型;虽然JavaScript的对象最初都是由Object.prototype对象克隆而来的,但对象构造器的原型并不仅限于Object.prototype上,而是可以动态指向其他对象;留意一点,原型链并不是无限长的;
  • 原型模式是一种设计模式,也是一种编程泛型,它构成了JavaScript这门语言的根本

第二章 this、call、apply

2.1.this

  • 跟别的语言大相径庭的是,JavaScript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境
  • this的指向大致可以分为以下4种(作为对象的方法调用;作为普通函数的调用;构造器调用;Function.prototype.call或Function.prototype.apply调用);
  • 1.作为对象的方法调用:this指向该对象
  • 2.作为普通函数调用:this指向全局对象;在ECMAScript5的strict模式下,这种情况下的this已经被规定为不会指向全局对象,而是undefined;
  • 3.构造器调用:当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象
  • 4.Function.prototype.call或Function.prototype.apply调用(可以动态的改变传入函数的this;call和apply方法能很好的体现JavaScript的函数式语言特性,在JavaScript中,几乎每一次编写函数式语言风格的代码,都离不开call和apply)
  • 丢失的this:当用另一个变量getName2来引用obj.getName,并且调用getName2时,此时是普通函数调用方式,this是指向全局window的,所以程序的执行结果是undefined

2.2.call和apply

  • ECMA3给Function的原型定义了两个方法,它们是Function.prototype.call和Function.prototype.apply
  • call和apply的区别:区别仅在于传入参数形式的不同;apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合;call传入的参数数量不固定,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数;当调用一个函数时,JavaScript的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript的参数在内部就是用一个数组表示的。从这个意义上说,apply比call的使用率更高,我们不必关心具体有多少参数被传入函数;
  • call和apply的用途:1.改变this指向(最常见的用途);2.Function.prototype.bind(大部分高级浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this指向);3.借用其他对象的方法;

第三章 闭包和高阶函数

  • 函数式语言的鼻祖是LISP,JavaScript在设计之初参考了LISP两大方言之一的Scheme,引入了Lambda表达式、闭包、高阶函数等特性

3.1.闭包

  • 闭包的形成与变量的作用域以及变量的生存周期密切相关
  • 变量的作用域:指变量的有效范围
  • 变量的生存周期:全局变量的生成周期是永久的,除非主动销毁它;在函数内用var关键字声明的局部变量,当退出函数时,这些局部变量就失去了价值,他们会随着函数调用的结束而被销毁;
  • 闭包的更多作用:1.封装变量(闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”);2.延长局部变量的寿命;
  • 闭包和面向对象设计:在JavaScript语言的祖先Scheme语言中,甚至都没有提供面向对象的原生设计,但可以使用闭包来实现一个完整的面向对象系统
  • 用闭包实现命令模式:命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者之间的耦合关系
  • 闭包与内存管理:一种怂人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用;局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去;在基于引用技术策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的;

3.2.高阶函数

  • 高阶函数是指至少满足下列条件之一的函数:1.函数作为参数传递;2.函数作为返回值输出;3.高阶函数实现AOP;4.高阶函数的其他应用;
  • 1.函数作为参数传递:其中一个重要应用场景就是常见的回调函数;Array.prototype.sort接受一个函数作为参数,这个函数里面封装了数组元素的排序规则;
  • 2.函数作为返回值输出:函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙;1.判断数据的类型,isType函数;2.getSingle,单例模式的例子;
  • 3.高阶函数实现AOP:AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能包括日志统计、安全控制、异常处理等;在Java语言中,可以通过反射和动态代理机制来实现AOP技术。而在JavaScript这种动态语言中,AOP的实现更加简单,这是JavaScript与生俱来的能力;使用AOP的方式来给函数添加职责,也是JavaScript语言中一种非常特别和巧妙的装饰者模式实现;
  • 4.高阶函数的其他应用:1.currying,函数柯里化;2.uncurrying;3.函数节流;4.分时函数;5.惰性加载函数;

第四章 单例模式

  • 单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点
  • 单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等

4.1.实现单例模式

  • 要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象

4.2.透明的单例模式

  • 实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样

4.3.用代理实现单例模式

  • 把负责管理单例的代码移除出去

4.4.JavaScript中的单例模式

  • Douglas Crockford多次把全局变量称为JavaScript中最糟糕的特性
  • 在对JavaScript的创造者Brendan Eich的访谈中,他本人也承认全局变量是设计上的失误,是在没有足够的时间思考一些东西的情况下导致的结果
  • 以下几种方式可以相对降低全局变量带来的命名污染
  • 1.使用命名空间:适当的使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量
  • 2.使用闭包封装私有变量:把一些变量封装在闭包的内部,只暴露一些接口跟外界通信

4.5.惰性单例

  • 惰性单例指的是在需要的时候才创建对象实例
  • 以WebQQ的登录浮窗为例:可以用一个变量来判断是否已经创建过登录浮窗

4.6.通用的惰性单例

  • 上一节还有如下问题:违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部;如果下次需要创建页面中唯一的iframe、script等用来跨域请求数据,必须照抄一遍代码;
  • 把如何管理单例的逻辑从原来的代码中抽离出来

4.7.小结

  • 单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个
  • 更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法,这两个方法组合起来才具有单例模式的威力

第五章 策略模式

  • 策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

5.1.使用策略模式计算奖金

  • 以年终奖的计算为例:1.最初if判断的代码实现;2.使用组合函数重构代码;3.使用策略模式重构代码;
  • 策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分分隔开始每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来
  • 一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类

5.2.JavaScript版本的策略模式

  • 实际上在JavaScript语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数

5.3.多态在策略模式中的体现

5.4.使用策略模式实现缓动动画

  • 实现动画效果的原理:动画片是把一些差距不大的原画以较快ide帧数播放,来达到视觉上的动画效果;JavaScript中,可以通过连续改变元素的某个CSS属性,比如left、top、background-position来实现动画效果;
  • 思路和一些准备工作:需要提前记录一些有用的信息
  • 让小球运动起来:Animate.prototype.start = function(propertyName, endPos, duration, easing){}

5.5.更广义的“算法”

  • 从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封装算法,未免有一点大材小用
  • 在实际开发中,我们通常会把算法的含义扩散开来,是策略模式也可以用来封装一系列的“业务规则”

5.6.表单校验

  • 表单校验的第一个版本:多个if判断
  • 用策略模式重构表单校验:先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则
  • 给某个文本输入框添加多种检验规则

5.7.策略模式的优缺点

  • 策略模式是一种常用且有效的设计模式,本章提供了计算奖金、缓动动画、表单校验这Sanger例子来加深对策略模式的理解
  • 总结策略模式的一些优点:策略模式利用组合、委托和多态等技术和思想,可以有效避免多重条件选择语句;策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换、易于理解、易于扩展;策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴模式;在策略模式中利用组合和委托来让Context拥有执行算法的能力,这yeshiva继承的一种更轻便的替代方案;

5.8.一等函数对象与策略模式

  • 实际上在JavaScript这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中

5.9.小结

  • 在JavaScript语言的策略模式中,策略类往往被函数所代替,这是策略模式就成为一种“隐形”的模式

第六章 代理模式

  • 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问
  • 代理模式的关键是:当客户不方便直接访问一个对象那个或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

6.1.第一个例子-小明追MM的故事

  • 让小明和MM共同的朋友代为送花

6.2.保护代理和虚拟代理

  • 保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象
  • 虚拟代理是最常用的一种代理模式

6.3.虚拟代理实现图片预加载

  • 图片预加载,常用的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点

6.4.代理的意义

  • 实际上我们需要的只是给img节点设置src,预加载图片只是一个锦上添花的功能
  • 代理的作用在这里体现出来,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage

6.5.代理和本体接口的一致性

  • 其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别
  • 在Java等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用
  • 在JavaScript这种动态类型语言中,我们有时通过鸭子类型来检测代理和本体是否都实现了setSrc方法,另外大多数时候甚至干脆不做检测,全部依赖程序员的自觉性,这对于程序的健壮性是有影响的

6.6.虚拟代理合并HTTP请求

  • 文件同步的功能
  • 解决方案是,可以通过一个代理函数来收集一段时间之内的请求,最后一次性发送给服务器

6.7.虚拟代理在惰性加载中的应用

  • miniConsole.js开源项目,希望在按下F12来主动唤出控制台的时候进行加载

6.8.缓存代理

  • 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
  • 缓存代理的例子-计算乘积
  • 缓存代理用于ajax异步请求数据
  • 常见的分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据

6.9.用高阶函数动态创建代理

  • 通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理

6.10其他代理模式

  • 代理模式的变体种类非常多:防火墙代理;远程代理;保护代理;智能引用代理;写时复制代理;

6.11.小结

  • 代理模式包括许多小分类,在JavaScript开发中最常见的是虚拟代理和缓存代理
  • 我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式,当真正发现不方便直接访问某个对象的时候,再编写代理也不迟

第七章 迭代器模式

  • 迭代器模式 是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示

7.1.jQuery中的迭代器

  • 迭代器模式无非就是循环访问聚合对象中的各个元素

7.2.实现自己的迭代器

7.3.内部迭代器和外部迭代器

  • 迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景
  • 在一些没有闭包的语言中,内部迭代器本身的实现也相当复杂
  • 外部迭代器必须显式的请求迭代下一个元素
  • 外部迭代器虽然调用方式相对复杂,但它的实用面更广,也能满足更多变的需求

7.5.倒序迭代器

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

7.6.中止迭代器

  • jQuery的each函数约定如果回调函数的执行结果返回false,则提前终止循环

7.7.迭代器模式的应用举例

  • 根据不同的浏览器获取相应的上传组件对象

7.8.小结

  • 迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式

第八章 发布订阅模式

  • 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
  • 在JavaScript开发中,我们一般用事件模型来替代传统的发布-订阅模式

8.1.现实中的发布-订阅模式

  • 售楼的例子:购房者的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们

8.2.发布-订阅模式的作用

  • 发布-订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案
  • 发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式的调用另外一个对象的某个接口

8.3.DOM事件

  • 实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过发布-订阅模式
  • 还可以随意增加或者删除订阅者,增加任何订阅者都不会影响发布者代码的编写

8.4.自定义事件

  • 如何一步步实现发布-订阅模式:首先要指定好谁充当发布者(比如售楼处);然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面的订阅者回调函数(遍历花名册,挨个发短信);

8.5.发布-订阅模式的通用实现

  • 把发布-订阅的功能提取出来,放在一个单独的对象内

8.7.真实的例子-网站登录

  • 网站里有header头部、nav导航、消息列表、购物车等模块。这些模块有一个共同的前提条件,就是必须先用Ajax异步请求获取用户的登录信息
  • 更重要的一点是,我们不知道除了header头部、nav导航、消息列表、购物车之外,将来还有哪些模块需要使用这些用户信息
  • 用发布-订阅模式重写后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务究竟要做什么,也不想去了解它们的内部细节

8.8.全局的发布-订阅对象

  • 买房子未必要亲自去售楼处,我们只要把订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司来发布房子消息。这样一来,我们不用关心消息是来自哪个房产公司,我们在意的是能否顺利收到消息
  • 发布-订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似”中介者“的角色,把订阅者和发布者联系起来

8.9.模块间通信

  • 要留意一个问题,模块之间如果用了太多的全局发布-订阅模式来通信,那么模块与模块之间的联系就被隐藏到了后面。最终搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其他模块调用

8.10.必须先订阅再发布吗

  • 在某些情况下,我们需要先将这条消息保存下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。就如同QQ中的离线消息一样,离线消息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条消息
  • 为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件

8.12.JavaScript实现发布-订阅模式的便利性

  • 在Java中实现一个自己的发布-订阅模式,通常会把订阅者对象自身当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如update的方法,供发布者对象在适合的时候调用
  • 而在JavaScript中,我们用注册回调函数的形式来代替传统的发布-订阅模式,显得更加优雅和简单
  • 在JavaScript中,我们无需去选择使用推模型还是拉模型。推模型是指事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。拉模型不同的地方是,发布者仅仅通知订阅者事件已经发生了,此外发布者需提供一些公开的接口供订阅者来主动拉去数据

8.13.小结

  • 发布-订阅模式的优点非常明显,一是时间上的解耦,而是对象之间的解耦
  • 应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写
  • 缺点:创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解

第九章 命令模式

9.1.命令模式的用途

  • 命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令
  • 命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

9.2.命令模式的例子-菜单程序

  • 在这里运用命令模式的理由:点击了按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者
  • 设计模式的主题总是把不变的事务和变化的事物分离开来,命令模式也不例外

9.3.JavaScript中的命令模式

  • 在面向对象设计中,命令模式的接收者被当成command对象的属性保存起来,同时约定执行命令的操作command.execute方法

9.4.撤销命令

  • 命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作
  • 撤销是命令模式里一个非常有用的功能,试想一下开发一个围棋程序的时候,我们把每一步棋子的变化都封装成命令,则可以轻而易举的实现毁棋功能

9.6.命令队列

  • 命令对象的生命周期跟初始请求发生的时间无关,command对象的execute方法可以在程序运行的任何时刻执行,即使点击按钮的请求早已发生,但我们的命令对象仍然是有生命的

9.7.宏命令

  • 宏命令是命令模式与组合模式的联用产物

9.9.小结

  • 跟许多其他语言不同,JavaScript可以用高阶函数非常方便地实现命令模式
  • 命令模式在JavaScript语言中是一种隐形的模式

第十章 组合模式

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

10.1.回顾宏命令

  • 宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个execute方法负责执行命令
  • 在macroCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象

10.2.组合模式的用途

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

10.3.请求在树中传递的过程

  • 在组合模式中,请求在树中传递的过程总是遵循一种逻辑
  • 作为客户,只需要关心树最顶层的组合对象,客户只需要请求这个组合对象,请求便会沿着树往下传递,依次到达所有的叶对象

10.4.更强大的宏命令

  • 基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度

10.5.抽象类在组合模式中的作用

  • 组合模式最大的优点在于可以一致地对待组合对象和基本对象。客户不需要知道当前处理的宏命令还是普通命令,只要它是一个命令,并且有execute方法,这个命令就可以被添加到树中
  • 在JavaScript这种动态类型语言中,对象的多态性是与生俱来的,也没有编译器去检查变量的类型,所以我们通常不会去模拟一个”怪异“的抽象类,在JavaScript中实现组合模式的难点在于要保证组合对象和叶对象拥有同样的方法,这通常需要用鸭子类型的思想对它们进行接口检查
  • 在JavaScript中实现组合模式,看起来缺乏一些严谨性,我们的代码算不上安全,但能更快速和自由地开发,这既是JavaScript的缺点,也是它的优点

10.6.透明性带来的安全问题

  • 组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上有区别的

10.7.组合模式的例子-扫描文件夹

  • 文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树
  • 在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作

10.8.一些值得注意的地方

  • 1.组合模式不是父子关系:组合模式是一种HAS-A(聚合)的关系,而不是IS-A
  • 2.对叶对象操作的一致性:组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性
  • 3.双向映射关系
  • 4.用职责链模式提高组合模式性能

10.10.何时使用组合模式

  • 适用于以下两种情况:表示对象的部分-整体层次结构;客户希望统一对待树中的所有对象;

10.11.小结

  • 组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上
  • 组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多/它们的区别只有在运行的时候才会显现出来,这会使代码难以理解

第十一章 模板方法模式

  • 一种基于继承的设计模式

11.1.模板方法模式的定义和组成

  • 模板方法模式是一种只需使用继承就可以实现的非常简单的模式
  • 模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。
  • 通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法

11.2.第一个例子-Coffee or Tea

  • 先泡一杯咖啡
  • 泡一壶茶
  • 3.分离出共同点:都能整理为下面四步:1.把水煮沸;2.用沸水冲泡饮料;3.把饮料倒进杯子;4.加调料
  • 4.创建Coffee子类和Tea子类:Beverage.prototype.init被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法

11.3.抽象类

  • 首先要说明的是,模板方法是一种严重依赖抽象类的设计模式
  • 抽象类的作用
  • 抽象方法和具体方法
  • 用Java实现Coffee or Tea的例子
  • JavaScript没有抽象类的缺点和解决方案:JavaScript并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具体类型,由于JavaScript是一门“类型模糊”的语言,所以隐藏对象的类型在JavaScript中并不重要;另一方面,当我们在JavaScript中使用原型继承来模拟传统的类式继承时,并没有编译器帮助我们进行任何形式的检查,我们也没有办法保证子类会重写父类中的“抽象方法”;在Java中编译器会保证子类会重写父类中德抽象方法,但在JavaScript中却没有进行这些检查工作;两种变通的解决方案(第一种方案是用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法;第2种方案是让Beverage.prototype.brew等方法直接抛出一个异常)

11.4.模板方法模式的使用场景

  • 模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空

11.5.钩子方法

  • 钩子方法(hook)可以用来解决这个问题(让子类不受某个步骤的约束),放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要”挂钩“,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能

11.6.好莱坞原则

  • 好莱坞原则:不要来找我,我会给你打电话
  • 在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候,以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”
  • 当我们用模板方法模式编写一个程序时,就意味着子类放弃对自己的控制权,而是改用父类通知子类,哪些方法应该在什么时候被调用
  • 好莱坞原则还常常应用于其他模式和场景,例如发布-订阅模式和回调函数

11.7.真的需要”继承“吗

  • 模板方法模式是为数不多的基于继承的设计模式,但JavaScript语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的

11.8.小结

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

第十二章 享元模式

写在前面

  • 享元(flyweight)模式是一种用于性能优化的模式
  • 享元模式的核心是运用共享技术来有效支持大量细粒度的对象
  • 如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了

12.1.初识享元模式

  • 50种男士内衣和50种女士内衣穿在塑料模特上拍成广告照片的例子

12.2.内部状态与外部状态

  • 享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)
  • 享元模式的目标是尽量减少共享对象的数量
  • 关于如何划分内部状态和外部状态的几条经验:内部状态存储于对象内部;内部状态可以被一些对象共享;内部状态独立于具体的场景,通常不会改变;外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享;
  • 享元模式是一种用时间换空间的优化模式
  • 通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象
  • 使用享元模式的关键时如何区别内部状态和外部状态
  • 可以被对象共享的属性通常被划分为内部状态,如同不管什么样式的衣服,都可以按照性别不同,穿在同一个男模特或者女模特身上,模特的性别就可以作为内部状态储存在共享对象的内部
  • 外部状态取决于具体的场景,并根据场景而变化,就像例子中每件衣服都是不同的,它们不能被一些对象共享,因此只能被划分为外部状态

12.3.享元模式的通用结构

12.4.文件上传的例子

  • 在微云上传模块的开发中,就曾经借助享元模式提升了程序的性能
  • 1.对象爆炸:支持同时选择2000个文件,每一个文件都对应着一个JavaScript上传对象的创建;支持好几种上传方式,比如浏览器插件、Flash和表单上传等;
  • 2.享元模式重构文件上传:upload对象必须依赖uploadType属性才能工作,这是因为插件上传、Flash上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,必须在对象创建之初就明确它是什么类型的插件,才可以在程序的运行过程中,让它们分别调用各自的start、pause、cancel、del等方法
  • 3.剥离外部状态:明确了uploadType作为内部状态之后,再把其他的外部状态从构造函数中抽离出来,Upload构造函数中只保留uploadType参数
  • 4.工厂进行对象实例化
  • 5.管理器封装外部状态

12.5.享元模式的实用性

  • 一般来说,以下情况发生时便可以使用享元模式
  • 1.一个程序中使用了大量的相似对象
  • 2.由于使用了大量对象,造成很大的内存开销
  • 3.对象的大多数状态都可以变为外部状态
  • 4.剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象

12.6.再谈内部状态和外部状态

  • 有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象
  • 1.没有内部状态的享元:管理器部分的代码不需要改动,还是负责剥离和组装外部状态。可以看到,当对象没有内部状态的时候,生产共享对象的工厂实际上变成了一个单例工厂
  • 2.没有外部状态的享元:享元模式的关键时区别内部状态和外部状态。享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻再把外部状态组成进共享对象

12.7.对象池

  • 对象池技术的应用非常广泛,HTTP连接池和数据库连接池都是其代表应用
  • 1.对象池实现
  • 2.通用对象池实现
  • 对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程

12.8.小结

  • 享元模式是为了解决性能问题而生的模式
  • 在一个存在大量相似对象的系统中,享元模式可以很好的解决大量对象带来的性能问题

第十三章 职责链模式

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

13.1.现实中的职责链模式

  • 公交车上递硬币的例子
  • 职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系

13.2.实际开发中的职责链模式

  • if..else再套if..else

13.3.用职责链模式重构代码

  • 去掉嵌套的条件分支语句,拆分成多个小函数

13.5.异步的职责链

  • 遇到异步的问题,比如要在节点函数中发起一个ajax异步请求,异步请求返回的结果才能决定是否继续在职责链中passRequest
  • 异步的职责链加上命令模式(把ajax请求封装成命令对象),可以很方便的创建一个异步ajax队列库

13.6.职责链模式的优缺点

  • 职责链模式的最大优点就是解耦了请求发送者和N个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可
  • 其次,使用了职责链模式之后,链中的节点对象可以灵活地拆分重组。增加或者删除一个节点,或者改变节点在链中的位置都是轻而易举的事情
  • 还有一个优点,那就是可以手动指定起始节点,请求并不是非得从链中的第一个节点开始传递
  • 一个弊端,首先我们不能保证某个请求一定会被链中德节点处理
  • 另外,职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,要避免过长的职责链带来的性能损耗

13.7.用AOP实现职责链

  • 利用JavaScript的函数式特性,有一种更加方便的方法来创建职责链
  • 改写一下之前的Function.prototype.after函数,使得第一个函数返回'nextSuccessor'时,将请求继续传递给下一个函数

13.8.用职责链模式获取文件上传对象

  • 之前创建了一个迭代器来迭代获取合适的文件上传对象,其实用职责链模式可以更简单,完全不用创建这个多余的迭代器

13.9.小结

  • 在JavaScript开发中,职责链模式是最容易被忽视的模式之一
  • 职责链模式可以很好的帮助我们管理代码,降低发起请求的对象和处理请求时的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点
  • 无论是作用域链、原型链,还是DOM节点中的事件冒泡,我们都能从中找到职责链模式的影子
  • 职责链还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率

第十四章 中介者模式

写在前面

  • 面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性
  • 中介者模式的作用就是解除对象与对象之间的紧耦合关系
  • 增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立的改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

14.1.现实中的中介者

  • 1.机场指挥塔

14.2.中介者模式的例子--泡泡堂游戏

  • 1.为游戏增加队伍:需要在每个玩家死亡的时候,都遍历其他队友的生存状况,如果队友全部死亡,则这局游戏失败,同时敌人队伍的所有玩家都取得胜利
  • 2.玩家增多带来的困扰:可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的
  • 3.用中介者模式改造泡泡堂:playerDirector开放一个对外暴露的接口receiveMessage,负责接收player对象发送的消息,而player对象发送消息的时候,总是把自身this作为参数发送给playerDirector,以便playerDirector识别消息来自于哪个玩家对象;除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象;

14.3.中介者的例子--购买商品

  • 1.开始编写代码
  • 2.对象之间的联系
  • 3.可能遇到的困难
  • 4.引入中介者

14.4.小结

  • 中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)
  • 在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方
  • 中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护
  • 最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象
  • 一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码

第十五章 装饰者模式

写在前面

  • 在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责
  • 装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象
  • 装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责

15.1.模拟传统面向对象语言的装饰者模式

  • 这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象

15.2.装饰者也是包装器

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

15.3.回到JavaScript的装饰者

15.4.装饰函数

  • 在JavaScript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境
  • 现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路
  • 一种答案,通过保存原引用的方式就可以改写某个函数

15.5.用AOP装饰函数

  • 首先给出Function.prototype.before方法和Function.prototype.after方法
  • 把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前运行(前置装饰),这样就实现了动态装饰的效果
  • Function.prototype.after的原理跟Function.prototype.before一模一样,唯一不同的地方在于让新添加的函数在原函数执行之后再执行

15.6.AOP的应用实例

  • 不论是业务代码的编写,还是框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统
  • 1.数据统计上报:分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一
  • 2.用AOP动态改变函数的参数:解决CSRF攻击最简单的一个办法就是在HTTP请求中带上一个Token参数
  • 3.插件式的表单验证:分离校验输入和提交Ajax请求的代码,把校验输入的逻辑放到validata函数中,并且约定当validata函数返回false的时候,表示校验未通过
  • 这种装饰方式叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响

15.7.装饰者模式和代理模式

  • 这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求
  • 代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情
  • 装饰者模式的作用就是为对象动态加入行为
  • 代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链

第十六章 状态模式

  • 状态模式的关键是区分事务内部的状态,事务内部状态的改变往往会带来事物的行为改变

16.1.初识状态模式

  • 电灯灯光切换的例子
  • 通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
  • 使用状态模式的好处很明显,它可以使一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码
  • 另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的if、else条件分支语言来控制状态之间的转换

16.2.状态模式的定义

  • GoF中对状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
  • 第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化
  • 第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果

16.4.缺少抽象类的变通方式

  • 在Java中,所有的状态必须继承自一个State抽象父类,当然如果没有共同的功能值得放入抽象父类中,也可以选择实现State接口

16.5.另一个状态模式示例--文件上传

  • 文件上传程序中有扫描、正在上传、暂停、上传成功、上传失败这几种状态,音乐播放器可以分为加载中、正在播放、暂停、播放完毕这几种状态
  • 1.更复杂的切换条件
  • 2.一些准备工作
  • 3.开始编写代码
  • 4.状态模式重构文件上传

16.6.状态模式的优缺点

  • 状态模式的优点如下:1、状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换;2、避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支;3、用对象代替字符串来记录当前状态,使得状态的切换更加一目了然;4、Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响;
  • 状态模式的缺点:1、会在系统中定义许多状态类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象;2、由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑;

16.7.状态模式中的性能优化点

  • 一些比较大的优化点:有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁它们;为每个Context对象都创建了一组state对象,实际上这些state对象之间是可以共享的,各Context对象可以共享一个state对象;

16.8.状态模式和策略模式的关系

  • 状态模式和策略模式向一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同
  • 策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行
  • 它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事发生在状态模式内部

16.9.JavaScript版本的状态机

  • 状态模式是状态机的实现之一,但在JavaScript这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外一点,JavaScript可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象

16.10.表驱动的有限状态机

  • 另外一种实现状态机的方法,核心是基于表驱动的。可以在表中很清楚的看到下一个状态是由当前状态和行为共同决定的。这样一来,我们就可以在表中查找状态,而不必定义很多条件分支

16.11.实际项目中的其他状态机

  • 在实际开发中,很多场景都可以用状态机来模拟,比如一个下拉菜单在hover动作下有显示、悬浮、隐藏等状态;一次TCP请求有建立连接、监听、关闭等状态;一个格斗游戏中人物有攻击、防御、跳跃、跌倒等状态

16.12.小结

  • 状态模式也许是被大家低估的模式之一
  • 实际上,通过状态模式重构代码之后,很多杂乱无章的代码会变得清晰

第十七章 适配器模式

写在前面

  • 适配器模式的作用是解决两个软件实体间的接口不兼容的问题
  • 在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求
  • 两种解决办法,第一种是修改原来的接口实现,第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道

17.1.现实中的适配器

  • 几个现实生活中的适配器模式:1.港式插头转换器;2.电源适配器;3.USB转接口

17.2.适配器模式的应用

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

17.3.小结

  • 适配器模式是一种相对简单的模式
  • 有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式
  • 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能
  • 外观模式的作用倒是和适配器模式比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口

第十八章 单一职责原则

  • 前辈总结的这些设计原则通常指的是单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、合成复用原则和最少知识原则

写在前面

  • 单一职责原则(SRP)的职责被定义为“引起变化的原因”
  • SRP原则体现为:一个对象(方法)只做一件事情

18.1.设计模式中的SRP原则

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

18.2.何时应该分离职责

  • SRP原则是所有原则中最简单也是最难正确运用的原则之一
  • 一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们
  • 另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义

18.4.SRP原则的优缺点

  • SRP原则的优点时降低单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试
  • SRP原则的一些缺点,最明显的是会增加编写代码的复杂度

第十九章 最少知识原则

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

19.1.减少对象之间的联系

  • 最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互

19.2.设计模式中的最少知识原则

  • 最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式
  • 1.中介者模式(通过增加一个中介者对象,让所有的相关对象都通过中介者对象来通信,而不是互相引用)
  • 2.外观模式(外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现;外观模式的作用主要有两点(为一组子系统提供一个简单便利的访问入口;隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节))

19.3.封装在最少知识原则中的体现

  • 封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问
  • 最少知识原则也叫做迪米特法则(Law of Demeter,LoD)

第二十章 开放-封闭原则

写在前面

  • 在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则
  • 开放-封闭原则定义如下:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改

20.1.扩展window.onload函数

  • 通过增加代码,而不是修改代码的方式,来给window.onload函数添加新的功能
  • 通过动态装饰函数的方式,我们完全不用理会从前window.onload函数的内部实现,无论它的实现优雅或是丑陋

20.2.开放和封闭

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

20.3.用对象的多态性消除条件分支

  • 利用多态的思想,我们把程序中不变的部分隔离出来(动物都会叫),然后把可变的部分封装起来(不同类型的动物发出不同的叫声),这样一来程序就具有了可扩展性

20.4.找出变化的地方

  • 我们还是能找到一些让程序尽量遵守开放-封闭原则的规律,最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来
  • 通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离出来
  • 除了利用对象的多态性之外,还有其他方式可以帮助我们编写遵守开放-封闭原则的代码:1.放置挂钩(hook);2.使用回调函数

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

  • 几乎所有的设计模式都是遵守开放-封闭原则的,我们见到的好设计,通常都经得起开放-封闭原则的考验

20.6.开放-封闭原则的相对性

  • 实际上,让程序保持完全封闭是不容易做到的
  • 而且让程序符合开放-封闭原则的代价是引入更多的抽象层次,更多的抽象有可能会增大代码的复杂度

第二十一章 接口和面向接口编程

谈到接口时通常涉及以下几种含义

  • 通过主动暴露的接口来通信,可以隐藏软件系统内部的工作细节。这也是我们最熟悉的第一种接口含义
  • 第二种接口是一些语言提供的关键字,比如Java的interface。interface关键字可以产生一个完全抽象的类
  • 第三种接口即是我们谈论的“面向接口编程”中的接口,接口的含义在这里体现得更为抽象

21.1.回到Java的抽象类

  • 静态类型语言通常设计为可以“向上转型”。当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类
  • 从过程上来看,“面向接口编程”其实是“面向超类型编程”

21.2.interface

  • 虽然很多人在实际使用中刻意区分抽象类和interface,但使用interface实际上也是继承的一种方式,叫做接口继承

21.3.JavaScript语言是否需要抽象类和interface

  • 抽象类和interface的作用主要都是以下两点(1.通过向上转型来隐藏对象的真正类型,以表现对象的多态性;2.约定类与类之间的一些契约行为)
  • 很少人在JavaScript开发中去关心对象的真正类型
  • 因为不需要进行向上转型,接口在JavaScript中的最大作用就退化到了检查代码的规范性

21.4.用鸭子类型进行接口检查

  • 鸭子类型是动态语言面向对象设计中德一个重要概念。利用鸭子类型的思想,不必借助超类型的帮助,就能在动态类型语言中轻松地实现本章提到的设计原则:面向接口编程,而不是面向实现编程

21.5.用TypeScript编写基于interface的命令模式

  • Typescript是微软开发的一种编程语言,是JavaScript的一个超集。Typescript代码最终会被编译成原生的JavaScript代码执行。通过Typescript,我们可以使用静态语言的方式来编写JavaScript程序

第二十二章 代码重构

  • 模式和重构之间有着一种与生俱来的关系。从某种角度来看,设计模式的目的即使为许多重构行为提供目标

22.1.提炼函数

  • 这是一种很常见的优化工作,这样做的好处主要有以下几点:1.避免出现超大函数;2.独立出来的函数有助于代码复用;3.独立出来的函数更容易被覆写;4.独立出来的函数如果拥有一个良好的命名,本身就起到注释的作用;

22.2.合并重复的条件片段

  • 条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作

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

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

22.4.合理使用循环

  • 合理利用循环不仅可以完成同样的功能,还可以使代码量更少

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

  • 关于“函数只有一个出口”,往往会有一些不同的看法
  • 有一个常见的技巧,即在面对一个嵌套的if分支时,我们可以把外层if表达式进行反转

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

  • 有时候一个函数可能接受多个参数,而参数的数量越多,函数就越难以理解和使用

22.7尽量减少参数数量

  • 在实际开发中,向函数传递参数不可避免,但我们应该尽量减少函数接收的参数数量

22.8.少用三目运算符

  • 三目运算符性能高,代码量少,这理由很难站住脚
  • 相比损失的代码可读性和可维护性,三目运算符节省的代码量也可以忽略不计

写在后面


小磊哥er
409 声望37 粉丝

大前端工程师