软件系统设计-设计(4) 行为型模式
2023-08-19 00:15:38 # NJU # 软件系统设计

1. 模板方法模式(类)

1.1 模式动机

image-20230612093800218

  1. 模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中
  2. 在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

1.2 模式定义

  1. 模板方法模式(Template Method Pattern):定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法是一种类行为型模式。
  2. Template Method Pattern: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

1.3 模式结构

image-20230612094432867

模板方法模式包含如下角色:

  1. AbstractClass: 抽象类
  2. ConcreteClass: 具体子类

1.4 模式分析

  1. 模板方法模式是一种类的行为型模式,在它的结构图中只有类之间的继承关系,没有对象关联关系
  2. 在模板方法模式的使用过程中,要求开发抽象类和开发具体子类的设计师之间进行协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法称为基本方法(Primitive Method),而将这些基本法方法汇总起来的方法称为模板方法(Template Method),模板方法模式的名字从此而来。
  3. 模板方法:一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
  4. 基本方法:基本方法是实现算法各个步骤的方法,是模板方法的组成部分。
    1. 抽象方法(Abstract Method)
    2. 具体方法(Concrete Method)
    3. 钩子方法(Hook Method):“挂钩”方法和空方法
  5. 钩子方法(Hook Method)
1
2
3
4
5
6
7
8
9
10
public void template(){
open();
display();
if(isPrint()){
print();
}
}
public boolean isPrint(){
return true;
}
  1. 典型的抽象类代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
public abstract class AbstractClass{
public void templateMethod() { //模板方法
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}
public void primitiveOperation1() { //基本方法—具体方法
//实现代码
}
public abstract void primitiveOperation2(); //基本方法—抽象方法
public void primitiveOperation3() {} //基本方法—钩子方法
}
  1. 典型的具体子类代码如下所示:
1
2
3
4
5
6
7
8
public class ConcreteClass extends AbstractClass {
public void primitiveOperation2() {
//实现代码
}
public void primitiveOperation3() {
//实现代码, 覆盖空方法
}
}
  1. 在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法,子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制

1.5 模板方法模式实例与解析

实例一:银行业务办理流程

在银行办理业务时,一般都包含几个基本步骤,首先需要取号排队,然后办理具体业务,最后需要对银行工作人员进行评分。无论具体业务是取款、存款还是转账,其基本流程都一样。现使用模板方法模式模拟银行业务办理流程。模板方法模式

image-20230612094805890

实例二:数据库操作模板

对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了connDB()openDB()useDB()closeDB() 四个方法分别对应这四个步骤。对于不同类型的数据库(如SQL Server和Oracle),其操作步骤都一致,只是连接数据库 connDB() 方法有所区别,现使用模板方法模式对其进行设计。

image-20230612094835824

1.6 模式优缺点

模板方法模式的优点

  1. 模板方法模式在一个类中抽象地定义算法,而由它的子类实现细节的处理
  2. 模板方法模式是一种代码复用的基本技术
  3. 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”

模板方法模式的缺点

  1. 每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。

1.7 模式适用环境

在以下情况下可以使用模板方法模式:

  1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
  3. 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
  4. 控制子类的扩展

1.8 模式应用

  1. 模板方法模式广泛应用于框架设计(如Spring,Struts等)中,以确保父类控制处理流程的逻辑顺序(如框架的初始化)。
  2. Java单元测试工具JUnit中的TestCase类的设计:
1
2
3
4
5
6
7
8
9
public void runBare() throws Throwable {
setUp();
try {
runTest();
}
finally {
tearDown();
}
}

1.9 模式扩展

关于继承的讨论

  1. 模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。这也进一步说明,虽然继承复用存在一些问题,但是在某些情况下还是可以给开发人员带来方便,模板方法模式就是体现继承优势的模式之一

好莱坞原则

  1. 在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”。
  2. 在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程

钩子方法的使用

  1. 钩子方法的引入使得子类可以控制父类的行为。
  2. 最简单的钩子方法就是空方法,也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
  3. 比较复杂一点的钩子方法可以对其他方法进行约束,这种钩子方法通常返回一个boolean类型,即返回true或false,用来判断是否执行某一个基本方法。

