设计模式系列文章导航

  1. 设计模式概述
  2. 面向对象设计原则
  3. 设计模式 - 创建型模式
  4. 设计模式 - 结构型模式
  5. 设计模式 - 行为型模式 📍当前位置

职责链模式(Chain of Responsibility)

定义

避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止。

模式结构

结构图

image-20231229155049439

实现代码

  • 抽象处理者(Handler)

    它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。每一个处理者的下家还是一个处理者,故在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor)作为其对下家的引用,通过该引用处理者可以连成一条链。

    1
    2
    3
    4
    5
    6
    7
    8
    public abstract class Handler {
    //维持对下家的引用
    protected Handler successor;
    public void setSuccessor(Handler successor) {
    this.successor=successor;
    }
    public abstract void handleRequest(String request);
    }
  • 具体处理者(ConcreteHandler)

    它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中的下一个对象,以便请求的转发。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class ConcreteHandler extends Handler {
    public void handleRequest(String request) {
    if (请求满足条件) {
    //处理请求
    }
    else {
    //转发请求
    this.successor.handleRequest(request);
    }
    }
    }

应用实例

某企业的SCM(供应链管理)系统中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。如下图所示:

采购单分级审批示意图

现使用职责链模式设计并实现该系统。

image-20231229155417392

  • 抽象处理者(抽象传递者):PurchaseRequest
  • 具体处理者(具体传递者):DirectorVicePresidentPresidentCongress
  • 请求类:PurchaseRequest

特点及使用环境

优点

  1. 使得一个对象无须知道是其他哪一个对象处理其请求,降低了系统的耦合度
  2. 可简化对象之间的相互连接
  3. 给对象职责的分配带来更多的灵活性
  4. 增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可

缺点

  1. 不能保证请求一定会被处理
  2. 对于比较长的职责链,系统性能将受到一定影响,在进行代码调试时不太方便
  3. 如果建链不当,可能会造成循环调用,将导致系统陷入死循环

适用环境

  1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定
  2. 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
  3. 可动态指定一组对象处理请求

命令模式(Command)

定义

将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。

image-20231229155739538

模式结构

结构图

image-20231229155808876

实现代码

  • 抽象命令类(Command)

    抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。

    1
    2
    3
    public abstract class Command {
    public abstract void execute();
    }
  • 具体命令类(ConcreteCommand)

    具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。具体命令类在实现execute()方法时将调用接收者对象的相关操作(Action)。

    1
    2
    3
    4
    5
    6
    7
    public class ConcreteCommand extends Command {
    private Receiver receiver; //维持一个对请求接收者对象的引用

    public void execute() {
    receiver.action(); //调用请求接收者的业务处理方法action()
    }
    }
  • 调用者(Invoker)

    调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Invoker {
    private Command command;

    //构造注入
    public Invoker(Command command) {
    this.command = command;
    }

    //设值注入
    public void setCommand(Command command) {
    this.command = command;
    }

    //业务方法,用于调用命令类的execute()方法
    public void call() {
    command.execute();
    }
    }
  • 接收者(Receiver)

    接收者执行与请求相关的操作,具体实现对请求的业务处理。

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

应用实例

为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,例如功能键FunctionButton可以用于退出系统(由SystemExitClass类来实现),也可以用于显示帮助文档(由DisplayHelpClass类来实现)。

用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,可为同一个功能键设置不同的功能。

image-20231229160222136

  • 请求调用者:FunctionButton
  • 请求接收者:SystemExitClassDisplayHelpClass
  • 抽象命令类:Command
  • 具体命令者:ExitCommandHelpCommand

模式变化

实现命令队列

  • 实现动机:
    • 当一个请求发送者发送一个请求时,有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理
    • 增加一个CommandQueue类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者
    • 批处理

记录请求日志

  • 实现动机:

    将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中

    • 为系统提供一种恢复机制
    • 可以用于实现批处理
    • 防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行

实现撤销操作

  • 可以通过对命令类进行修改使得系统支持撤销(Undo)操作和恢复(Redo)操作

宏命令

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

特点及使用环境

优点

  1. 降低系统的耦合度
  2. 新的命令可以很容易地加入到系统中,符合开闭原则
  3. 可以比较容易地设计一个命令队列或宏命令(组合命令)
  4. 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案

