设计原则
-
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
把会变化的部分取出并封装起来,以便以后可以轻易改动或扩充此部分,而不影响不需要变化的其他部分。
-
针对接口编程,而不是针对实现编程。
“针对接口编程”真正的意思是“针对超类型编程”。
这里所谓的“接口”有多个含义,接口是一个“概念”,也是一种
Java
的interface
构造。你可以在不涉及Java interface
的情况下 “针对接口编程”,关键就在于多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。“针对超类型编程”这句话,可以更明确地说成“变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型。“ -
多用组合,少用继承
如你所见,使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以”运行时动态地改变行为“,只要这的行为对象符合正确的接口标准即可。
-
为了交互对象之间的松耦合设计而努力
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的相互依赖降到了最低。
-
开放 - 关闭原则:类应该对扩展开放,对修改关闭。
我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
-
依赖倒置原则:要依赖抽象,不要依赖具体类。
大师:你需要实现一个披萨店,你第一件想到的事情是什么?
门徒:披萨店进行准备、烘烤、装盒,所以我的店必须能够制作许多不同风味的披萨,例如:芝士披萨、素食披萨、蛤喇比萨······
大师:没错!先从顶端开始,然后往下到具体类。但是,正如你所看到的你不想让披萨店理会这些具体类,要不然披萨店将全部依赖这些具体类。现在,”倒置“你的想法·····别从顶端开始,而是从披萨开始,然后想想看能抽象化些什么。
门徒:是的,芝士披萨、素食披萨和蛤喇披萨都是披萨,所以它们应该共享一个
Pizza
接口。大师:对了,你想要抽象化一个
Pizza
。好,现在回头重新思考如何设计披萨店。门徒:既然我已经有一个披萨抽象,就可以开始设计披萨店,而不用理会具体的披萨类了。
大师:很接近了,但是要这么做,必须靠一个工厂来将这些具体类取出披萨店。一旦你这么做了,各种不同的具体披萨类型就只能依赖一个抽象,而披萨店也会依赖这个抽象。我们已经倒置了一个商店依赖具体类的设计,而且也倒置了你的思考方式。
-
最少知识原则:只和你的密友谈话。
这个原则希望我们在设计中,不要让太多的类耦合在一起,免得修改系统中一部分,会影响到其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,会会因为太复杂而不容易被其他人了解。
-
好莱坞原则:别调用(别打电话给)我们,我们会调用(打电话给)你。
好莱坞原则和依赖倒置原则之间的关系如何?
依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高级组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。 好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。
低层组件不可以调用高层组件中的方法吗?
并不尽然。事实上,低层组件在结束时,常常会调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。
-
一个类应该只有一个引起变化的原因。
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。
这个原则告诉我们,尽量让每个类保持单一责任。
内聚 这个术语你应该听过,它用来度量一个类或模块紧密地达到单一目的或责任。
当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
内聚是一个比单一责任原则更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类容易具有很高的凝聚力,而且比背负许多责任的地内聚类更容易维护。
设计模式
策略模式
策略模式: 定义算法族,分别封装起来,让它们之间可以互相替换,正因为每一个算法都被封装起来了,所以客户可以轻易地使用不同的算法,此模式让算法的变化独立于使用算法的客户。
"有一个"可能比”是一个“更好
”有一个“关系相当有趣:每一鸭子都有一个 FlyBehavior
和一个 QucakBehavior
,好将飞行和呱呱叫委托给它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合、这种做法和”继承“不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象”组合“来的。
这是一个很重要的技巧、其实是使用了我们的第三个设计原则:多用组合,少用继承 。
如你所见,使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以”运行时动态地改变行为“,只要这的行为对象符合正确的接口标准即可。
观察者模式
观察者模式: 在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
优点:
- 观察者和可观察者之前用松耦合的方式结合,可观察者不知道观察者的细节,只知道观察者实现了观察者的接口。
缺点:
- 可观察者是一个”类“,而不是一个”接口“,更糟的是,它甚至没有实现一个接口。不幸的是,
java.util.Observable
的有许多问题,限制了它的使用和复用。 - 因为
Observable
是一个”类“,你必须设计一个类继承它。如果某类想同时具有Observable
类和另一个超类的行为,就会陷入两难,毕竟Java
不支持多重继承。
装饰者模式
装饰者模式: 动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
不过这里需要注意的是,装修模式与继承不一样,继承是需要一个子类来拓展对象的功能,而装饰模式则不一样,它是通过定义一个装饰类,在这个装饰类中持有某些对象的引用,然后通过使用对象之间的关联关系来取代类之间的继承关系。
工厂模式
工厂方法模式: 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
抽象工厂模式: 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。抽象工厂模式,其实就是工厂模式更高级的抽象。从名字可以知道,抽象二字是用来形容工厂的,那说明在抽象工厂模式中,工厂也被抽象出来了。
抽象工厂比起工厂方法,最大的区别在于:抽象工厂是两层的抽象结构,而工厂方法则只有一层抽象。这就使得抽象工厂能够表示更多的内容,而工厂方法表达的内容更少。
- 所有的工厂都是用来封装对象的创建
单件模式
单件模式: 确保一个类只有一个实例,并提供一个全局访问点。
命令模式
命令模式: 将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
- 命令模式将发出请求的对象和执行请求的对象解耦
- 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者的一个或一组动作。
空对象
NoCommand
对象是一个空对象(null object
)的例子。当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理 null
的责任转移给空对象。举例来说,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了 NoCommand
对象作为代用品,当调用它的 execute()
方法时,这种对象什么事情都不做。
在许多设计模式中,都会看到空对象的使用。甚至有些时候,空对象本身也被视为一种设计模式。
适配器模式与外观模式
适配器模式: 将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
-
装饰者: 不改变接口,但加入责任
-
适配器: 将一个接口转成另一个接口
-
外观: 让接口更简单
外观不只是简化了接口,也将客户从组件的子系统中解耦。
外观模式也允许你将客户实现从任何子系统中解耦。比方说,你得到了大笔加薪,所以想要升级你的家庭影院,采用全新的和以前不一样的接口的组件。如果当初你的客户代码是针对外观而不是针对子系统编写的,现在你就不需要改变客户代码,只需要修改外观代码(而且有可能厂商会提供新版的外观代码)。
外观和适配器可以包装许多类,但是外观的意图是简化接口,而适配器的意图是将接口转换成不同接口。
外观模式: 提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
- 当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器。
- 当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观。
- 适配器改变接口以符合客户的期望
- 外观将客户从一个复杂的子系统中解耦
- 适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象”包装“起来以简化其接口。
模板方法模式
模板方法模式 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 模板方法:子类决定如何实现算法中的步骤
- 策略:封装可互换的行为,然后使用委托来决定要采用哪一个行为
- 工厂方法:由子类决定实例化哪个具体类
观察鸭子排序的内部工作
让我们追踪 Array
类的 sort()
模板方法的工作过程。我们会看到模板方法是如何控制算法的,以及在算法中的某些点上它是如何要求我们的鸭子提供某个步骤的实现的......
-
首先,我们需要一个鸭子数组
Duck[] ducks = {new Duck("Daffy", 8), ···};
-
然后调用
Array
类的sort()
模板方法,并传入鸭子数组:Array.sort(ducks);
// sort()方法控制算法,没有类可以改变这一点。sort()依赖一个 Comparable 类提供 CompareTo()的实现 for (int i = low; i < high; i++) { ... compareTo() ... ... swap() .... }
这个
sort()
方法控制排序过程。 -
想要排序一个数组,你需要一次又一次地比较两个项目,直到整个数组都排序完毕。
当比较两只鸭子的时候,排序方法需要依赖鸭子的
CompareTo()
方法,以得知谁大谁小。第一只鸭子的CompareTo()
方法被调用,并传入另一只鸭子当成比较对象:ducks[0].compareTo(ducks[1]);
-
如果鸭子的次序不对,就用
Array
的具体swap()
方法将两者对调:// 这里不使用继承,不像典型的模板方法 swap()
-
排序方法会持续比较并对调鸭子,直到整个数组的次序是正确的!
排序的实现实际上看起来更像是策略模式,而不是模板方法模式。为什么我们要将它归为模板方法?
你之所以会这么认为,可能是因为策略模式使用对象组合。在某种程度上,你是对的———我们使用数组对象排序我们的数组,这部分和策略模式非常相似。但是请记住,在策略模式中,你所组合的类实现了整个算法。数组所实现的排序算法并不完整,它需要一个类填补 compareTo()
方法的实现。因此,我们认为这更像模板方法。
模板方法: 我的工作是要定义一个算法的大纲,而由我的子类定义其中某些步骤的内容。这么一来,我在算法的结构依然维持不变。不过你就不一样了,似乎你必须放弃对算法的控制。
策略: 我不确定话可以这么说······更何况,我并不是使用继承进行算法的实现,我是通过对象组合的方式,让客户选择算法实现。
模板方法: 这我记得。但是我对算法有更多的控制权,而且不会重复代码。会重复使用到的代码,都被我放进了超类中,好让所有子类共享。
策略: 你或许更有效率一点,也的确需要更少的对象。和我所采用的委托模型比起来,你也没那么复杂。但是因为我使用对象组合,所以我更有弹性。利用我,客户就可以在运行时改变他们的算法,而客户所需要做的,只是改用不同的策略对象罢了。拜托,作者选择把我摆在第1章,这不是没有道理的!
模板方法: 好吧,我真替你感到高兴,但是你别忘了,环顾四周,我可是最常被使用的模式。为什么呢?因为我在超类中提供了一个基础的方法,达到代码的复用,并允许子类指定行为。我相信你会看到这一点在创建框架时是非常棒的!
策略: 也许呢·····但是,别忘了依赖! 你的依赖程度比我高。
模板方法: 这话怎么说,我的超类是抽象的。
策略: 但是你必须依赖超类中的方法的实现,因为这是你算法中的一部分。但我就不同了,我不依赖任何人;整个算法我自己搞定!
模板方法要点
- 模板方法模式为我们提供了一种代码复用的重要技巧。
- 模板方法的抽象类可以定义具体方法。抽象方法和钩子。
- 抽象方法由子类实现。
- 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为
final
. - 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
- 工厂方法是模板方法的一种特殊版本。
迭代器与组合模式
迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露其内部的表示。
把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。
组合模式: 允许你将对象组合成树形结构来表现”整体/部分“层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。
使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。
空迭代器: 返回一个迭代器,而这个迭代器的 hasNext()
永远返回 fasle
import java.util.Iterator
// 这是你锁看过最懒的迭代器,什么事情都不做
public class NullIterator implements Iterator {
// 当 next() 被调用时,返回 null
public Object next() {
return null;
}
// 最重要的,当 hasNext() 被调用时,永远返回 false
public boolean hasNext() {
return false;
}
// 空迭代器当然不支持 remove
public void remove() {
throw new UnsupportedOperationException();
}
}
状态模式
状态模式 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
这个模式将状态封装成为独立的类,并将动作委托到代表当前状态的对象,我们知道行为会随着内部状态而改变。
状态: 封装基于状态的行为,并将行为委托到当前状态
策略: 将可以互换的行为封装起来,然后使用委托的方法,决定使用哪一个行为
模板方法: 由子类决定如何实现算法中的某些步骤
代理模式
你的客户对象所做的就像是在远程方法调用,但其实只是调用本地堆中的”代理“对象上的方法,再由代理处理所有网络上远程对象的一些方法。
代理模式 为另一个对象提供一个替身或占位符以控制对这个对象的访问。
使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
RealSubject
是真正做事的对象,它是被 proxy
代理和控制访问的对象。
Proxy
持有 RealSubject
的引用。在某些例子中,Proxy
还会负责 RealSubject
对
象的创建与销毁。客户和 RealSubject
的交互都必須通过 Proxy
。因为 Proxy
和
RealSubject
实现相同的接口(Subject
),所以任何用到 RealSubject
的地方,都可
以用 Proxy
取代。Proxy
也控制了对 RealSubject
的访问,在某些情况下,我们可能
需要这样的控制。这些情况包括 RealSubject
是远程的对象、RealSubject
创建开销
大,或 RealSubject
需要被保护。
装饰者 包装另一个对象,并提供额外的行为。
外观 包装许多对象以简化它们的接口
代理 包装另一个对象,并控制对它的访问
适配器 包装另一个对象,并提供不同的接口
代理模式 为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
装饰者模式 为对象加上行为,而代理则是控制访问
复合模式
模式通常被一起使用,并被组合在同一个设计解决方案中。
复合模式在一个解决方案中结合两个或多个模式,以解决一般或重复发生的问题。
-
MVC 是复合模式,结合了观察者模式、策略模式和组合模式。
-
模型使用观察者模式,以便观察者更新,同时保持两者之间解耦。
-
控制器是视图的策略,视图可以使用不同的控制器实现,得到不同的行为。
-
视图使用组合模式实现用户界面,用户界面通常组合了嵌套的组件,像面板、框架和按钮。
-
适配器模式用来将新的模型适配成已有的视图和控制器。
与设计模式相处
创建型
创建型模式涉及到将对象实例化,这类模式都提供一个方法,将客户从所需要实例化的对象中解耦。
单件、工厂方法、抽象工厂
- 单件:确保有且只有一个对象城创建
- 工厂方法:由子类决定要创建的具体类是哪一个
- 抽象工厂:允许客户创建对象的家族,而无需指定他们的具体类
行为型
只要是行为型模式,都涉及到类和对象如何交互及分配职责。行为型的目的是对象之间的沟通与互连。
模板方法、状态、迭代器、命令、观察者、策略
- 模板方法:由子类决定如何实现一个算法中的步骤
- 状态:封装了基于状态的行为,并使用委托在行为之间切换
- 迭代器:在对象的集合中游走,而不暴露集合的实现
- 命令:封装请求成为对象
- 观察者:让对象能够在状态改变时被通知
- 策略:封装可以互换的行为,并使用委托来决定要使用哪一个
结构型
结构型模式可以让你把类或对象组合到更大的结构中。结构型模式的焦点在于如何动态地组合对象以获取功能(装饰者)。
装饰者、适配器、组合、外观、代理
- 装饰者:包装一个对象,以提供新的行为
- 适配器:封装对象,并提供不同的接口
- 组合:客户用一致的方式处理对象集合和单个对象
- 外观:简化了一群类的接口
- 代理:包装对象,以控制对此对象的访问
反模式
- 开发团队致力于采用他们所熟悉的技术
- 开发团队并不熟悉其他技术
- 采用不熟悉的技术被认为风险比较高
- 使用熟悉的技术做开发,比较容易规划和预估
原本的解决方案:反正就使用熟悉的技术好了。将熟悉的技术强迫性的用在许多问题上,甚至在明显不适当的地方也照用。
重构的解决方案:开发人员通过教育、培训和读书会,可以学会新的解决方案。
例子:当采用开放源码的替代品时,Web
公司依然持续使用并维护他们内部自行开发的缓存系统。
参考文献:
《 HeadFirst设计模式 》