第3章
行为型模式
本章介绍行为型模式。行为型模式关注对象交互、通信和控制流。大多数行为型模式都基于组合和委托而不是继承。我们将在本章中研究以下行为型模式:
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 观察者模式
- 中介者模式
- 备忘录模式
- 状态模式
- 策略模式
- 模板方法模式
- 空对象模式
- 访问者模式
3.1 责任链模式
计算机软件是用来处理信息的,有多种不同的方式来组织和处理信息。从前文了解到,当我们在讨论面向对象编程时,应该赋予一个类单一的职责,从而使得类容易维护和扩展。
设想一个场景,需要对一批从客户端来的数据进行多种不同的操作,我们会使用多个不同的类负责不同的操作,而不是使用一个类集成所有操作,这样做能让代码松耦合且简洁。
这些类被称为处理器,第一个处理器会接收请求,如果它需要执行操作则会进行一次调用,如果不需要则会将请求传递给第二个处理器。类似地,第二个处理器确认并将请求传递给责任链中的下一个处理器。
1.目的
责任链模式可以让处理器按以下方式处理:如果需要则处理请求,否则将请求传递给下一个处理器。
2.实现
如图3-1所示的类图描述了责任链模式的结构和行为。
图3-1包括以下类:
- Client(客户端):客户端是使用责任链模式的应用程序的主要结构。它的职责是实例化一个处理器的链,然后在第一个对象中调用handleRequest方法。
- Handler(处理器):这是一个抽象类,提供给所有实际处理器进行继承。它拥有一个handleRequest方法,用来接收需要处理的请求。
- ConcreteHandler(具体处理器):这是一个实现了handleRequest方法的具体类。每一个具体处理器都维持一个引用,指向链中下一个具体处理器,需要检查它自身是否能处理这个请求,不能就将请求传递给链中的下一个具体处理器。
每一个处理器需要实现一个方法,该方法被客户端所使用,并能够设置下一个处理器,当它无法处理请求时,将请求传给下一个处理器。这个方法可以加入到Handle基类当中。
在每一个ConcreteHandler类中,我们实现下列代码,检查它是否能处理请求,不能则会传递请求:
客户端负责在调用链头之前建立处理器链。这次调用会被传递,直到发现了能正确处理这个请求的处理器。
以汽车服务程序为例。每有一个损坏的汽车进入,首先由机修工进行检查,如果在机修工的专业范围内,机修工会对汽车进行维修。如果机修工不会维修,他们会把损坏的汽车传递给电工。如果电工也无法修理坏车,他们会将车交给下一个专家。
图3-2展示了如何运行。
3.适用情况和示例
以下是责任链模式的适用情况和示例:
- 事件处理器:举个例子,大部分图形用户界面框架使用责任链模式来处理事件。例如,一个窗口包含了一个面板,面板上有一些按钮,我们需要写按钮的事件处理器。如果我们决定跳过它并传递它,责任链中的下一个处理器面板将会处理这个请求。如果面板也跳过了它,它将会被传递到窗口。
- 日志处理器:与事件处理器类似,每一个处理器都要么记录一个基于其状态的特殊请求,要么将请求传送给下一个处理器。
- servlet:在Java中,javax.servlet.Filter(http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html )被用来过滤请求或者响应。doFilter方法把过滤器链作为一个参数接收,它能够传递请求。
3.2 命令模式
在面向对象编程当中,一个很重要的事情是设计能够使得代码松耦合。举个例子,我们需要开发一个复杂的程序,用来绘制诸如点、线、线段、圆、矩形等许多图形。
为了让代码能够实现所有种类的形状,我们需要实现很多操作来处理菜单操作。为了让程序可维护,我们需要创建一个统一的方法来定义所有的命令,这样做便能够将所有实现细节隐藏在程序之中(这个程序实际上就是客户端)。
1.目的
命令模式能够做到:
- 提供一个统一的方法来封装命令和其所需要的参数来执行一个动作。
- 允许处理命令,例如将命令存储在队列中。
2.实现
如图3-3所示的类图展示了命令模式的实现。
前面的类图中包括以下元素:
- Command(命令类):这是表示命令封装的抽象类。它声明了执行的抽象方法,该方法应该由所有具体命令实现。
- ConcreteCommand(具体命令类):这是命令类的实际实现。它必须执行命令并处理与每个具体命令相关的参数。它将命令委托给接收者。
- Receiver(接收者):这是负责执行与命令关联的操作的类。
- Invoker(调用者):这是触发命令的类。通常是外部事件,例如用户操作。
- Client(客户端):这是实例化具体命令对象及其接收者的实际类。
最初,我们的想法是在一个大的if-else块中处理所有可能出现的命令:
之后,我们决定为绘图程序应用命令模式。首先创建一个命令接口:
下一步是将所有对象(如菜单项和按钮)定义为类,实现命令接口和execute()
方法:
在重复上一个操作并为每个可能的操作创建一个类之后,用以下方法替换前面实现的if-else代码块:
从代码中看到调用者(触发performAction方法的客户端)和接收者(实现命令接口的类)是分离的。我们可以轻松扩展代码而无须更改它。
3.适用情况和示例
命令模式的适用性和示例如下:
- Undo/Redo operation(撤销/重做操作):命令模式允许我们将命令对象存储在队列中。这样就可以实现撤销和重做操作。
- Composite command(组合命令):复杂命令可以使用组合模式由简单命令组成,并按顺序运行。通过这种方式,我们可以以面向对象的方式构建宏。
- The asynchronous method invocation(异步方法调用):命令模式用于多线程应用程序。命令对象可以在后台以单独的线程执行。
java.lang.Runnable是一个命令接口。
在以下代码中,Runnable接口充当命令接口,由RunnableThread实现:
客户端调用命令以启动新线程:
3.3 解释器模式
计算机用来解释句子或表达式。当需要编写一系列处理这种需求的代码时,首先要知道句子或表达式的结构,要有一个表达式或句子的内部表示。多数情况下,最合适的结构是基于组合模式的组合结构。我们将在第4章中进一步讨论组合模式。现在可以将组合表示视为将相似性质的对象集合在一起。
1.目的
解释器模式定义语法的表示以及该语法的对应解释。
2.实现
解释器模式使用组合模式来定义对象结构的内部表示。除此之外,它还添加了实现来解释表达式并将其转换为内部结构。因此,解释器模式属于行为型模式。解释器模式的类图如图3-4所示。
解释器模式由以下类组成:
- Context(环境):Context用于封装解释器的全局信息,所有具体的解释器均需访问Context。
- AbstractExpression(抽象表达式):一个抽象类或接口,声明执行的解释方法,由所有具体的解释器实现。
- TerminalExpression(终结符表达式):一种解释器类,实现与语法的终结符相关的操作。终结符表达式必须始终被实现和实例化,因为它表示表达式的结尾。
- NonTerminalExpression(非终结符表达式):这是实现语法的不同规则或符号的类。对于每一个语法都应该创建一个类。
在实践当中,解释器模式用来解释正则表达式。为这种场景实现解释器模式是一个很好的练习,这里我们选择一个简单的语法作为例子。我们将应用解释器模式来解析带有一个变量的简单函数f (x)。
为了简单,我们选择逆波兰表示法,这是一种在运算符末尾添加操作数的表示法。1 + 2变为1 2 +, (1 + 2)* 3变为1 2 + 3 *。优点是不再需要括号,因此它简化了任务。
下面的代码为表达式创建了接口:
实现具体类需要下列元素:
- Number(数字)类:它解释所有数字。
- Operatorc((操作符)+、-、*、/)类:在下面的例子中,将使用加号(+)和减号(-)。
现在到了复杂的部分,我们需要实现操作符类,操作符类是组合表达式,由两个表达式组合而成:
类似地,接下来实现一个减号类:
可以看到,我们已经创建了一个类,该类允许我们构建一棵这样的语法树:操作是节点,变量和数字是叶子。结构非常复杂,可用于解释表达式。
现在写一段代码,通过建立好的类来实现语法树:
3.适用情况和示例
解释器模式适用于表达式被解释并转换为其内部表示的情况。内部表示是基于组合模式的,因此解释器模式不适用于复杂的语法。
Java在java.util.Parser中实现了解释器模式,它用于解释正则表达式。在解释正则表达式时返回匹配器对象。匹配器使用基于正则表达式的模式类创建的内部结构:
3.4 迭代器模式
迭代器模式可能是Java中最广为人知的模式之一。Java程序员在使用集合(collection)
时,并不需要关注其类型是数组、列表、集合(set)还是其他,有些人并不知道这些集合包其实是使用了迭代器模式来实现的。
我们可以以相同的方式处理集合,无论它是列表还是数组,这是因为它提供了一种迭代其元素而不暴露其内部结构的机制。更重要的是,不同类型的集合能够使用相同的统一的机制。这种机制被称为迭代器模式。
1.目的
迭代器模式提供了一种顺序遍历聚合对象元素而不暴露其内部实现的方法。
2.实现
迭代器模式基于两个抽象类或接口,可以通过成对的具体类来实现。类图如图3-5所示。
迭代器模式使用了以下类:
- Aggregate(抽象容器):应该由所有类实现的抽象类,并且可以由迭代器遍历。这对应于java.util.Collection接口。
- Iterator(抽象迭代器):抽象迭代器是迭代器抽象类,它定义遍历容器对象的操作以及返回对象的操作。
- ConcreteAggregate(具体容器):具体容器可以实现内部不同的结构,但会暴露处理遍历容器的具体迭代器。
- ConcreteIterator(具体迭代器):这是处理特定具体容器类的具体迭代器。实际上,对于每个具体容器,必须实现一个具体迭代器。
每一个Java程序员在日常工作中都会使用迭代器。让我们看看如何实现迭代器。首先,定义一个简单的迭代器接口:
然后实现一个简单的容器,它维护一个String类型数组:
我们在容器中嵌套了迭代器类。这是最好的选择,因为迭代器需要访问容器的内部变量。我们可以在这里看到它的外观:
3.适用情况和示例
如今,迭代器在大多数编程语言中都很流行,它可能是Java中使用最广泛的集合包。当使用以下循环结构遍历集合时,它也在语言级别实现:
可以使用泛型机制来实现迭代器模式,这样就可以避免强制转换生成的运行时错误。
在Java现有版本中的java.util.Iterator 类和java.util.Collection 类,是实现新容器和迭代器很好的例子。当需要具有特定行为的容器时,我们应该考虑扩展java.
collection包中实现的一个类,而不是创建一个新类。
3.5 观察者模式
随着本书的进展,我们不断提到解耦的重要性。当减少依赖关系时,我们可以扩展、开发和测试不同的模块,而无须了解其他模块的实现细节,只需要知道它们实现的抽象。
尽管如此,在实践当中,模块是需要协同工作的。一个对象往往能够知道另一个对象的变化。例如在游戏中实施汽车类,汽车的引擎需要知道加速器何时改变其位置。一般的解决方案是创建一个引擎类,一直轮询检查加速器位置,看它是否已经改变。而更智能的方法是使加速器调用引擎以通知它有关更改。但如果想得到设计良好的代码,这还不够。
如果加速器(Accelerator)类保留对引擎(Engine)类的引用,当需要在屏幕上显示Accelerator的位置时会发生什么?最好的解决方案是让两者都依赖于抽象,而不是让加速器依赖于引擎。
1.目的
观察者模式使得一个对象的状态改变时,已经登记的其他对象能够观察到这一改变。
2.实现
观察者模式的类图如图3-6所示。
观察者模式依赖于以下类:
- Subject(主题):主题通常是由类实现的可观察的接口。应通知的观察者使用attach方法注册。当它们不再需要被告知变更时,使用detach方法取消注册。
- ConcreteSubject(具体主题):具体主题是一个实现主题接口的类。它处理观察者列表并更新它们的变化。
- Observer(观察者):观察者是一个由对象实现的接口,应该根据主题中的更改来进行更新。每个观察者都应该实现update方法,该方法通知它们新的状态变化。
3.6 中介者模式
在许多情况下,当设计和开发软件应用程序时会遇到这样的场景,程序中有必须相互通信的模块和对象,最简单的实现方法是让它们彼此了解并直接发送消息。
但是,这种做法可能会造成混乱。例如,想象一个通信应用程序,程序中每个客户端必须连接到另一个客户端,那么客户端需要管理许多连接,这对于客户端来说其实并没有意义。更好的解决方案是让客户端都连接到中央服务器,让服务器管理客户端之间的通信。客户端将消息发送到服务器,服务器对客户端所有的连接都保持活动状态,并且可以向所有收件人广播消息。
另一个例子是需要一个专门的类来在图形界面中的不同控件之间扮演中介者,这些控件包括按钮、下拉列表和列表。例如,GUI中的图形控件可以保持对彼此的引用,以便相互调用它们的方法。但显然这么做会创建一段耦合度高的代码,其中每个控件都依赖于所有其他控件。更好的方法是在需要完成某些事情时让窗口负责向所有必需的控件广播消息。当控件中的某些内容修改时,它会通知窗口,该窗口将检查哪些控件需要通知,然后通知它们。
1.目的
中介者模式定义了一个对象,该对象封装了一组对象的交互方式,从而减少了它们之间的相互依赖。
2.实现
中介者模式基于两个抽象—Mediator和Colleague,如图3-7所示。
中介者模式依赖于以下类:
- Mediator(抽象中介者):抽象中介者定义了参与者的交互方式。在此接口或抽象类中声明的操作与场景相关。
- ConcreteMediator(具体中介者):它实现了中介者声明的操作。
- Colleague(抽象同事角色):这是一个抽象类或接口,用于定义需要调解的参与者如何进行交互。
- ConcreteColleague(具体同事角色):这是实现Colleague接口的具体类。
3.适用情况和示例
当有许多实体以类似的方式进行交互并且这些实体应该解耦时,就应该使用中介者模式。
在Java库中,中介者模式用于实现java.util.Timer。timer(计时器)类可用于调度线程以固定间隔运行一次或重复多次运行。线程对象对应于ConcreteColleague类。timer
类实现了管理后台任务执行的方法。
3.7 备忘录模式
封装是面向对象设计的基本原则之一。我们知道每个类都承担一项职责。当向对象添加功能时,我们可能意识到需要保存其内部状态,以便能够在以后阶段恢复它。如果直接在类中实现这样的功能,这个类可能会变得太复杂,最终可能会违反单一职责原则。同时,封装阻止我们直接访问需要记忆的对象的内部状态。
1.目的
备忘录模式用于保存对象的内部状态而不破坏其封装,并在以后阶段恢复其状态。
2.实现
备忘录模式依赖于三个类—Originator、Memento和Caretaker,如图3-8所示。
备忘录模式依赖于以下类:
- Originator(发起者):发起者是我们需要记住状态的对象,以便在某个时刻恢复它。
- Caretaker(管理者):这是负责触发发起者的变化或触发发起者返回先前状态的动作的类。
- Memento(备忘录):这是负责存储发起者内部状态的类。备忘录提供了两种设置和获取状态的方法,但这些方法应该对管理者隐藏。
备忘录实际上比听起来容易得多。我们将它应用于汽车服务应用程序。机械师必须测试每辆车。他们使用自动装置测量汽车的所有输出,以获得不同的参数(速度、挡位、制动器等)。他们执行所有测试并且必须重新检查那些看起来可疑的测试。
首先创建originator类,我们将它命名为CarOriginator,添加两个成员变量。state表示测试运行时汽车的参数,这是我们想要保存的对象的状态。第二个成员变量是结果,这是汽车的测量输出,我们不需要将其存储在备忘录中。这是带有空嵌套备忘录的发起者:
现在我们为不同的状态运行汽车测试:
3.适用情况
只要需要执行回滚操作,就会使用备忘录模式。它可用于各种原子事务,如果其中一个操作失败,则必须将对象恢复到初始状态。
3.8 状态模式
有限状态机是计算机科学中的一个重要概念。它具有强大的数学基础,代表了一个可以处于有限数量状态的抽象机器。有限状态机用于计算机科学的所有领域。
状态模式只是面向对象设计中的有限状态机的实现。类图如图3-9所示。
3.9 策略模式
行为模式的一个特定情况,是我们需要改变解决一个问题与另一个问题的方式。正如在第1章中学到的那样,改变是不好的,而扩展是好的。因此,我们可以将两块代码封装在一个类中,而不是用一部分代码替换另一部分代码。然后可以创建代码所依赖类的抽象。这样会使代码变得非常灵活,我们可以使用任何实现了刚刚创建的抽象的类。
1.目的
策略模式定义了一系列算法,封装了每个算法,并使它们可以互换。
2.实现
策略模式的结构实际上与状态模式的相同。但是实现和意图完全不同。如图3-10所示。
策略模式非常简单:
- Strategy(抽象策略):特定策略的抽象。
- ConcreteStrategy(具体策略):实现抽象策略的类。
- Context(环境):运行特定策略的类。
3.10 模板方法模式
顾名思义,模板方法模式为代码提供了一个模板,可以由实现不同功能的开发人员填写。理解这一点的最简单方法是考虑HTML模板。你访问的大多数网站都遵循某种模板。例如,网站通常有页眉、页脚和侧边栏,它们之间会有核心内容。这意味着模板定义了页眉、页脚和侧边栏,每个内容编写者都可以使用此模板添加其内容。
1.目的
使用模板方法模式的目的是避免编写重复的代码,以便开发人员可以专注于核心逻辑。
2.实现
模板方法模式实现的最好方式是使用抽象类。抽象类可以提供给我们所知道的实现区域,默认实现和为实现而保持开放的区域即为抽象。
例如,实现一个非常高级别的数据库抽取查询。我们需要执行以下步骤:
1)创建一个数据库连接;
2)创建一个query语句;
3)执行query语句;
4)解析并返回数据;
5)关闭数据库连接。
可以看到,打开和关闭连接部分都是一样的,所以可以用模板方法模式实现这一部分,其余部分则根据需要独立地实现。
3.11 空对象模式
空对象模式是本书中涉及的最轻的模式之一。有时它被认为只是策略模式的一个特例,考虑到它在实践中的重要性,它也有自己独特的部分。如果我们使用测试驱动的方法开发程序,或者只是想在没有应用程序的其余部分的情况下开发模块,可以简单地用模拟类替换没有的类,模拟类具有相同的结构但是什么也不做。
实现
在图3-11中可以看到我们只是创建了一个NullClass,它可以替换程序中的真实类。如前所述,这只是我们选择什么都不做的策略模式的一个特例。
3.12 访问者模式
回到我们在讨论命令模式时介绍的形状应用程序。我们应用了命令模式,因此必须重做已实现的操作。现在考虑增加保存功能。
如果将一个抽象的Save方法添加到基本形状类中,并且为每个形状扩展它,我们就解决了问题。这个解决方案可能是最直观的,但不是最好的。首先,每个类都应该承担一项责任。其次,如果需要更改我们想要保存每个形状的格式会发生什么?如果实现相同的方法来生成XML,那么是否必须更改为JSON格式?这种设计绝对不遵循开放/封闭原则。
1.目的
访问者模式将操作与其操作的对象结构分开,允许添加新操作而不更改结构类。
2.实现
访问者模式在单个类中定义了一组操作:它为每个类型的对象定义一个方法,该方法来自它必须操作的结构。只需创建另一个访问者即可添加一组新操作。类图如图3-12所示。
访问者模式基于以下类:
- Element(元素):表示对象结构的基类。结构中的所有类都是它派生的,它们必须实现
- accept(visitor:Visitor)方法。
- ConcreteElementA(具体元素A)和ConcreteElementB(具体元素B):这是我们想要添加在Visitor类中实现的外部操作的具体类。
- (Visitor)访问者:这是基本的Visitor类,它声明了与每个ConcreteElementA相对应的方法。方法的名称相同,但每种方法都按其接受的类型区分。我们可以采用这种解决方案,因为在Java中可以使用名称相同而实际不一样的方法,但如果有需要,我们可以声明具有不同名称的方法。
- ConcreteVisitor(具体访问者):这是访问者的实现。当需要一组单独的操作时,只需创建另一个访问者。
3.13 总结
本章讨论了各种行为型模式。我们研究了一些常用的行为型模式,例如责任链、命令模式、解释器模式等。这些模式有助于我们以受控方式来管理对象的行为。在下一章中,我们将研究有助于我们管理复杂结构的结构型模式。