1.10 小结

  1. 在模板方法模式中,定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法是一种类行为型模式。
  2. 模板方法模式包含两个角色
    1. 在抽象类中定义一系列基本操作,这些基本操作可以是具体的,也可以是抽象的,同时,在抽象类中实现了一个模板方法,用于定义一个算法的骨架
    2. 具体子类是抽象类的子类,用于实现在父类中定义的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中实现的具体基本操作。
  3. 在模板方法模式中,方法可以分为模板方法和基本方法,其中基本方法又可以分为抽象方法、具体方法和钩子方法,钩子方法根据其特点又分为空方法和与实现算法步骤的基本方法“挂钩”的方法。
  4. 模板方法模式
    1. 优点在于在子类定义详细的处理算法时不会改变算法的结构,实现了代码的复用,通过对子类的扩展可以增加新的行为,符合“开闭原则”
    2. 其缺点在于需要为每个不同的实现都定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
  5. 模板方法模式适用情况包括
    1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
    2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
    3. 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法,而一些可以改变的细节由其子类来实现
    4. 通过模板方法模式还可以控制子类的扩展。

2. 命令模式(对象)

2.1 模式动机

命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。

image-20230314112052652

2.2 模式定义

命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

2.3 模式结构

image-20230314112241809

命令模式包含如下角色:

  1. Command: 抽象命令类
  2. ConcreteCommand: 具体命令类
  3. Invoker: 调用者
  4. Receiver: 接收者
  5. Client:客户类

2.4 模式分析

  1. 命令模式的本质是对命令进行封装将发出命令的责任和执行命令的责任分割开
  2. 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作
  3. 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
  4. 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  5. 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联

image-20230314114539376

典型的抽象命令类代码:

1
2
3
public abstract class Command{
public abstract void execute();
}

典型的调用者代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Invoker{
private Command command;
public Invoker(Command command){
this.command=command;
}
public void setCommand(Command command){
this.command=command;
}
//业务方法,用于调用命令类的方法
public void call(){
command.execute();
}
}

典型的具体命令类代码

1
2
3
4
5
6
public class ConcreteCommand extends Command {
private Receiver receiver;
public void execute() {
receiver.action();
}
}

典型的请求接收者代码

1
2
3
4
5
public class Receiver {
public void action() {
//具体操作
}
}

2.5 命令模式实例与解析

电视机遥控器

电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。

image-20230314114828230

功能键设置

为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,如功能键 FunctionButton 可以用于退出系统(SystemExitClass),也可以用于打开帮助界面 (DisplayHelpClass)。用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,相同的功能键可以对应不同的功能。

image-20230314115051773

2.6 模式优缺点

命令模式的优点

  1. 降低系统的耦合度。
  2. 新的命令可以很容易地加入到系统中。
  3. 可以比较容易地设计一个命令队列和宏命令(组合命令)。
  4. 可以方便地实现对请求的Undo和Redo

命令模式的缺点

  1. 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用

2.7 模式适用环境

  1. 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  2. 系统需要在不同的时间指定请求、将请求排队和执行请求
  3. 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
  4. 系统需要将一组操作组合在一起,即支持宏命令。

2.8 模式应用

Java语言使用命令模式实现AWT/Swing GUI的委派事件模型 (Delegation Event Model, DEM)

  • 在AWT/Swing中,Frame、Button等界面组件请求发送者,而AWT提供的事件监听器接口和事件适配器类抽象命令接口用户可以自己写抽象命令接口的子类来实现事件处理,即实现具体命令类,而在具体命令类中可以调用业务处理方法来实现该事件的处理。对于界面组件而言,只需要了解命令接口即可,无须关心接口的实现,组件类并不关心实际操作,而操作由用户来实现。

很多系统都提供了宏命令功能,如UNIX平台下的Shell编程,可以将多条命令封装在一个命令对象中,只需要一条简单的命令即可执行一个命令序列,这也是命令模式的应用实例之一。