缺点

  1. 使用命令模式可能会导致某些系统有过多的具体命令类(针对每一个对请求接收者的调用操作都需要设计一个具体命令类)

适用环境

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

解释器模式(Interpreter)

定义

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

模式结构

结构图

image-20231229175727479

实现代码

  • 抽象表达式(AbstractExpression)

    在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。

    1
    2
    3
    public abstract class AbstractExpression {
    public abstract void interpret(Context ctx);
    }
  • 终结符表达式(TerminalExpression)

    终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。

    1
    2
    3
    4
    5
    public class TerminalExpression extends AbstractExpression {
    public void interpret(Context ctx) {
    //终结符表达式的解释操作
    }
    }
  • 非终结符表达式(NonterminalExpression)

    非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class NonterminalExpression extends AbstractExpression {
    private AbstractExpression left;
    private AbstractExpression right;

    public NonterminalExpression(AbstractExpression left,AbstractExpression right) {
    this.left=left;
    this.right=right;
    }

    public void interpret(Context ctx) {
    //递归调用每一个组成部分的interpret()方法
    //在递归调用时指定组成部分的连接方式,即非终结符的功能
    }
    }
  • 环境类(Context)

    • 用于存储一些全局信息,一般包含一个HashMap或ArrayList等类型的集合对象(也可以直接由HashMap等集合类充当环境类),存储一系列公共信息,例如变量名与值的映射关系(key/value)等,用于在执行具体的解释操作时从中获取相关信息
    • 可以在环境类中增加一些所有表达式解释器都共有的功能,以减轻解释器的职责
    • 当系统无须提供全局公共信息时可以省略环境类,根据实际情况决定是否需要环境类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Context {
    private HashMap<String, String> map = new HashMap<String, String>();

    public void assign(String key, String value) {
    //往环境类中设值
    map.put(key, value);
    }

    public String lookup(String key) {
    //获取存储在环境类中的值
    return map.get(key);
    }
    }

应用实例

某软件公司要开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式。每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中,移动方向包括向上(up)、向下(down)、向左(left)、向右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。

用户通过对图形化的设置界面进行操作可以创建一个机器人控制指令,机器人在收到指令后将按照指令的设置进行移动,例如输入控制指令“up move 5”将“向上移动5个单位”;输入控制指令“down run 10 and left move 20”将“向下快速移动10个单位再向左移动20个单位”。

image-20231229180138141

  • 抽象表达式角色:AbstractNode
  • 终结符表达式角色:DirectionNodeActionNodeDistanceNode
  • 非终结符表达式角色:AndNodeSentenceNode

特点及使用环境

优点

  1. 易于改变和扩展文法
  2. 可以方便地实现一个简单的语言
  3. 实现文法较为容易(有自动生成工具)
  4. 增加新的解释表达式较为方便

缺点

  1. 对于复杂文法难以维护
  2. 执行效率较低

适用环境

  1. 可以将一个需要解释执行的语言中的句子表示为一棵抽象语法树
  2. 一些重复出现的问题可以用一种简单的语言来进行表达
  3. 一个语言的文法较为简单
  4. 执行效率不是关键问题

迭代器模式★(Iterator)

定义

提供一种方法顺序访问一个聚合对象中的各个元素,而又不用暴露该对象的内部表示。

模式结构

结构图

image-20231229140926871

实现代码

  • 抽象迭代器(Iterator)

    它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如用于获取第一个元素的first()方法、用于访问下一个元素的next()方法、用于判断是否还有下一个元素的hasNext()方法、用于获取当前元素的currentItem()方法等, 在具体迭代器中将实现这些方法。

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

    它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时游标通常是一个表示位置的非负整数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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() { ...... }
    }
  • 抽象聚合类(Aggregate)

    它用于存储和管理元素对象,声明一个createlterator() 方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。

    1
    2
    3
    public interface Aggregate {
    Iterator createIterator();
    }
  • 具体聚合类(ConcreteAggregate)

    它是抽象聚合类的子类,实现了在抽象聚合类中声明的createlterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器 ConcreteIterator 实例。

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

应用实例

