# 设计模式
# 多态
某些时候,在享受静态语言类型检查带来的安全性的同时,我们亦会感觉被束缚住了手脚。为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。这就像我们在描述天上的一只麻雀或者一只喜鹊时,通常说“一只麻雀在飞”或者“一只喜鹊在飞”。但如果想忽略它们的具体类型,那么也可以说“一只鸟在飞”
在JavaScript中,并不需要诸如向上转型之类的技术来取得多态的效果。
多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。换句话说,多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。 对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的
在JavaScript这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够被四处传递。当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是“多态性”的一种体现,也是很多设计模式在JavaScript中可以用高阶函数来代替实现的原因。
# 封装
封装的目的是将信息隐藏。
封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。
# 原型模式和基于原型继承的JavaScript对象系统
原型模式选择了另外一种方式,我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。
原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。
依赖倒置原则提醒我们创建对象的时候要避免依赖具体类型
目前我们一直在讨论“对象的原型”,就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给它的构造器的原型。
JavaScript给对象提供了一个名为__proto__的隐藏属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,即{Constructor}.prototype。
实际上,__proto__就是对象跟“对象构造器的原型”联系起来的纽带。正因为对象要通过__proto__属性来记住它的构造器的原型,如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型
# 闭包
对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。
# 高阶函数
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。
# 策略模式
策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。
# 策略模式的优缺点
策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
# 保护代理和虚拟代理
虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建
代理模式包括许多小分类,在JavaScript开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
# 命令模式
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。命令模式的例子——菜单程序
设计模式的主题总是把不变的事物和变化的事物分离开来
# 智能命令与傻瓜命令
一般来说,命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦。
# 何时使用组合模式
客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆if、else语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力
# 模板方法模式的定义和组成
如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。但在JavaScript中,我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择。
# 抽象类
把对象的真正类型隐藏在抽象类或者接口之后,这些对象才可以被互相替换使用。这可以让我们的Java程序尽量遵守依赖倒置原则。
抽象类也可以表示一种契约。
# 享元模式
享元模式的目标是尽量减少共享对象的数量,享元模式是一种用时间换空间的优化模式。
# 享元模式的适用性
一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
# 职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
# 现实中的职责链模式
请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。
无论是作用域链、原型链,还是DOM节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率。学会使用职责链模式,相信在以后的代码编写中,将会对你大有裨益。
# 中介者模式
面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)
一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
# 装饰者也是包装器
请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会
# 装饰者模式和代理模式
装饰者模式的作用就是为对象动态加入行为,装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样
# 状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
# 状态模式的优缺点
状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
状态模式的缺点是会在系统中定义许多状态类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。
# 状态模式和策略模式的关系
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
它们之间的区别是**策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,**所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在
# 适配器模式
适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用
装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次 有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。
# 设计原则和编程技巧
# 单一职责原则
单一职责原则(SRP)的职责被定义为“引起变化的原因”。
# 何时应该分离职责
一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们
# 违反SRP原则
在方便性与稳定性之间要有一些取舍。
# SRP原则的优缺点
但SRP原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
# 最少知识原则
最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式 为一组子系统提供一个简单便利的访问入口。 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节
# 开放-封闭原则
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。在项目需求变迁的过程中,我们经常会找到相关代码,然后改写它们。这似乎是理所当然的事情,不改动代码怎么满足新的需求呢?想要扩展一个模块,最常用的方式当然是修改它的源代码
# 用对象的多态性消除条件分支
过多的条件分支语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新的if语句时,都要被迫改动原函数。把if换成switch-case是没有用的,这是一种换汤不换药的做法。实际上,每当我们看到一大片的if或者swtich-case语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们。
# 找出变化的地方
最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来。 通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。
# 回到Java的抽象类
静态类型语言通常设计为可以“向上转型”。当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。就像看到天上有只麻雀,我们既可以说“一只麻雀在飞”,也可以说“一只鸟在飞”,甚至可以说成“一只动物在飞”。通过向上转型,对象的具体类型被隐藏在“超类型”身后。当对象类型之间的耦合关系被解除之后,这些对象才能在类型检查系统的监视下相互替换使用,这样才能看到对象的多态性。
Scott Meyers曾指出,只要有可能,不要从具体类继承。
向上转型。让Duck对象和Chicken对象的类型都隐藏在Animal类型身后,隐藏对象的具体类型之后,duck对象和chicken对象才能被交换使用,这是让对象表现出多态性的必经之路。
总而言之,不关注对象的具体类型,而仅仅针对超类型中的“契约方法”来编写程序,可以产生可靠性高的程序,也可以极大地减少子系统实现之间的相互依赖关系,这就是我们本章要讨论的主题:
面向接口编程,而不是面向实现编程。
从过程上来看,“面向接口编程”其实是“面向超类型编程”。当对象的具体类型被隐藏在超类型身后时,这些对象就可以相互替换使用,我们的关注点才能从对象的类型上转移到对象的行为上。“面向接口编程”也可以看成面向抽象编程,即针对超类型中的abstract方法编程,接口在这里被当成abstract方法中约定的契约行为。这些契约行为暴露了一个类或者对象能够做什么,但是不关心具体如何去做
# 用鸭子类型进行接口检查
如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子