2.9 模式扩展

撤销操作的实现

image-20230314151619323

宏命令

  1. 宏命令又称为组合命令,它是命令模式和组合模式联用的产物。
  2. 宏命令也是一个具体命令,不过它包含了对其他命令对象的引用,在调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员对象可以是简单命令,还可以继续是宏命令。执行一个宏命令将执行多个具体命令,从而实现对命令的批处理。

image-20230314151824589

3. 迭代器模式(对象)

3.1 模式动机

image-20230627221420555

  • 电视机 <-> 存储电视频道的集合 <-> 聚合类(Aggregate Classes)
  • 电视机遥控器 <-> 操作电视频道 <-> 迭代器(Iterator)
  • 访问一个聚合对象中的元素但又不需要暴露它的内部结构
  • 聚合对象的两个职责:
    • 存储数据,聚合对象的基本职责
    • 遍历数据,既是可变化的,又是可分离的
  • 将遍历数据的行为从聚合对象中分离出来,封装在迭代器对象中
  • 由迭代器来提供遍历聚合对象内部数据的行为,简化聚合对象的设计,更符合单一职责原则

3.2 模式定义

  • 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,且不用暴露该对象的内部表示。
  • Iterator Pattern: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
  • 又名游标(Cursor)模式
  • 通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式

3.3 模式结构

image-20230627221720676迭代器模式包含以下4个角色:

  • Iterator (抽象迭代器)
  • ConcreteIterator (具体迭代器)
  • Aggregate (抽象聚合类)
  • ConcreteAggregate (具体聚合类)

典型的抽象迭代器代码:

1
2
3
4
5
6
public interface Iterator {
public void first(); //将游标指向第一个元素
public void next(); //将游标指向下一个元素
public boolean hasNext(); //判断是否存在下一个元素
public Object currentItem(); //获取游标指向的当前元素
}

典型的具体迭代器代码:

1
2
3
4
5
6
7
8
9
10
11
public class ConcreteIterator implements Iterator {
private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据
private int cursor; //定义一个游标,用于记录当前访问位置
public ConcreteIterator(ConcreteAggregate objects) {
this.objects=objects;
}
public void first() { ...... }
public void next() { ...... }
public boolean hasNext() { ...... }
public Object currentItem() { ...... }
}

典型的抽象聚合类代码:

1
2
3
public interface Aggregate {
Iterator createIterator();
}

典型的具体聚合类代码:

1
2
3
4
5
6
7
public class ConcreteAggregate implements Aggregate {
......
public Iterator createIterator() {
return new ConcreteIterator(this);
}
......
}

3.4 模式分析

  • 如果需要增加一个新的具体聚合类,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合开闭原则
  • 如果需要更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法即可,原有迭代器代码无须修改,也符合开闭原则
  • 如果要在迭代器中增加新的方法,则需要修改抽象迭代器的源代码,这将违背开闭原则

不同的实现(宽接口 vs. 窄接口)

  • 宽接口:一个聚集的接口提供了可以用来修改聚集元素的方法
  • 窄接口:一个聚集的接口没有提供修改聚集元素的方法

白箱聚集 vs. 黑箱聚集

  • 白箱聚集:聚集对象为所有对象提供同一个接口(宽接口)
    • 迭代子可以从外部控制聚集元素的迭代,控制的仅仅是一个游标—游标(Cursor)/外禀(Extrinsic)迭代子

image-20230627222858106

  • 黑箱聚集:聚集对象为迭代子对象提供一个宽接口,而为其它对象提供一个窄接口。同时保证聚集对象的封装和迭代子功能的实现。
    • 迭代子是聚集的内部类,可以自由访问聚集的元素。迭代子可以自行实现迭代功能并控制聚集元素的迭代逻辑—内禀迭代子(Intrinsic Iterator)

image-20230627222954258

3.5 模式优缺点

迭代器模式优点

  • 支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
  • 简化了聚合类
  • 由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,符合开闭原则

迭代器模式缺点

  • 在增加新的聚合类时需要对应地增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是一件很容易的事情