某软件公司为某商场开发了一套销售管理系统,在对该系统进行分析和设计时,开发人员发现经常需要对系统中的商品数据、客户数据等进行遍历,为了复用这些遍历代码,开发人员设计了一个抽象的数据集合类AbstractObjectList,将存储商品和客户等数据的类作为其子类,AbstractObjectList类结构如下图所示:

AbstractObjectList类结构图

在图中,List类型的对象objects用于存储数据,其方法与说明如下表所示:

方法名 方法说明
AbstractObjectList() 构造方法,用于给objects对象赋值
addObject() 增加元素
removeObject() 删除元素
getObjects() 获取所有元素
next() 移至下一个元素
isLast() 判断当前元素是否是最后一个元素
previous() 移至上一个元素
isFirst() 判断当前元素是否是第一个元素
getNextItem() 获取下一个元素
getPreviousItem() 获取上一个元素

AbstractObjectList类的子类ProductList和CustomerList分别用于存储商品数据和客户数据。

通过分析,发现AbstractObjectList类的职责非常重,它既负责存储和管理数据,又负责遍历数据,违背了单一职责原则,实现代码将非常复杂。因此,开发人员决定使用迭代器模式对AbstractObjectList类进行重构,将负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,还可以给不同的具体数据集合类提供不同的遍历方式。

image-20231229143108928

  • 抽象聚合类:AbstractObjectList
  • 具体聚合类:ProductList
  • 抽象迭代器:AbstractIterator
  • 具体迭代器:ProductIterator

特点及使用环境

优点

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

缺点

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

适用环境

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

中介者模式✲

备忘录模式✲

观察者模式★(Observer)

定义

定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新。

模式结构

结构图

image-20231229143706930

实现代码

  • 目标(Subject)

    目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.util.*;
    public abstract class Subject {
    //定义一个观察者集合用于存储所有观察者对象
    protected ArrayList observers<Observer> = new ArrayList();
    //注册方法,用于向观察者集合中增加一个观察者
    public void attach(Observer observer) {
    observers.add(observer);
    }
    //注销方法,用于在观察者集合中删除一个观察者
    public void detach(Observer observer) {
    observers.remove(observer);
    }
    //声明抽象通知方法
    public abstract void notify();
    }
  • 具体目标(ConcreteSubject)

    具体目标是目标类的子类,它通常包含有经常发生改变的数据,当它的状态发生改变时将向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有)。如果无须扩展目标类,则具体目标类可以省略。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ConcreteSubject extends Subject {
    //实现通知方法
    public void notify() {
    //遍历观察者集合,调用每一个观察者的响应方法
    for(Object obs:observers) {
    ((Observer)obs).update();
    }
    }
    }
  • 观察者(Observer)

    观察者将对观察目标的改变作出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。

    1
    2
    3
    4
    public interface Observer {
    //声明响应方法
    public void update();
    }
  • 具体观察者(ConcreteObserver)

    在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中 删除。

    1
    2
    3
    4
    5
    6
    public class ConcreteObserver implements Observer {
    //实现响应方法
    public void update() {
    //具体响应代码
    }
    }

应用实例

在某多人联机对战游戏中,多个玩家可以加入同一战队组成联盟,当战队中的某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将做出响应。

现使用观察者模式设计并实现该过程,以实现战队成员之间的联动。

image-20231229144428657

  • 抽象目标类:AllyCOntrolCenter
  • 具体目标类:ConcreteAllyControlCenter
  • 抽象观察者:Observer
  • 具体观察者:Player

特点及使用环境

优点

  1. 可以实现表示层和数据逻辑层的分离
  2. 在观察目标和观察者之间建立一个抽象的耦合
  3. 支持广播通信,简化了一对多系统设计的难度
  4. 符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便

缺点

  1. 将所有的观察者都通知到会花费很多时间
  2. 如果存在循环依赖时可能导致系统崩溃
  3. 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化

适用环境

  1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用
  2. 一个对象的改变将导致一个或多个其他对象发生改变,且并不知道具体有多少对象将发生改变,也不知道这些对象是谁
  3. 需要在系统中创建一个触发链

状态模式☒

策略模式☒

模板方法模式☒

访问者模式✲