图片均来自《Head First 设计模式》和网络,侵删。
策略模式
例子:不同鸭子的行为。
观察者模式
例子:气象站更新布告板。
推拉模型
- 推模型实现前提被观察者对象(主题对象)知道观察者需要什么数据,所以只传递数据;拉模型可能是主题对象不知道观察者需要什么样的数据,所以只能把自身传递过去,观察者根据自身需要到主题对象中拉取数据;
- 推模型可能使观察者对象无法复用,因为推模型传递给观察者的是已经定义好的数据,一旦新的观察者要求不同格式数据,就需要重新定义
update()
方法;而拉模型就不会造成这样的情况,因为拉模型下,update()
方法的参数是主题对象本身,这基本上是主题对象能传递的最大数据集合了,基本上可以适应各种情况的需要。 JDK
中的观察者模式采用的推模型,但是在update(Observable o, Object arg)
方法中定义了两个参数,不仅传递了数据内容,也将被观察者对象(主题对象)本身传递给了观察者。
发布-订阅模式
ChangeManager:中介者模式的实例。如果一个操作涉及到对几个相互依赖的目标进行改动,就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。总结其作用有三:
- 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用,反之亦然。
- 它定义一个特定的更新策略。
- 根据一个目标的请求,它更新所有依赖于这个目标的观察者。
装饰者模式
例子:咖啡添加配料,实际不太好,可以直接使用组合模式,更加灵活透明,装饰者模式更主要的还是动态修改或增加功能,因此最好的案例还是 JDK 中 I/O 相关的实现。
开闭原则。
更加灵活的封装。
工厂模式
例子:披萨制作,简单工厂不是设计模式,只是单独将制作披萨的条件判断函数独立。工厂方法模式是通过继承,主要是创建单个对象,由子类来具体确定对象创建类型。抽象工厂是创建对象簇,需要先实例化对应的工厂,更多使用的是对象的组合。
工厂方法
抽象工厂
单例模式
例子:巧克力工厂,只能有一个锅炉,还需要解决多线程加入原料的问题。
双重校验锁实现单例:
public class SingletonDoubleCheck {
// 类引用
private static volatile SingletonDoubleCheck singletonDoubleCheck;
// 构造函数私有化
private SingletonDoubleCheck() {}
// 双重校验 + 锁实现单例
public static SingletonDoubleCheck getInstance() {
// 第一次校验是否为null
if (singletonDoubleCheck == null) {
// 不为空则加锁
synchronized (SingletonDoubleCheck.class) {
// 第二次校验是否为null
if (singletonDoubleCheck == null) {
singletonDoubleCheck = new SingletonDoubleCheck();
}
}
}
return singletonDoubleCheck;
}
}
每个类加载器都有自己的命名空间。这意味着如果有两个类加载器,它们可能会加载同一个类,但在虚拟机中被视为两个不同的类,因为它们处于不同的命名空间。为了避免单例模式的实例化问题,建议在使用单例模式的同时,明确指定类加载器,并确保在整个程序中只使用这一个类加载器。 单例模式相比全局变量:延迟初始化,减少大量小对象对命名空间的污染。
命令模式
例子:女招待(invoker)接收命令(setCommand),厨师(receiver)完成命令(execute),以及遥控器 undo
命令设置。
空插槽没有预先设置对应的命令:预先将所有插槽均设置一个空命令(空对象),空命令被调用
execute()
将无事发生。
宏命令:支持一组命令,undo
时候需要逆向逐个撤销。
队列和日志管理。
适配器模式
例子:充电器,鸭子和火鸡的行为,新的迭代器适配枚举类(迭代器有 remove 方法而枚举类没有,迭代器接口的 remove 被设计为抛出 UnsupportOperationException 异常)。
对象适配器
不需要引入继承,但是需要全部实现被适配器的方法,代码重复度可能较高。
类适配器
可以根据被适配器的需要选择性地重写适配器的方法,但是使用了继承,相对不灵活。
双向适配器
外观模式
例子:家庭影院。
外观没有“封装”子系统的类,外观只提供简化的接口。所以客户如果觉得有必要,依然可以直接使用子系统的类。这是外观模式一个很好的特征:提供简化的接口的同时,依然将系统完整的功能暴露出来,以供需要的人使用。
最小知识原则。
模板方法模式
例子:冲泡咖啡和冲泡茶。
钩子方法:通常是空的缺省实现,子类自行决定是否要覆盖它。也就是说,算法的必须步骤是抽象方法,而可选的则是钩子。钩子的另一个用法,是让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤作出反应。比方说,名为 justReOrderedList()的钩子方法允许子类在内部列表重新组织后执行某些动作(例如在屏幕上重新显示数据)正如你刚刚看到的,钩子也可以让子类有能力为其抽象类作一些决定。
排序方法中 compareTo
由子类实现,除此之外,java.io 的 InputStream 类有一个 read()
方法,是由子类实现的,而这个方法又会被 read(byte b[],int off,int len)
模板方法使用,Android 的生命周期函数也都是典型例子。
模板方法和策略模式都封装算法,前者用继承,后者组合。
防止子类更改算法可以在模板类定义该方法为 final
。
工厂方法是模板方法的一种特殊情况。因为在工厂方法模式中,工厂接口的创建方法可以被认为是一个模板方法。这个模板方法定义了创建对象的框架,而具体的创建过程则由具体的工厂子类实现。
好莱坞原则:高层告诉低层,别管我们,我们会调用你。
迭代器模式
例子:两家菜单不同的餐厅合并,服务员需要统一接口。
单一职责模式。
组合模式
例子:菜单不同的餐厅合并后要在某个菜单下添加子菜单。
有时候,如果这个组合结构很复杂,或者遍历的代价太高,那么实现组合节点的缓存就很有帮助。比方说,如果你要不断地遍历一个组合,而且它的每一个子节点都需要进行某些计算,那你就应该使用缓存来临时保存结果,省去遍历的开支。
状态模式
例子:糖果机的不同状态,有硬币、无硬币、糖果售罄等。
一般来讲,当状态转换是固定的时候,就适合放在 Context 中:然而,当转换是更动态的时候,通常就会放在状态类中(例如,在 GumballMachine 中,由运行时糖果的数目来决定状态要转换到 NoQuarter 还是 SoldOut)。将状态转换放在状态类中的缺点是:状态类之间产生了依赖。在我们的 GumballMachine 实现中,我们试图通过使用 Context 上的 getter 方法把依赖减到最小,而不是显式硬编码具体状态类。
请注意,在做这个决策的同时,也等于是在为另一件事情做决策:当系统进化时,究竟哪个类是对修改封闭(Context 还是状态类)的。
如果你有一个应用,它有很多状态,但是你决定不将这些状态封装在不同的对象中,那么你就会得到巨大的、整块的条件语句。这会让你的代码不容易维护和理解。通过使用许多对象,你可以让状态变得很干净,在以后理解和维护它们时,就可以省下很多的工夫。
状态类可以被多个 Context 实例共享。
代理模式
例子:糖果机监测状态,网站图像加载代理(未下载好时显示加载中)。
增加“下载中”消息可以算是增加新功能,但是更重要的是将客户端和图像服务解耦,代理对象控制对图像服务的访问,因此更符合代理模式而不是装饰者模式。
一个常用的技巧是提供一个工厂,实例化并返回主题。因为这是在工厂方法内发生的,我们可以用代理包装主题再返回,而客户不知道也不在乎他使用的是代理还是真东西。
防火墙代理、智能引用代理、缓存代理、同步代理、复杂隐藏代理、写入时复制代理。
动态代理类图。
MVC 使用的设计模式。