3.6 模式适用环境

  • 访问一个聚合对象的内容而无须暴露它的内部表示
  • 需要为一个聚合对象提供多种遍历方式
  • 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口

3.7 模式应用

使用内部类实现迭代器

  • JDK中的AbstractList
1
2
3
4
5
6
7
8
9
10
package java.util;
......
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
......
private class Itr implements Iterator<E> {
int cursor = 0;
......
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用内部类实现的商品数据类
public class ProductList extends AbstractObjectList {
public ProductList(List products) {
super(products);
}
public AbstractIterator createIterator() {
return new ProductIterator();
}
//商品迭代器:具体迭代器,内部类实现
private class ProductIterator implements AbstractIterator {
private int cursor1;
private int cursor2;
//省略其他代码
}
}

结构

image-20230627223238811

实现

  • java.util.Collection

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package java.util;
    public interface Collection<E> extends Iterable<E> {
    ......
    boolean add(Object c);
    boolean addAll(Collection c);
    boolean remove(Object o);
    boolean removeAll(Collection c);
    boolean remainAll(Collection c);
    Iterator iterator();
    ......
    }
  • java.util.Iterator

    1
    2
    3
    4
    5
    6
    package java.util;
    public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
    }

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Java java.util.*;

public class IteratorDemo {
public static void process(Collection c) {
Iterator i = c.iterator(); //创建迭代器对象
//通过迭代器遍历聚合对象
while(i.hasNext()) {
System.out.println(i.next().toString());
}
}
public static void main(String args[]) {
Collection persons;
persons = new ArrayList(); //创建一个ArrayList类型的聚合对象
persons.add("张无忌");
persons.add("小龙女");
persons.add("令狐冲");
persons.add("韦小宝");
persons.add("袁紫衣");
persons.add("小龙女");
process(persons);
}
}

4. 观察者模式(对象)

4.1 模式动机

image-20230612085531412

  • 建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应
    • 发生改变的对象称为观察目标
    • 被通知的对象称为观察者
  • 一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。

4.2 模式定义

  1. 观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新
  2. 观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。
  3. Observer Pattern: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

4.3 模式结构

image-20230612085646353

  • 观察者模式包含如下角色:
    • Subject: 目标
    • ConcreteSubject: 具体目标
    • Observer: 观察者
    • ConcreteObserver: 具体观察者

4.4 模式分析

  • 观察者模式描述了如何建立对象与对象之间的依赖关系,如何构造满足这种需求的系统。
  • 这一模式中的关键对象是观察目标和观察者,一个目标可以有任意数目的与之相依赖的观察者,一旦目标的状态发生改变,所有的观察者都将得到通知
  • 作为对这个通知的响应,每个观察者都将即时更新自己的状态,以与目标状态同步,这种交互也称为发布-订阅 (publish-subscribe)

松耦合

  • 当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。
  • 观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
  • 改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,就可以自由地改变他们。
  • 为了交互对象之间的松耦合设计而努力

4.5 观察者模式实例与解析

WeatherData

  • WeatherData类具有getter方法,可以取得三个测量值:温度、湿度与气压。
  • 当新的测量数据备妥时,measurementsChanged() 方法就会被调用。
  • 需要实现三个使用天气数据的布告板: “目前状况” 布告、“气象统计”布告、“天气预报”布告。一旦 WeatherData 有新测量,这些布告必须马上更新。
  • 此系统必须可扩展,让其他开发人员建立定制的布告板,用户可以随心所欲地添加或删除任何布告板

image-20230612090525324

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CurrentConditions implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;

public CurrentConditions(Subject weatherSubject) {
this.weatherData = weatherSubject;
weatherData.registerObserver(this);
}

