行为模式设计到算法和对象间职责的分配,不仅描述对象或类的模式,还描述它们这间的通信模式。这些模式刻画了在运行时难以跟踪的复杂控制流。它们将你的注意从控制流转移到对象间的联系方式上来。 行为类模式使用继承机制在类间分派行为。 Template Method 逐步地定义算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。Interpreter将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的操作。 行为对象模式使用对象复合而不是继承。 一些行为对象描述了一组对等的对象怎么相互协作以完成其中任一个对象都无法单独完成的任务。因此对等对象之间要了解对方,但那会增加它们的耦合度。Mediator在对等对象间引入一个mediator对象以避免出现这种情况,提供了所需的间接性。 Chain of Responsibility 提供更松的耦合,通过一条候选对象链隐式的向一个对象发送请求;Observer 模式定义并保持对象间的依赖关系,典型的例子是MVC;Stategy模式将算法封装在对象中,可以方便地指定和改变一个对象所使用的算法;Command模式将请求封装在对象中,这样它就可作为参数来传递,或者其他方式使用;State模式封装了一个对象的状态,使得当这个对象的状态改变时,该对象可以改变它的行为;Vistor封装分布于多个类这间的行为;Iterator抽象了访问和遍历一个集合中的对象的方式。
传递请求 —— Chain of Responsibility
使多个对象都有机会处理请求,从而避免请求的发送者和接收者这间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
适用性
以下情况适用于Responsibility链:
- 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
- 可处理一个请求的独享集合应被动态指定
结构
参与者
- Handler: 定义一个处理请求的接口;实现后继(可选)
- ConcreteHandler: 如果可以处理请求,就处理;否则将该请求转发给它的后继者
- Client: 向链上的具体处理者对象提交请求
效果
- 降低耦合度。职责链可简化对象相互连接,它们仅需保持一个指向后继者的引用,而不需保持它所有的候选者的引用
- 增强了给对象指派职责的灵活性。可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。
- 不保证被接受。 既然一个请求没有明确的接收者,那么就不能保证它一定会被处理。
实现
下面是责任链模式中要考虑的实现问题:
- 实现后继者链:
- 定义新的链接(通常在Handler中定义,但也可由ConcreteHandlers来定义)
- 使用已有的链接。当已有的链接能够支持所需要的链时,完全可以使用这些链
- 连接后继者。如果没有已有的引用可用,那么我们必须自己引入他们。这种情况下Handler不仅定义该请求的接口,通常也维护后继链接
- 表示请求。有不同的方式可以表示请求:硬编码、以一个请求码为参数的函数、封装的对象,需要分析具体需求。
代码示例
HelpHandler
类定义了处理帮助请求的接口,维护一个帮助主题,并保持对帮助处理对象链中它的后继者的引用:
public class HelpHandler {
public static int NO_HELP_TOPIC = -1;
private HelpHandler successor;
private int topic;
public HelpHandler(HelpHandler successor, int topic) {
this.successor = successor;
this.topic = topic;
}
public boolean hasHelp() {
return topic != NO_HELP_TOPIC;
}
public void handleHelp() {
System.out.println("HelpHandler help");
if (successor != null) {
successor.handleHelp();
}
}
}
所有的窗口组件都是Widget类的子类。Widget是HelpHandler子类,因为所有的用户界面都可有相关帮助:
public class Widget extends HelpHandler {
public Widget(HelpHandler successor, int topic) {
super(successor, topic);
}
}
Button是Widget类的子类,Button构造函数有两个参数:对它的窗口组件的引用和自身的帮助主题:
public class Button extends Widget {
public Button(HelpHandler successor, int topic) {
super(successor, topic);
}
@Override
public void handleHelp() {
if (hasHelp()) {
super.handleHelp();
// handle button help
} else {
super.handleHelp();
}
}
}
Dialog实现了一个类似的策略,只不过它的后继者不是一个窗口组件而是任意的帮助请求处理对象:
public class Dialog extends Widget {
public Dialog(HelpHandler successor, int topic) {
super(successor, topic);
}
@Override
public void handleHelp() {
System.out.println("Dialog help");
if (hasHelp()) {
// handle help
} else {
super.handleHelp();
}
}
}
链的末端是 Application 类的一个实例,当帮助请求传递到这一层时,该应用可以提供一般性信息:
public class Application extends HelpHandler {
public Application(int topic) {
this(null, 1);
}
public Application(HelpHandler successor, int topic) {
super(successor, topic);
}
@Override
public void handleHelp() {
// show a list of help
}
}
我们可用如下方式使用责任链模式:
private static int PRINT_TOPIC = 1;
private static int PAPER_ORIENTATION_TOPIC = 2;
private static int APPLICATION_TOPIC = 3;
public static void main(String [] args) {
Application application = new Application(APPLICATION_TOPIC);
Dialog dialog = new Dialog(application, PRINT_TOPIC);
Button button = new Button(dialog, PAPER_ORIENTATION_TOPIC);
button.handleHelp();
}
相关模式
责任链模式常与Composite一起使用。这种情况下,一个构件的父构件可以作为它的后继
Command
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消操作
适用性
以下情况使用Command模式:
- 抽象出待执行的动作以参数化某对象。
- 在不同时刻指定、排列和执行请求。
- 支持取消操作。
- 支持修改日志。
- 用构建的原语操作上的高层操作构造一个系统
结构
参与者
- Command: 声明执行操作的接口
- ConcreteCommand: 将一个接收者对象绑定于一个动作;调用接收者相应的操作,以实现Execute
- Client: 创建一个具体命令对象并设定它的接收者
- Invoker: 要求该命令执行这个请求
- Receiver: 知道如何实施与执行一个请求相关的操作,任何类可能作为一个接收者
协作
- Client创建一个ConcreteCommand对象并指定它的Receiver对象
- 某Invoker对象存储该ConcreteCommand对象
- 该I女OK而通过调用Command对象的execute操作来提交一个请求,若该命令是可撤销的,ConcreteCommand就在执行execute操作之前存储当前状态以用于取消该命令
- ConcreteCommand 对象对调用它的Receiver的一些操作以执行该请求
效果
Command模式_有如下效果:
- Command模式将调用操作的对象与知道如何实现该操作的对象解耦
- Command是头等的对象,它们可像其他的对象一样被操控和扩展
- 可将多个命令装配成一个符合命令。一般来说,复合命令是Composite模式的一个实例
- 新增加Command很容易,无需改变已有的类
实现
实现Command模式时需要考虑如下问题:
- 一个命令对象应达到何种程度。命令对象的能力可大可小,一个极端是它仅能确定一个接收者和执行该请求的动作;另一个极端是它能自己实现所有功能,根本不需要额外的接收者对象。
- 支持取消和重做。为了达到支持取消和重做的功能,ConcreteCommand类需要存储额外的状态信息:
- 接收者对象,它真正执行处理该请求的各种操作
- 如果处理请求的操作会改变接收者对象的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可以将接收者恢复到它先前的状态 若应用支持取消操作,那么需要存储一个已被执行命令的历史列表,该列表的最大长度决定了取消和重做的次数。 有时可能不得不将一个可撤销的命令在它可以被放入历史列表中之前先拷贝下来,这是因为执行原来的请求的命令对象将在撤销后执行其他请求,例如删除操作
- 避免取消操作过程中的错误积累。需要保证处理对象取消过程的原子性
代码
首先定义一个抽象的Command
类
public abstract class Command {
public abstract void execute();
}
OpenCommand
打开一个名字由用户指定的文档:
public class OpenCommand extends Command {
private Application application;
String response;
public OpenCommand(Application application) {
this.application = application;
}
private String askUser() {
return "answer";
}
@Override
public void execute() {
String name = askUser();
// do some thing
}
}
MacroCommand
管理一个子命令序列,提供了增加和删除子命令的操作:
public class MacroCommand extends Command {
private List<Command> cmds;
public void remove(Command c) {
cmds.remove(c);
}
public void add(Command c) {
cmds.add(c);
}
@Override
public void execute() {
for (Command command: cmds) {
command.execute();
}
}
}
相关模式
Memento模式可以用来保持某个状态,命令用这一状态来取消它的效果
Interpreter
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 如果一种特定类型的问题发生的频率足够高,那么值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,通过解释这些句子来解决问题,比如正则表达式。 解释器模式描述了如何为简单的语言定义一个文法,如何为简单语言定义一个文法,如何在语言中表示一个句子,以及如何解释这些句子。解释器模式使用类来表示每一条文法规则。
适用性
当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
- 该文法简单对于复杂的文法,文法的类层次变得庞大而无法管理。
- **解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式,比如状态机。
结构
()参与者
- AbstractExpression(抽象表达式): 声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享
- TerminalExpression(终结符表达式): 实现与文法中的终结符相关联的解释操作;一个句子中的每个终结符需要该类的一个实例
- NonterminalExpression(非终结符表达式)
- Context: 包含解释器之外的一些全局信息
- Client: 构建表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语法树由NonterminalExpression和TerminalExpression的实例装配而成
协作
- Client构建一个句子,由NonterminalExpression和TerminalExpression实例构建的一个语法树,然后初始化上下文并调用解释操作
- 每一非终结符表达式节点定义相应子表达式的解释操作,而各终结符表达式的解释操作构成递归的基础
- 每一节点的解释操作用上下文阿里存储和访问解释器的状态
效果
优点:
- 易于改变和扩展文法。
- 易于实现文法。
- 增加了新的解释表达式的方式。
缺点: 复杂的文法难以维护。当包含许多规则的文法时,为每一条规则定义一个类,将会使项目难以管理和维护。可以用其他模式来缓解这一问题,或者其他的技术,如语法分析程序或编译器生成器。
实现
- 创建抽象语法树。解释器模式不小腹语法分析,抽象语法树可用一个表驱动的语法分析程序来生成,也可以用手写(递归下降法)语法分析程序创建,或直接由Client提供
- 定义解释操作。如果经常要创建一种新的解释器,那么Visitor模式将解释放入一个独立的访问者对象更好一些。比如类型检测、优化、代码生成等等。恰当的做法是使用一个访问者以避免在每一个类都定义这些操作
- 与Flyweight模式共享终结符。
迭代器
提供一种顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示的方法。
适用性
迭代器模式可用来:
- 访问一个聚合对象的内容而无需暴露它的内部表示
- 支持对聚合对象的多种遍历
- 为遍历不同的聚合结构提供一个统一的接口
结构
参与者
- Iterator: 迭代器定义访问和遍历元素的接口
- ConcreteIterator: 具体迭代器实现接口;对该聚合遍历时跟踪当前位置
- Aggregate: 聚合定义创建相应迭代器对象的接口
- ConcreteAggregate: 具体聚合实现创建相应迭代器接口,该操作返回一个适当的ConcretIterator实例
效果
迭代器模式有三个重要作用:
- 它支持以不同的方式遍历一个集合。
- 迭代器简化了聚合的接口。
- 在同一个聚合上可以有多个遍历。每个迭代器保持它自己的遍历状态,可以同时进行多个遍历
实现
迭代器的实现上有许多变化和选择:
- 谁控制迭代。迭代器根据是客户控制还是迭代器控制迭代可以分为:外部迭代器和内部迭代器。使用外部迭代器的客户必须主动推进遍历的步伐,显式地向迭代器请求下一个元素;而内部迭代器只需要向其提交一个待执行的操作,迭代器会对聚合中的每一个元素实施该操作
- 谁定义遍历算法。有两种定义遍历算法的地方:聚合本身和迭代器。迭代器负责遍历算法,那么将易于在相同的聚合上使用不同的迭代算法,同时也易于在不同的聚合上重用相同的算法。但是遍历算法可能需要访问私有变量,破坏聚合的封装性
- 迭代器健壮程度如何。一个健壮的迭代器要保证插入和删除操不会干扰遍历,且不需拷贝该聚合。大多数方法需要向这个聚合注册该迭代器,当插入或删除,该聚合 要么调整迭代器的内部状态,要么在内部的维护额外的信息以保证正确的遍历
- 迭代器可有特权访问。迭代器可被看为创建它的聚合的一个扩展,同聚合紧密耦合。
- 复合对象的迭代推荐使用内部迭代器。在Composite模式中的那些递归聚合结构上,外部迭代器难以实现。因为在该结构中不同对象处于嵌套聚合的多个不同层次,需要跟踪当前的对象必须存储一条纵贯该Composite的路径。
- 空迭代器。空迭代器是一个退化的迭代器,有助于处理边界条件。
代码示例
- 列表和迭代器接口
public class List<T> {
private ArrayList<T> list;
private int size;
List(int size) {
list = new ArrayList<>();
this.size = size;
}
int count() {
return this.size;
}
int size() {
return this.list.size();
}
T get(int index) {
return list.get(index);
}
void add(T item) {
if (list.size() >= this.size) { return ;}
list.add(item);
}
// ...
}
迭代器接口:
public interface Iterator<T> {
void first();
void next();
boolean isDone();
T currentItem();
}
- 迭代器子类实现:
public class ListIterator<T> implements Iterator<T> {
private List<T> list;
private int current;
ListIterator(List<T> aList) {
this.list = aList;
current = 0;
}
@Override
public void first() {
current = 0;
}
@Override
public void next() {
current ++;
}
@Override
public boolean isDone() {
return current >= list.size();
}
@Override
public T currentItem() {
if (isDone()) {
return null;
}
return list.get(current);
}
}
- 使用迭代器:
List<String> names = new List<>(4);
names.add("First name");
names.add("Last name");
Iterator<String> iterator = new ListIterator<>(names);
for (iterator.first(); !iterator.isDone(); iterator.next()) {
System.out.println(iterator.currentItem());
}
- 避免限定于一种特定的列表实现,我们可以在
List
类中增加如下方法,这样继承List
的类可以返回合适的迭代器:
Iterator<T> createIterator() {
return new ListIterator<T>(this);
}
- 使用内部的ListIterator,其方式是通过迭代器控制迭代,并对列表中的每一个元素施行同一个操作: 首先定义一个函数式接口:
@FunctionalInterface
public interface ProcessInterface<T> {
public void doWork(T t);
}
定义一个迭代器:
public class ListTraverser<T> {
private ListIterator<T> iterator;
ListTraverser(List<T> aList) {
this.iterator = new ListIterator<>(aList);
}
public void processItem(ProcessInterface<T> process) {
for (iterator.first();!iterator.isDone();iterator.next()) {
process.doWork(iterator.currentItem());
}
}
}
使用方式如下:
ListTraverser<String> traverser = new ListTraverser<>(names);
traverser.processItem((item)-> System.out.println("name:" + item));
相关模式
- Composite:迭代器长被应用到对象复合这样的递归结构上
- Factory Method:多态迭代器依靠Factory Method来实例化适当的迭代器子类
- Memento:常与迭代器模式一起使用。迭代器可使用Memento捕获一个迭代状态,迭代器在其内部存储Memento实例。
解耦复杂通信关系对象——Mediator
用一个中介对象来封装一系列的对象交互,中介者使各个对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 面向对象设计鼓励将行为分布到各个对象中,这种分布可能会导致对象间有许多链接。最坏的情况下,每个对象都知道其他所有对象,使得对象间的相互连接大大增加,导致维护困难。我们可以通过将集体行为封装到一个单独的中介者对象来避免这个问题。中介者对象负责控制和协调一组对象间的交互,使得组中的对象不再相互显示引用,降低耦合性。
适用性
以下情况适用于中介者模式:
- 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解
- 一个对象引用其它很多对象并且直接与这些对象通信,导致难以复用
- 想定制一个分布在多个类中的行为,而又不想生成太多子类。
结构
参与者
Mediator
(中介者): 中介者定义一个接口用于与各同事对象通信ConcreteMediator
(具体中介者): 具体中介者通过协调各同事对象实现协作行为;了解并维护它的各个同事Colleague
(同事类): 每一个同事类都知道它的中介者对象;每一个同事对象在需与其他同事通信时,与它的中介者通信
效果
中介者模式有忙不优点和缺点:
- 减少了子类生成。Mediator将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成Meditator的子类即可
- 它将各Colleague解耦。
- 它简化了对象协议。一对多的关系更易于理解、维护和扩展。
- 它对对象如何协作进行了抽象。将中介作为一个独立的概念并将其封装在一个对象中,使你将注意从对象各自的行为转移到它们的交互上来。
- 它使控制集中化
实现
当一个感兴趣的事发生时,Colleague必须与其Mediator通信。 一种实现方法是使用Observer模式,将Mediator实现为一个Observer,各Colleagues作为Subject;另一种方式是在Mediator中定义一个特殊的通知接口,各Colleagues在通信时直接调用该接口,并将自身作为参数传递给Mediator。
代码示例
抽象类DialogDirector
定义了一个接口:
public abstract class DialogDirector {
public abstract void showDialog();
public abstract void widgetChanged(Widget widget);
protected abstract void createWidgets();
}
Widget
是窗口组件的抽象基类:
public class Widget {
private DialogDirector director;
public Widget(DialogDirector d) {
this.director = d;
}
// 调用中介者的 widgetChanged() 方法,通知中介者某个事件发生了
public void changed() {
this.director.widgetChanged(this);
}
}
ListBox
, EntryField
和 Button
是 Widget
的子类:
public class ListBox extends Widget {
public ListBox(DialogDirector d) {
super(d);
}
}
public class EntryField extends Widget {
public EntryField(DialogDirector d) {
super(d);
}
// other method ...
}
public class Button extends Widget {
public Button(DialogDirector d) {
super(d);
}
void handleMouse () {
//...
// 调用通信接口
changed();
}
}
FontDialogDirectator
类在对话框中的窗口组件间进行中介:
public class FontDialogDirectator extends DialogDirector {
private Button ok;
private Button cancel;
private ListBox fontList;
private EntryField fontName;
@Override
public void showDialog() {
}
@Override
public void widgetChanged(Widget widget) {
if (widget == ok) {
// apply something change
} else if (widget == cancel) {
// apply cancel widget
} else if (widget == fontList) {
} else if (widget == fontName) { }
}
@Override
protected void createWidgets() {
ok = new Button(this);
cancel = new Button(this);
fontList = new ListBox(this);
fontName = new EntryField(this);
}
}
widgetChanged
方法的复杂度随对话框的复杂度增加而增加。大对话框不受欢迎的一个重要原因是中介者的复杂性可能会抵消该模式在其他方面带来的好处。
相关模式
Facade与中介者的不同之处在于它是对一个对象子系统进行抽象,从而提供一个更为方便的接口,它的协议是单向的,而Mediator的协议是多向的。 Coleague可使用Observer模式与Mediator通信。
记录对象内部状态——Memento
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。 一个备忘录是一个对象,它存储了另一个对象在某个瞬间的内部状态,而后者成为备忘录的原发器。当需要设置原发器的检查点时,取消操作机制会向原发器请求一个备忘录。原发器用描述当前状态的信息初始化该备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他对象不可见。
适用性
一下情况使用备忘录:
- 必须保存一个对象在某一个时刻的状态,这样以后需要它时才能恢复到先前的状态。
- 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象在实现细节并破坏对象的封装性。
结构
参与者
Memento
(备忘录): 备忘录存储原发器对象的内部状态;防止原发器以外的其他对象访问备忘录。备忘录有两个接口:窄接口的宽接口。管理者(caretaker)只能看到窄接口,将备忘录传递给其他对象。原发器能够看到宽接口,允许它访问先前状态所需的所有数据。Originator
(原发器): 原发器创建一个备忘录,用心记录当前时刻它的内部状态;使用备忘录恢复内部状态。Caretaker
(负责人): 负责保存好备忘录的内容进行扣件或检查
效果
备忘录模式有以下一些效果:
- 保持封装边界。 使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。
- 它简化了原了器。 在其他保持封装性的设计中,Originator负责保持客户请求的内部状态版本。
- 使用备忘录可能代价很高。 如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。
- 定义窄接口和宽接口。 在一些语言中可能 难以保证只有原发器可访问备忘录的状态
- 维护备忘录的潜在代价。 管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少状态,因此一个本来很小的管理器,可能 会产生大量的存储开销。
实现
当实现备忘录模式时应该考虑以下两个问题:
- 语言支持。 备忘录有两个接口:一个为原发器使用的宽接口;一个为其他对象使用的窄接口。Java中对此支持并不好,只能采用
protected
来支持宽接口。 - 存储增量式改变。如果备忘录的创建及其返回的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。
代码示例
我们使用MoveCommand
命令对象来执行/取消一个图形对象从一个位置到另一个位置的移动变换。命令对象存储它的目标、移动距离和一个ConstraintSolverMemento
的实例,它是一个包含约束解释器状态的备忘录。
/**
* ConstraintSolver 用操作将自身状态存储在外部的一个ConstraintSolverMemento实例中。
*/
public class ConstraintSolver {
private ConstraintSolver(){}
public void solve() {}
public void addConstraint (Graphic s, Graphic e) {}
public void removeConstraint(Graphic s, Graphic e) {}
public ConstraintSolverMemento createMemento() {
return new ConstraintSolverMemento();
}
public void setMemento(ConstraintSolverMemento memento) {}
public static ConstraintSolver getInstance() {
return new ConstraintSolver();
}
}
/**
* execute 在移动图形前先获取一个 ConstraintSolverMemento 备忘录,Unexecute 先将图形移回,再将约束解释器设回原来的状态,并让约束解释器解释这些约束。
*/
public class MoveCommand {
private ConstraintSolverMemento state;
private Point delta;
private Graphic target;
public MoveCommand(Graphic t, Point delta) {
this.delta = delta;
this.target = t;
}
void execute () {
ConstraintSolver solver = ConstraintSolver.getInstance();
// 存储当前的状态
state = solver.createMemento();
target.move(delta);
solver.solve();
}
void unexecute() {
ConstraintSolver solver = ConstraintSolver.getInstance();
target.move(delta);
solver.setMemento(state);
// 重新建立约束
solver.solve();
}
}
public class ConstraintSolverMemento {
// private constraint solver state
}
相关模式
- Command: 命令可使用备忘录来为可撤消的操作维护状态
- Iterator: 备忘录可用于迭代
Collection<ItemType> aCollection;
IteratioinState state;
state = aCollection.createInitialState();
while (!aCollection.isDone(state)) {
aCollection.currentItem(state).process();
aCollection.next();
}
多个对象依赖于一个对象的状态 —— Observer
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护它们的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可用性。
适用性
以下情况下可以使用观察者模式:
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。
结构
参与者
Subject
: 目标知道它的观察者,可以有任意多个观察者观察同一目标;提供注册和删除观察者对象的接口Observer
: 为那些在目标发生改变时需获得通知的对象定义一个更新接口ConcreteSubject
: 将有关状态存入各ConcreteObserver
对象;当它的状态发生改变时,向它的各个观察者发出通知。ConcreteObserver
: 维护一个指向ConcreteSubject
对象的引用;存储有关状态,这些状态应与目标的状态保持一致;实现Observer
的更新接口以使自身状态与目标的状态保持一致。
效果
Observer模式允许我们独立地改变目标和观察者。以下是观察者模式的一些优缺点:
- 目标和观察者间的抽象耦合。 一个目标仅仅知道它有一系列的观察者,每个都符合抽象的Observer类的简单接口,这样目标和观察者之间的耦合是抽象的和最小的。
- 支持广播通信。 通知被自动广播给所有已向该目标对象登记的有关对象。
- 意外的更新。因为一个观察者并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新。
实现
创建目标到其观察者之间的映射。
观察多个目标。 在某些情况下,一个观察者依赖于多个目标是有意义的。目标对象可以简单地将自己作为update操作的一个参数,让观察者知道应去检查哪一个目标。
谁触发更新。 目标和它的观察者依赖于通知机制来保持一致,但哪一个对象调用notify来触发更新?有两个选择:
- 由目标对象的状态设定操作在改变目标对象的状态后生动调用notify。这种方法的优点是客户不需要记住要在目标对象上调用notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低
- 主客户负责在适当的时候调用notify。这样做的优点是客户可以在一系列的状态改变写成后再上次性触发更新,避免了不必要的中间更新,缺点是给客户增加了负担。
对已删除的目标悬挂引用。 一种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。
在发出通知前确保目标的状态自身是一致的。 当
Subject
的子类调用继承的该项操作时,很容易无意中违反这条自身一致的准则:void operation(int newValue) { super.operation(newValue); super.update(); instVar += newValue; }
我们可以用抽象的
Subject
类中的模板方法来发送通知以避免这种错误。避免特定于观察者的更新协议——推/拉模型。
- 推模型:目标向观察者发送关于改变的详细信息,不管它们是否需要,其假定目标知道一些观察者需要的信息,该模型使得观察者相对难以复用,因为目标对观察者的假定并不一定正确。
- 拉模型:目标除最小通知外什么也不管,由观察者显式地向目标询问,拉模型可能效率较差,因为观察者对象需在没有目标对象帮助不确定什么改变了。
显式地指定感兴趣的改变。 可以扩展目标的注册接口,让各观察者注册为仅对特定的事感兴趣,以提高效率。
封装复杂的更新语义。 当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。其目的是尽量减少观察者反映其目标状态变化所需的工作量。
结合目标类和观察者类。 将目标类和观察者类结合到一个类中,这就允许我们定义一个既是目标又是观察者的对象,而不需要多重继承。
代码示例
定义一个Observer接口和一个Subject类:
public interface Observer {
void update(Subject subject);
}
public class Subject {
private List<Observer> os;
public Subject() {
os = new ArrayList<>();
}
public void attach(Observer observer) {
os.add(observer);
}
public void remove(Observer observer) {
os.remove(observer);
}
public void notifyObservers () {
for (Observer o: os) {
o.update(this);
}
}
}
ClockTimer
是一个用于存储和维护一天时间的具体目标:
public class ClockTimer extends Subject {
private LocalDateTime time = LocalDateTime.now();
public int getHour() {
return time.getHour();
}
public int getMinute() {
return time.getMinute();
}
public int getSecond () {
return time.getSecond();
}
public void tick () {
time = LocalDateTime.now();
notifyObservers();
}
}
我们可以定义两个观察者:
public class DigitalClock implements Observer {
private ClockTimer timer ;
public DigitalClock(ClockTimer timer) {
this.timer = timer;
timer.attach(this);
}
@Override
public void update (Subject subject) {
if (this.timer == subject) {
// do something
System.out.println("DigitalClock update:" + timer.getHour() + ":" + timer.getMinute() + ":" + timer.getSecond());
}
}
}
public class AnalogClock implements Observer {
private ClockTimer timer ;
public AnalogClock(ClockTimer timer) {
this.timer = timer;
timer.attach(this);
}
@Override
public void update (Subject subject) {
if (this.timer == subject) {
// do something
System.out.println("AnalogClock update:" + timer.getHour() + ":" + timer.getMinute() + ":" + timer.getSecond());
}
}
}
可以如下调用:
public static void main(String [] args) throws InterruptedException {
ClockTimer timer = new ClockTimer();
AnalogClock analogClock = new AnalogClock(timer);
DigitalClock digitalClock = new DigitalClock(timer);
while (true) {
Thread.sleep(1000);
timer.tick();
}
}
相关模式
- Mediator: 通过封装复杂的更新语义,充当目标和观察者之间的中介者
- Singleton: 中介者可使用Singleton模式来保证它是唯一的并且是可全局访问的。
不同状态间解耦 —— State
允许一个对象在其内部状态改变时,改变它的行为。对象看起来修改了它的类。
适用性
以下两种情况可以使用State模式:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。State模式将每一个条件分支放入一个独立的类中,这使得你可以根据自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
结构
参与者
Context
(环境): 定义客户感兴趣的接口;维护一个ConcreateState
子类,这个实例定义当前状态。State
(状态): 定义一个接口以封装与Context
的特定状态相关的行为。ConcreteState
(具体子类): 每一个子类实现一个与Context
的一个状态相关的行为
协作
Context
将与状态相关的请求委托给当前的ConcreteState
对象处理Context
可将自身一个参数传递给处理该请求的状态对象,这样状态对象在必要时可访问Context
Context
是客户使用的主要接口。客户可用状态对象来配置一个Context
,一旦一个Context
配置完毕,它的客户不再地需要直接与状态对象打交道
效果
- 它将与特定状态的相关行为局部化,并且将一同状态的行为分割开来。 State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中,所以通过定义新的子类可以很容易的新的状态和转换。State避免了巨大的条件语句所带来的问题,但该模式瘵不同的行为分布在多个State子类中,增加了子类的数目,相对于单个类来说不够紧凑。
- 它使得状态转换显式化。 而且State对象可保证
Context
不会发生内部状态不一致的情况,因为状态转换是原子的。 - State对象可以被共享。如果State对象没有实例变量,即它们表示的状态完全以它们的类型来编码。那么各Context对象可以共享一个State对象。
实现
- 谁定义状态转换。State模式不指定哪一个参与者转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然而若让State子类自身指定它们的后继状态以及何时进行转换,通常更灵活更合适。
- 基于表的另一种方法。 我们可以使用表将输入映射状态转换。对每一个状态,一张表将每一个可能的输入映射到一个后继状态。这种方法将条件代码映射为一介查找表。但是查找表有一些缺点:
- 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。
- 通常难以加入伴随状态转换的一些动作。 State模式对与状态相关的行为进行建模,而表驱动的方法着重于定义状态转换。
- 创建和销毁State对象. 如果对象存储大量的信息时,仅当需要State时才创建它们并随后销毁它们比较好;如果状态改变频繁,提前创建它们并始终不销毁它们比较好。
代码示例
我们定义类TCPConnection
,它提供了一个传送数据的接口并修理改变状态的请求:
public class TCPConnection {
private TCPState state;
public TCPConnection() {
}
public void passiveOpen(TCPConnection tcpConnection) {
state.passiveOpen(tcpConnection);
}
public void close(TCPConnection tcpConnection) {
state.close(tcpConnection);
}
public void send(TCPConnection tcpConnection) {
state.send(tcpConnection);
}
protected void changeState(TCPState state) {
this.state = state;
}
}
TCPConnection
在state
中保持一个TCPState
类的实例,TCPState
定义了一个状态和接口,TCPEstablished
, TCPListen
和TCPClosed
定义了几个状态:
public class TCPEstablished extends TCPState {
private static TCPState established = new TCPEstablished();
public static TCPState getInstance() {
return established;
}
@Override
public void transmit(TCPConnection tcpConnection) {
}
@Override
public void passiveOpen(TCPConnection tcpConnection) {
}
@Override
public void close(TCPConnection tcpConnection) {
changeState(tcpConnection, TCPClosed.getInstance());
}
@Override
public void send(TCPConnection tcpConnection) {
}
}
public class TCPListen extends TCPState {
private static TCPState listen = new TCPEstablished();
public static TCPState getInstance() {
return listen;
}
@Override
public void transmit(TCPConnection tcpConnection) {
}
@Override
public void passiveOpen(TCPConnection tcpConnection) {
}
@Override
public void close(TCPConnection tcpConnection) {
}
@Override
public void send(TCPConnection tcpConnection) {
changeState(tcpConnection, TCPEstablished.getInstance());
}
}
public class TCPClosed extends TCPState {
private static TCPState closed = new TCPClosed();
public static TCPState getInstance() {
return closed;
}
@Override
public void transmit(TCPConnection tcpConnection) {
}
@Override
public void passiveOpen(TCPConnection tcpConnection) {
changeState(tcpConnection, TCPListen.getInstance());
}
@Override
public void close(TCPConnection tcpConnection) {
}
@Override
public void send(TCPConnection tcpConnection) {
}
}
在完成相关工作后,这些操作调用changeState
来改变TCPConnection
的状态。
相关模式
Flyweight模式解释了何时以及怎样共享状态对象 状态对象通常是Singleton
一系列算法中取一个 —— Strategy
定义一系列的算法,把它们封装起来,并且使它们可相互替换,使得算法可独立于它的客户而变化。
适用性
Strategy模式适用于以下情况:
- 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个来配置一个类的方法
- 需要使用一个算法的不同变体。当这些变体实现为一个算法的类层次时,可使用策略模式
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
结构
参与者
Strategy
(策略): 定义所有支持的算法的公共接口。Context
使用这个接口来调用某ConcreteStrategy
定义的算法ConcreteStrategy
(具体策略): 以Strategy
接口实现某具体算法Context
(上下文): 用一个ConcreteStrategy
对象来配置;维护一个队Strategy
对象的引用;可定义一个接口来让Strategy
访问它的数据
效果
Strategy模式有下面的一些优点和缺点:
- 相关算法系列。
- 一个替换继承的方法。 继承提供了另一种支持多种算法或行为的方法,可以直接生成一个Context类的子类,从而给它以不同的行为。但这样会将行为硬编码到Context中,而将算法的实现与Context混合起来,使Context难以理解、难以维护和扩展。
- 消除了一些条件语句
- 客户有较大的选择空间,但是必须了解不同的Strategy
- 增加了对象数目
实现
- 定义Strategy和Context接口:
- Context将数据放在参数中传递给Strategy,可以将Strategy和Context解耦,但是参数中可能会有些不需要的数据
- 将Context作为参数传递给Strategy,Strategy再显示地向Context请求数据。这时需要Context定义一个更精确的接口,这也将Strategy和Context更紧密地耦合在一起。
- 将Strategy作为泛型参数。 这需要满足两个条件:在编译时选择Strategy;不需要在运行时改变。
- Strategy对象可选。 如果Strategy并不是必须的,可在Context中定义缺省实现。
代码示例
Composition
类定义了一个上下文环境:
public class Composition {
private Compositor compositor;
public Composition(Compositor compositor) {
this.compositor = compositor;
}
public void repair() {
// do some action
compositor.Compose();
// do other thing
}
public void setCompositor(Compositor compositor) {
this.compositor = compositor;
}
}
算法策略如下:
/**
* Compositor 接口需要经过仔细设计,以支持子类可能实现的各种算法,不希望在生成一个新的子类就不得不修改这个接口
*/
public interface Compositor {
int Compose();
}
public class SimpleCompositor implements Compositor {
@Override
public int Compose() {
return 0;
}
}
public class TexCompositor implements Compositor {
@Override
public int Compose() {
return 1;
}
}
相关模式
Flyweight: Strategy对象常常是很好的轻量级对象
Template Method
定义一个算法的骨架,将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构就可重定义该算法的某些特定步骤。
适用性
模板方法适用于如下情况:
- 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
- 控制子类扩展,模板方法只在特定点调用“hook”操作。
结构
参与者
AbstractClass
(抽象类): 定义抽象的原语操作,具体子类将重定义它们以实现一个算法的各个步骤;实现一个模板方法,定义一个算法的骨架,该模板方法不仅调用原语操作,也调用定义在AbstractClass
或其他对象中的操作。ConcreteClass
(具体类): 实现原语操作以完成算法中与特定子类相关的步骤
ConcreteClass
靠AbstractClass
来实现算法中不变的步骤。
效果
模板方法是一种代码复用的基本技术。它在类库中尤为重要,它提取了类库中的公共行为。 模板方法也导致一种反向控制结构,一个父类调用一个子类的操作,而不是相反。 模板方法调用下列类型的操作:
- 具体的操作(
ConcreteClass
或对客户类的操作) - 具体的
AbstractClass
的操作(通常对子类有用的操作) - 原语操作
- Factory Method
- 钩子操作: 提供缺省操作,子类可以在必要时进行扩展,一个钩子操作在缺省操作通常是一个空操作。
模板方法应该指明那些操作是钩子操作(可以被重定义),哪些是抽象操作(必须重定义)。一般是在父类中调用钩子操作,子类重定义这个钩子操作。
实现
有三个问题值得注意:
- 使用访问控制 一个模板方法调用的原始原语操作可以被定义为保护成员,这保证它们只被模板方法调用。
- 尽量减少原语操作 定义模板方法的一个重要目的是尽量减少一个子类具体实现该算法时必须重定义的那些原语操作的数目。
- 命名约定 可以给应被重定义的那些操作的名字上加一个前缀以识别它们。
代码示例
View
中定义了两个操作,doDisplay
钩子操作实施真正的功能:
public class View {
public void display () {
setFocus();
doDisplay();
resetFocus();
}
void setFocus() {}
/* 定义一个缺省操作 */
void doDisplay() {}
void resetFocus() {}
}
MyView
重新定义了 doDisplay
操作:
public class MyView extends View {
@Override
protected void doDisplay() {
// render the view's contents
}
}
相关模式
- Factory Method 模式常被模板方法调用。
- Strategy: 模板方法使用继承来改变算法的一部分,Strategy使用委托来改变整个算法。
Visitor
表示一个作用于某对象结果中的各元素的操作,使你可以不改变各元素的类的前提下定义作用于这些元素的新操作。 使用Visitor模式,必须定义两个类层次:一个对应于接受操作的元素(Node层次),另一个对应于定义对元素操作的访问者(NodeVisitor层次)。
适用性
下列情况使用Visitor:
- 一个对象结构包含很多类对象,他们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义所有访问者的接口,这三只熊需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
结构
参与者
Vistor
(访问者): 为对象结构中的ConcreteElement
的每一个类声明一个Visit操作。该操作的名字和特征标识了发送Visit请求给该访问者的那个类。这使得访问者可以确定正被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。ConcreteVisitor
(具体访问者): 实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片段乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果Element
(元素): 定义一个accept操作ConcreteElement
(具体元素): 实现accept操作ObjectStructure
(对象结构): 能枚举它的元素;可以提供一个高层的接口,允许访问者访问它的元素;可以是一个复合或是一个集合。
效果
- 访问者模式使得易于增加新的操作
- 访问者集中相关的操作而分离无关的操作 相关行为不是分布定义在该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。
- 增加新的ConcreteElement类很困难 每新增一个ConcreteElement类都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVistor类中实现相应的操作。Element类层次是稳定的,才适合用访问者模式
- 可以访问不具有相同父类的对象
- 累积状态 当访问者访问对象结构中的每一个元素时,它可能会累积状态。
- 破坏封装 访问者方法假定ConcreteElement接口提供足够的功能,常常会迫使你提供访问元素内部状态的公共操作。
实现
谁负责遍历对象结构 我们可以使用一下三个中的任一个遍历对象:对象结构中,访问者中,一个独立的迭代器对象中。 通常由对象结构复杂迭代。一个集合只需对它的元素进行迭代,并对每一个元素调用accept。复合对象通常让accept操作遍历该元素的各子构建并对它们中的每一个递归地调用accept。
代码示例
public abstract class Equipment {
private String name;
public Equipment(String name) {
this.name = name;
}
public abstract void accept(EquipmentVisitor visitor);
public String getName() {
return name;
}
}
public class FloppyDisk extends Equipment {
public FloppyDisk(String name) {
super(name);
}
@Override
public void accept(EquipmentVisitor visitor) {
visitor.visitFloppyDisk(this);
}
public int netPrice() {
return new Random().nextInt();
}
}
public class Chassis extends Equipment {
private List<Equipment> equipments;
public Chassis(String name) {
super(name);
}
public int discountPrice() {
return new Random().nextInt();
}
@Override
public void accept(EquipmentVisitor visitor) {
for (Equipment equipment: equipments) {
equipment.accept(visitor);
}
}
}
public interface EquipmentVisitor {
void visitFloppyDisk(FloppyDisk f);
void visitChassis(Chassis c);
}
public class PricingVisitor implements EquipmentVisitor {
private int total = 0;
@Override
public void visitFloppyDisk(FloppyDisk f) {
total += f.netPrice();
}
@Override
public void visitChassis(Chassis c) {
total += c.discountPrice();
}
}
相关模式
Composite: 访问者可以用对一个由Composite模式定义的对象结构进行操作 Interpreter: 访问者可以用于解释器
总结
封装变化是许多行为模式的主题。当一个程序的某个方面的特征经常发生改变时,这些模式就定义一个封装这个方面的对象。
- Strategy 对象封装一个算法
- State 对象封装一个与状态相关的行为
- Mediator 对象封装对象间的协议
- Iterator 对象封装访问和遍历一个聚集对象的各个构件的方法。
解耦发送者和接收者 解耦是设计中永恒的主题。
- Command 对象提供一个提交请求的简单接口,将发送者和接收者解耦
- Observer 通过一个接口来通知目标中发生的改变,从而将发送者与接收者解耦
- Mediator 让对象通过一个Mediator对象间接的互相引用,从而对它们解耦
- 职责链模式通过沿一个潜在的接收者链传递请求而将发送者与接收者解耦
模式组合 除了少数例外情况,各个行为设计模式之间是相互补充和相互加强的关系。 一个职责链中的类可能包括至少一个Template Method的应用。该模板方法可使用原语操作确定该对象是否应处理该请求并选择应转发的对象。 职责链使用Command模式将请求表示为对象。
ddf
–