@Override
public void display() {
System.out.println("Current Conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}

@Override
public void update(float t, float h) {
this.temperature = t;
this.humidity = h;
display();
}
}

4.6 模式优缺点

观察者模式的优点

  1. 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
  2. 观察者模式在观察目标和观察者之间建立一个抽象的耦合
  3. 观察者模式支持广播通信
  4. 观察者模式符合开闭原则的要求。

观察者模式的缺点

  1. 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

4.7 模式适用环境

在以下情况下可以使用观察者模式:

  1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用
  2. 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  3. 一个对象必须通知其他对象,而并不知道这些对象是谁
  4. 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制

4.8 模式应用

  1. JDK1.1版本及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(Delegation Event Model, DEM)
    1. 在DEM中,事件的发布者称为事件源(Event Source),而订阅者叫做事件监听器(Event Listener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。
    2. 事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。
  2. 除了AWT中的事件处理之外,Java语言解析XML的技术SAX2以及Servlet技术的事件处理机制都基于DEM,它们都是观察者模式的应用。
  3. 观察者模式在软件开发中应用非常广泛,如某电子商务网站可以在执行发送操作后给用户多个发送商品打折信息,某团队战斗游戏中某队友牺牲将给所有成员提示等等,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式

4.9 模式扩展

Java语言提供的对观察者模式的支持

  • 在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了Java语言对观察者模式的支持。

image-20230612091826405

  • Observer接口:void update(Observable o, Object arg);
  • Observable类

    • Observable()
    • addObserver(Observer o)
    • deleteObserver (Observer o)
    • notifyObservers()notifyObservers(Object arg)
    • deleteObservers
    • setChanged()
    • clearChanged()
    • hasChanged()
    • countObservers()
  • 谁触发更新:目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用 Notify 来触发更新? 此时有两个选择:

    • 由目标对象的状态设定操作在改变目标对象的状态后自动调用 Notify。这种方法的优点是客户不需要记住要在目标对象上调用 Notify,缺点是多个连续的操作会产生多次连续的更新, 可能效率较低。
    • 让客户负责在适当的时候调用 Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新, 避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用 Notify,这种方式较易出错。

封装复杂的更新语义

  • 当目标和观察者间的依赖关系特别复杂时, 可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(Change Manager)。
  • 它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如, 如果一个操作涉及到对几个相互依赖的目标进行改动, 就必须保证仅在所有目标都已更改完毕后,才一次性地通知它们的观察者, 而不是每个目标都通知观察者。

image-20230612092416439

MVC模式

  • MVC模式是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。观察者模式可以用来实现MVC模式,观察者模式中的观察目标就是MVC模式中的模型(Model),而观察者就是MVC中的视图(View),控制器(Controller)充当两者之间的中介者(Mediator)。当模型层的数据发生改变时,视图层将自动改变其显示内容

image-20230612092529510

4.10 小结

  1. 观察者模式定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅模式、模型-视图模式、源-监听器模式或从属者模式。观察者模式是一种对象行为型模式。
  2. 观察者模式包含四个角色:
    1. 目标又称为主题,它是指被观察的对象
    2. 具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知
    3. 观察者将对观察目标的改变做出反应
    4. 在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致。
  3. 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
  4. 观察者模式
    1. 主要优点在于可以实现表示层和数据逻辑层的分离,并在观察目标和观察者之间建立一个抽象的耦合,支持广播通信
    2. 主要缺点在于如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,而且如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  5. 观察者模式适用情况包括:
    1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面
    2. 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变
    3. 一个对象必须通知其他对象,而并不知道这些对象是谁;需要在系统中创建一个触发链。
  6. 在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了Java语言对观察者模式的支持。

5. 中介者模式(对象)

image-20230612092658438

5.1 模式动机

  1. 在用户与用户直接聊天的设计方案中,用户对象之间存在很强的关联性,将导致系统出现如下问题:
    1. 系统结构复杂:对象之间存在大量的相互关联和调用,若有一个对象发生变化,则需要跟踪和该对象关联的其他所有对象,并进行适当处理。
    2. 对象可重用性差:由于一个对象和其他对象具有很强的关联,若没有其他对象的支持,一个对象很难被另一个系统或模块重用,这些对象表现出来更像一个不可分割的整体,职责较为混乱。
    3. 系统扩展性低:增加一个新的对象需要在原有相关对象上增加引用,增加新的引用关系也需要调整原有对象,系统耦合度很高,对象操作很不灵活,扩展性差。
  2. 在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责
  3. 对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。

5.2 模式定义

  1. 中介者模式(Mediator Pattern)定义:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式
  2. Mediator Pattern: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

5.3 模式结构

image-20230612092759415

中介者模式包含如下角色:

  1. Mediator: 抽象中介者
  2. ConcreteMediator: 具体中介者
  3. Colleague: 抽象同事类
  4. ConcreteColleague: 具体同事类

5.4 模式分析

  • 中介者模式可以使对象之间的关系数量急剧减少:

image-20230612092851729

  • 中介者承担两方面的职责:

    1. 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。该中转作用属于中介者在结构上的支持
    2. 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持
  • 典型的抽象中介者类代码:

1
2
3
4
5
6
7
public abstract class Mediator {
protected ArrayList colleagues;
public void register(Colleague colleague) {
colleagues.add(colleague);
}
public abstract void operation();
}
  • 典型的具体中介者类代码:
1
2
3
4
5
6
7
public class ConcreteMediator extends Mediator {
public void operation() {
......
((Colleague)(colleagues.get(0))).method1();
......
}
}
  • 典型的抽象同事类代码:
1
2
3
4
5
6
7
8
public abstract class Colleague{
protected Mediator mediator;
public Colleague(Mediator mediator){
this.mediator=mediator;
}
public abstract void method1();
public abstract void method2();
}
  • 典型的具体同事类代码:
1
2
3
4
5
6
7
8
9
10
11
public class ConcreteColleague extends Colleague{
public ConcreteColleague(Mediator mediator){
super(mediator);
}
public void method1(){
......
}
public void method2(){
mediator.operation1();
}
}

5.5 中介者模式实例与解析

实例:虚拟聊天室

某论坛系统欲增加一个虚拟聊天室,允许论坛会员通过该聊天室进行信息交流,普通会员(CommonMember)可以给其他会员发送文本信息,钻石会员(DiamondMember)既可以给其他会员发送文本信息,还可以发送图片信息。该聊天室可以对不雅字符进行过滤,如“日”等字符;还可以对发送的图片大小进行控制。用中介者模式设计该虚拟聊天室。

image-20230612093348250

5.6 模式优缺点

中介者模式的优点

  1. 简化了对象之间的交互。
  2. 将各同事解耦。
  3. 减少子类生成。
  4. 可以简化各同事类的设计和实现。

中介者模式的缺点

  1. 在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护

5.7 模式适用环境

在以下情况下可以使用中介者模式:

  1. 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
  2. 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象
  3. 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类。

5.8 模式应用

  1. 中介者模式在事件驱动类软件中应用比较多,在设计GUI应用程序时,组件之间可能存在较为复杂的交互关系,一个组件的改变将影响与之相关的其他组件,此时可以使用中介者模式来对组件进行协调。
  2. MVC 是Java EE 的一个基本模式,此时控制器Controller作为一种中介者,它负责控制视图对象View和模型对象Model之间的交互。如在Struts中,Action就可以作为JSP页面与业务对象之间的中介者。

5.9 模式扩展

  1. 中介者模式与迪米特法则
    1. 在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代。因此,中介者模式就是迪米特法则的一个典型应用
  2. 中介者模式与GUI开发
    1. 中介者模式可以方便地应用于图形界面(GUI)开发中,在比较复杂的界面中可能存在多个界面组件之间的交互关系
    2. 对于这些复杂的交互关系,有时候我们可以引入一个中介者类,将这些交互的组件作为具体的同事类,将它们之间的引用和控制关系交由中介者负责,在一定程度上简化系统的交互,这也是中介者模式的常见应用之一。

5.10 小结

  1. 中介者模式用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
  2. 中介者模式包含四个角色:
    1. 抽象中介者用于定义一个接口,该接口用于与各同事对象之间的通信
    2. 具体中介者是抽象中介者的子类,通过协调各个同事对象来实现协作行为,了解并维护它的各个同事对象的引用
    3. 抽象同事类定义各同事的公有方法
    4. 具体同事类是抽象同事类的子类,每一个同事对象都引用一个中介者对象;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中定义的方法。
  3. 通过引入中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,中介者承担了中转作用和协调作用。中介者类是中介者模式的核心,它对整个系统进行控制和协调,简化了对象之间的交互,还可以对对象间的交互进行进一步的控制。
  4. 中介者模式
    1. 主要优点在于简化了对象之间的交互,将各同事解耦,还可以减少子类生成,对于复杂的对象之间的交互,通过引入中介者,可以简化各同事类的设计和实现
    2. 主要缺点在于具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
  5. 中介者模式适用情况包括
    1. 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解
    2. 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象
    3. 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。

6. 状态模式(对象)

6.1 模式动机

在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

在UML中可以使用状态图来描述对象状态的变化。

6.2 模式定义

状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。

State Pattern: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

6.3 模式结构

image-20230307115128774

状态模式包含如下角色:

  1. Context: 环境类
  2. State: 抽象状态类
  3. ConcreteState: 具体状态类

在结构上策略模式和状态模式是一致的,但是在使用上是很不同的

  • 状态模式由状态自己进行转换
  • 策略模式由客户端决定

6.4 模式分析

  1. 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为
  2. 状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换

image-20230307115352857

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(state=="空闲"){
if(预订房间){
// 预订操作;
state="已预订";
}else if(住进房间){
// 入住操作;
state="已入住";
}
}else if(state=="已预订"){
if(住进房间){
// 入住操作;
state="已入住";
}else if(取消预订){
// 取消操作;
state="空闲";
}
}

使用状态模式重构

image-20230307151033908

1
2
3
4
5
6
7
8
//重构之后的“空闲状态类”示例代码
if(预订房间){
// 预订操作;
context.setState(new 已预订状态类());
}else if(住进房间){
// 入住操作;
context.setState(new 已入住状态类());
}

在状态模式结构中需要理解环境类与抽象状态类的作用:

  1. 环境类实际上就是拥有状态的对象,环境类有时候可以充当状态管理器(State Manager)的角色,可以在环境类中对状态进行切换操作
  2. 抽象状态类可以是抽象类,也可以是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足两个条件:这些状态经常需要切换,在不同的状态下对象的行为不同。因此可以将不同对象下的行为单独提取出来封装在具体的状态类中,使得环境类对象在其内部状态改变时可以改变它的行为,对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的。由于环境类可以设置为任一具体状态类,因此它针对抽象状态类进行编程,在程序运行时可以将任一具体状态类的对象设置到环境类中,从而使得环境类可以改变内部状态,并且改变行为。

6.5 状态模式实例与解析

实例:论坛用户等级

在某论坛系统中,用户可以发表留言,发表留言将增加积分;用户也可以回复留言,回复留言也将增加积分;用户还可以下载文件,下载文件将扣除积分。该系统用户分为三个等级,分别是新手、高手和专家,这三个等级对应三种不同的状态,这三种状态分别定义如下:

  1. 如果积分小于100分,则为新手状态,用户可以发表留言、回复留言,但是不能下载文件。如果积分大于等于1000分,则转换为专家状态;如果积分大于等于100分,则转换为高手状态。
  2. 如果积分大于等于100分但小于1000分,则为高手状态,用户可以发表留言、回复留言,还可以下载文件,而且用户在发表留言时可以获取双倍积分。如果积分小于100分,则转换为新手状态;如果积分大于等于1000分,则转换为专家状态;如果下载文件后积分小于0,则不能下载该文件。
  3. 如果积分大于等于1000分,则为专家状态,用户可以发表留言、回复留言和下载文件,用户除了在发表留言时可以获取双倍积分外,下载文件只扣除所需积分的一半。如果积分小于100分,则转换为新手状态;如果积分小于1000分,但大于等于100,则转换为高手状态;如果下载文件后积分小于0,则不能下载该文件。

image-20230307151649799

6.6 模式优缺点

状态模式的优点

  1. 封装了转换规则
  2. 枚举可能的状态,在枚举状态之前需要确定状态种类。
  3. 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  4. 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
  5. 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

状态模式的缺点

  1. 状态模式的使用必然会增加系统类和对象的个数
  2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
  3. 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

6.7 模式适用环境

  1. 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为
  2. 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。

6.8 模式应用

  1. 状态模式在工作流或游戏等类型的软件中得以广泛使用,甚至可以用于这些系统的核心功能设计,如在政府OA办公系统中,一个批文的状态有多种:尚未办理;正在办理;正在批示;正在审核;已经完成等各种状态,而且批文状态不同时对批文的操作也有所差异。使用状态模式可以描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。
  2. 在目前主流的RPG(Role Play Game,角色扮演游戏)中,使用状态模式可以对游戏角色进行控制,游戏角色的升级伴随着其状态的变化和行为的变化。对于游戏程序本身也可以通过状态模式进行总控,一个游戏活动包括开始、运行、结束等状态,通过对状态的控制可以控制系统的行为,决定游戏的各个方面,因此可以使用状态模式对整个游戏的架构进行设计与实现

6.9 模式扩展

共享状态

在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象

简单状态模式与可切换状态的状态模式

  1. 简单状态模式:简单状态模式是指状态都相互独立,状态之间无须进行转换的状态模式,这是最简单的一种状态模式。对于这种状态模式,每个状态类都封装与状态相关的操作,而无须关心状态的切换,可以在客户端直接实例化状态类,然后将状态对象设置到环境类中。如果是这种简单的状态模式,它遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。
  2. 可切换状态的状态模式:大多数的状态模式都是可以切换状态的状态模式,在实现状态切换时,在具体状态类内部需要调用环境类Context的setState()方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法,因此状态类与环境类之间通常还存在关联关系或者依赖关系。通过在状态类中引用环境类的对象来回调环境类的setState()方法实现状态的切换。在这种可以切换状态的状态模式中,增加新的状态类可能需要修改其他某些状态类甚至环境类的源代码,否则系统无法切换到新增状态。

7. 策略模式(对象)

7.1 模式描述

策略模式定义了一系列算法,将每个算法封装在一起,并使它们可替换。策略使算法可以独立于使用该算法的客户端而变化

  1. 变化在客户使用时才会出现,也就是要实现这个模式就必须要将细节暴露给用户。
  2. 实际开发的时候,可能是由多个设计模式组合成的
  3. 我们可能需要一个算法族,希望彼此是可以替换的

image-20230302010414510

7.2 模式动机

  • 例如:存在许多用于将文本流分成行的算法。将所有这样的算法硬连接到类中是不可取的。
    • 不满足开闭原则,每次修改都要反复检查每一个条件语句

image-20230302010708842

7.3 应用场景

在以下情况下使用策略模式

  1. 许多相关的类仅在行为上有所不同,策略提供了一种使用其中一种行为配置类的方法
  2. 您需要算法的不同变体。例如,您可能定义了反映不同空间/时间权衡的算法。将这些变体实现为算法的类层次结构时,可以使用策略。
  3. 一种算法使用客户端不应该知道的数据。使用策略模式可避免暴露复杂的、特定于算法的数据结构
  4. 一个类定义了许多行为,这些行为在其操作中显示为多个条件语句。代替许多条件,将相关的条件分支移到他们自己的策略类中。

7.4 产生的结果

  • 相关算法族(Families of related algorithms)
    • 策略类的层次结构定义了一个算法族或行为,以供上下文重用。继承可以帮助分解出算法的通用功能
  • 子类化的替代方法(An alternative to subclassing)
  • 策略消除了条件语句(Strategies eliminate conditional statements)
  • 多种实现方式(A choice of implementations)
    • 策略可以提供相同行为的不同实现。客户可以选择具有不同时间和空间权衡的策略
  • 客户必须意识到不同的策略(Clients must be aware of different Strategies)
    • 这种模式有一个潜在的缺点,即客户在选择合适的策略之前必须先了解策略的不同,不然客户可能会遇到实现问题
  • 策略和上下文之间的通信开销(Communication overhead between Strategy and Context)
  • 对象数量增加(Increased number of objects)