设计模式(3)行为型模式

行为型模式

行为模式负责对象间的高效沟通和职责委派。

image-20200906192336001

行为型模式:

  • 策略模式:将一组算法封装成一系列对象。通过传递这些对象可以灵活的改变程序的功能。比如说:java.util.Comparator#compare()
  • 模板方法模式:让子类可以重写方法的一部分,而不是整个重写,你可以控制子类需要重写那些操作。比如说:java.util.Collections#sort()
  • 观察者模式:它使得一个对象可以灵活的将消息发送给感兴趣的对象。比如说 java.util.EventListener
  • 迭代器模式:提供一个一致的方法来顺序访问集合中的对象,这个方法与底层的集合的具体实现无关。比如说:java.util.Iterator
  • 责任链模式:通过把请求从一个对象传递到链条中下一个对象的方式,直到请求被处理完毕,以实现对象间的解耦。比如说:java.util.logging.Logger#log()
  • 命令模式:将操作封装到对象内,以便存储,传递和返回。比如说:java.lang.Runnable
  • 备忘录模式:生成对象状态的一个快照,以便对象可以恢复原始状态而不用暴露自身的内容。比如说,Date 对象通过自身内部的一个 long 值来实现备忘录模式。
  • 状态模式:通过改变对象内部的状态,使得你可以在运行时动态改变一个对象的行为。比如说:java.util.Iterator
  • 访问者模式:提供一个方便的可维护的方式来操作一组对象。它使得你在不改变操作的对象前提下,可以修改或者扩展对象的行为。比如说:javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor
  • 中介者模式:通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖。比如说 java.util.concurrent.Executor#execute()
  • 解释器模式:定义了一个语言的语法,然后解析相应语法的语句。比如说:java.util.Pattern

责任链模式

意图

允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

image-20200906192845965

问题

假如你正在开发一个在线订购系统。 你希望对系统访问进行限制, 只允许认证用户创建订单。 此外, 拥有管理权限的用户也拥有所有订单的完全访问权限。

简单规划后, 你会意识到这些检查必须依次进行。 只要接收到包含用户凭据的请求, 应用程序就可尝试对进入系统的用户进行认证。 但如果由于用户凭据不正确而导致认证失败, 那就没有必要进行后续检查了。

image-20200906192906929

请求必须经过一系列检查后才能由订购系统来处理。

在接下来的几个月里, 你实现了后续的几个检查步骤。

  • 一位同事认为直接将原始数据传递给订购系统存在安全隐患。 因此你新增了额外的验证步骤来清理请求中的数据。
  • 过了一段时间, 有人注意到系统无法抵御暴力密码破解方式的攻击。 为了防范这种情况, 你立刻添加了一个检查步骤来过滤来自同一 IP 地址的重复错误请求。
  • 又有人提议你可以对包含同样数据的重复请求返回缓存中的结果, 从而提高系统响应速度。 因此, 你新增了一个检查步骤, 确保只有没有满足条件的缓存结果时请求才能通过并被发送给系统。

image-20200906192936725

代码变得越来越多, 也越来越混乱。

检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当你希望复用这些检查步骤来保护其他系统组件时, 你只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。

系统会变得让人非常费解, 而且其维护成本也会激增。 你在艰难地和这些代码共处一段时间后, 有一天终于决定对整个系统进行重构。

解决方案

与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。

模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。

最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

在我们的订购系统示例中, 处理者会在进行请求处理工作后决定是否继续沿着链传递请求。 如果请求中包含正确的数据, 所有处理者都将执行自己的主要行为, 无论该行为是身份验证还是数据缓存。

image-20200906193018550

处理者依次排列, 组成一条链。

不过还有一种稍微不同的方式 (也是更经典一种), 那就是处理者接收到请求后自行决定是否能够对其进行处理。 如果自己能够处理, 处理者就不再继续传递请求。 因此在这种情况下, 每个请求要么最多有一个处理者对其进行处理, 要么没有任何处理者对其进行处理。 在处理图形用户界面元素栈中的事件时, 这种方式非常常见。

例如, 当用户点击按钮时, 按钮产生的事件将沿着 GUI 元素链进行传递, 最开始是按钮的容器 (如窗体或面板), 直至应用程序主窗口。 链上第一个能处理该事件的元素会对其进行处理。 此外, 该例还有另一个值得我们关注的地方: 它表明我们总能从对象树中抽取出链来。

image-20200906193052641

对象树的枝干可以组成一条链。

所有处理者类均实现同一接口是关键所在。 每个具体处理者仅关心下一个包含 execute执行方法的处理者。 这样一来, 你就可以在运行时使用不同的处理者来创建链, 而无需将相关代码与处理者的具体类进行耦合。

真实世界类比

image-20200906193127891

给技术支持打电话时你可能得应对多名接听人员。

最近, 你刚为自己的电脑购买并安装了一个新的硬件设备。 身为一名极客, 你显然在电脑上安装了多个操作系统, 所以你会试着启动所有操作系统来确认其是否支持新的硬件设备。 Windows 检测到了该硬件设备并对其进行了自动启用。 但是你喜爱的 Linux 系统并不支持新硬件设备。 抱着最后一点希望, 你决定拨打包装盒上的技术支持电话。

首先你会听到自动回复器的机器合成语音, 它提供了针对各种问题的九个常用解决方案, 但其中没有一个与你遇到的问题相关。 过了一会儿, 机器人将你转接到人工接听人员处。

这位接听人员同样无法提供任何具体的解决方案。 他不断地引用手册中冗长的内容, 并不会仔细聆听你的回应。 在第 10 次听到 “你是否关闭计算机后重新启动呢?” 这句话后, 你要求与一位真正的工程师通话。

最后, 接听人员将你的电话转接给了工程师, 他或许正缩在某幢办公大楼的阴暗地下室中, 坐在他所深爱的服务器机房里, 焦躁不安地期待着同一名真人交流。 工程师告诉了你新硬件设备驱动程序的下载网址, 以及如何在 Linux 系统上进行安装。 问题终于解决了! 你挂断了电话, 满心欢喜。

模式结构

image-20200906193330088

处理者 (Handler) 声明了所有具体处理者的通用接口。 该接口通常仅包含单个方法用于请求处理, 但有时其还会包含一个设置链上下个处理者的方法。

基础处理者 (Base Handler) 是一个可选的类, 你可以将所有处理者共用的样本代码放置在其中。

通常情况下, 该类中定义了一个保存对于下个处理者引用的成员变量。 客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。 该类还可以实现默认的处理行为: 确定下个处理者存在后再将请求传递给它。

具体处理者 (Concrete Handlers) 包含处理请求的实际代码。 每个处理者接收到请求后, 都必须决定是否进行处理, 以及是否沿着链传递请求。

处理者通常是独立且不可变的, 需要通过构造函数一次性地获得所有必要地数据。

客户端 (Client) 可根据程序逻辑一次性或者动态地生成链。 值得注意的是, 请求可发送给链上的任意一个处理者, 而非必须是第一个处理者。

通用代码实现

public class ChainPatternDemo {
   
   public static void main(String[] args) {
      // 业务流程1
      Handler thirdHandler = new Handler3(null);
      Handler secondHandler = new Handler2(thirdHandler);
      Handler firstHandler = new Handler1(secondHandler);
      firstHandler.execute();
      
      // 业务流程2
      thirdHandler = new Handler3(null);
      secondHandler = new Handler1(thirdHandler);
      firstHandler = new Handler2(secondHandler);
      firstHandler.execute();
   }
   
   public static abstract class Handler {
      
      protected Handler successor;
      
      public Handler(Handler successor) {
         this.successor = successor;
      }
      
      public abstract void execute();
      
   }
   
   public static class Handler1 extends Handler {
      
      public Handler1(Handler successor) {
         super(successor);
      }
      
      public void execute() {
         System.out.println("执行功能1");  
         if(successor != null) {
            successor.execute();
         }
      }
      
   }
   
   public static class Handler2 extends Handler {
      
      public Handler2(Handler successor) {
         super(successor);
      }
      
      public void execute() {
         System.out.println("执行功能2");  
         if(successor != null) {
            successor.execute();
         }
      }
      
   }
   
   public static class Handler3 extends Handler {
      
      public Handler3(Handler successor) {
         super(successor);
      }
      
      public void execute() {
         System.out.println("执行功能3");  
         if(successor != null) {
            successor.execute();
         }
      }
      
   }
   
}

过滤访问

使用示例: 责任链模式在 Java 程序中并不常见, 因为它仅在代码与对象链打交道时才能发挥作用。

该模式最流行的使用案例之一是在 GUI 类中将事件向上传递给父组件。 另一个值得注意的使用案例是依次访问过滤器。

本例展示了包含用户数据的请求如何依次通过处理者链来执行各种不同的行为 (例如认证、 授权与验证)。

本例与许多作者给出的该模式的标准版本有些不同。 绝大多数模式示例都会寻找正确的处理者, 并在处理后退出链。 但在这里我们会执行每个处理者, 直至某个处理者无法处理请求。 请注意, 尽管流程略有不同, 但这仍是责任链模式。

基础验证接口

/**
 * Base middleware class.
 */
public abstract class Middleware {
    private Middleware next;

    /**
     * Builds chains of middleware objects.
     */
    public Middleware linkWith(Middleware next) {
        this.next = next;
        return next;
    }

    /**
     * Subclasses will implement this method with concrete checks.
     */
    public abstract boolean check(String email, String password);

    /**
     * Runs check on the next object in chain or ends traversing if we're in
     * last object in chain.
     */
    protected boolean checkNext(String email, String password) {
        if (next == null) {
            return true;
        }
        return next.check(email, password);
    }
}

检查请求数量限制

/**
 * ConcreteHandler. Checks whether there are too many failed login requests.
 */
public class ThrottlingMiddleware extends Middleware {
    private int requestPerMinute;
    private int request;
    private long currentTime;

    public ThrottlingMiddleware(int requestPerMinute) {
        this.requestPerMinute = requestPerMinute;
        this.currentTime = System.currentTimeMillis();
    }

    /**
     * Please, not that checkNext() call can be inserted both in the beginning
     * of this method and in the end.
     *
     * This gives much more flexibility than a simple loop over all middleware
     * objects. For instance, an element of a chain can change the order of
     * checks by running its check after all other checks.
     */
    public boolean check(String email, String password) {
        if (System.currentTimeMillis() > currentTime + 60_000) {
            request = 0;
            currentTime = System.currentTimeMillis();
        }

        request++;
        
        if (request > requestPerMinute) {
            System.out.println("Request limit exceeded!");
            Thread.currentThread().stop();
        }
        return checkNext(email, password);
    }
}

检查用户登录信息

/**
 * ConcreteHandler. Checks whether a user with the given credentials exists.
 */
public class UserExistsMiddleware extends Middleware {
    private Server server;

    public UserExistsMiddleware(Server server) {
        this.server = server;
    }

    public boolean check(String email, String password) {
        if (!server.hasEmail(email)) {
            System.out.println("This email is not registered!");
            return false;
        }
        if (!server.isValidPassword(email, password)) {
            System.out.println("Wrong password!");
            return false;
        }
        return checkNext(email, password);
    }
}

检查用户角色

/**
 * ConcreteHandler. Checks a user's role.
 */
public class RoleCheckMiddleware extends Middleware {
    public boolean check(String email, String password) {
        if (email.equals("admin@example.com")) {
            System.out.println("Hello, admin!");
            return true;
        }
        System.out.println("Hello, user!");
        return checkNext(email, password);
    }
}

授权目标

/**
 * Server class.
 */
public class Server {
    private Map<String, String> users = new HashMap<>();
    private Middleware middleware;

    /**
     * Client passes a chain of object to server. This improves flexibility and
     * makes testing the server class easier.
     */
    public void setMiddleware(Middleware middleware) {
        this.middleware = middleware;
    }

    /**
     * Server gets email and password from client and sends the authorization
     * request to the chain.
     */
    public boolean logIn(String email, String password) {
        if (middleware.check(email, password)) {
            System.out.println("Authorization have been successful!");

            // Do something useful here for authorized users.

            return true;
        }
        return false;
    }

    public void register(String email, String password) {
        users.put(email, password);
    }

    public boolean hasEmail(String email) {
        return users.containsKey(email);
    }

    public boolean isValidPassword(String email, String password) {
        return users.get(email).equals(password);
    }
}

客户端代码

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private static Server server;

    private static void init() {
        server = new Server();
        server.register("admin@example.com", "admin_pass");
        server.register("user@example.com", "user_pass");

        // All checks are linked. Client can build various chains using the same
        // components.
        Middleware middleware = new ThrottlingMiddleware(2);
        middleware.linkWith(new UserExistsMiddleware(server))
                .linkWith(new RoleCheckMiddleware());

        // Server gets a chain from client code.
        server.setMiddleware(middleware);
    }

    public static void main(String[] args) throws IOException {
        init();

        boolean success;
        do {
            System.out.print("Enter email: ");
            String email = reader.readLine();
            System.out.print("Input password: ");
            String password = reader.readLine();
            success = server.logIn(email, password);
        } while (!success);
    }
}

模式的应用场景

1、当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。

模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。

2、当必须按顺序执行多个处理者时, 可以使用该模式。

无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。

3、如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。

如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序。

职责链模式存在以下两种情况:

  • 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。
  • 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。

JDK 中的责任链模式示例

  • javax.servlet.Filter#doFilter()
  • java.util.logging.Logger#log()

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 责任链和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。

    责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。

命令模式

意图

它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

image-20200906194256296

问题

假如你正在开发一款新的文字编辑器, 当前的任务是创建一个包含多个按钮的工具栏, 并让每个按钮对应编辑器的不同操作。 你创建了一个非常简洁的 按钮类, 它不仅可用于生成工具栏上的按钮, 还可用于生成各种对话框的通用按钮。

image-20200906194401963

应用中的所有按钮都可以继承相同的类

尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。 你会在哪里放置这些按钮的点击处理代码呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。

image-20200906194424253

大量的按钮子类。 没关系的。

你很快就意识到这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。 简单来说, GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。

image-20200906194452995

多个类实现同一功能。

还有一个部分最难办。 复制/粘贴文字等操作可能会在多个地方被调用。 例如用户可以点击工具栏上小小的 “复制” 按钮, 或者通过上下文菜单复制一些内容, 又或者直接使用键盘上的 Ctrl+C 。

我们的程序最初只有工具栏, 因此可以使用按钮子类来实现各种不同操作。 换句话来说, 复制按钮 Copy­Button 子类包含复制文字的代码是可行的。 在实现了上下文菜单、 快捷方式和其他功能后, 你要么需要将操作代码复制进许多个类中, 要么需要让菜单依赖于按钮, 而后者是更糟糕的选择。

解决方案

优秀的软件设计通常会将关注点进行分离, 而这往往会导致软件的分层。 最常见的例子: 一层负责用户图像界面; 另一层负责业务逻辑。 GUI 层负责在屏幕上渲染美观的图形, 捕获所有输入并显示用户和程序工作的结果。 当需要完成一些重要内容时 (比如计算月球轨道或撰写年度报告), GUI 层则会将工作委派给业务逻辑底层。

这在代码中看上去就像这样: 一个 GUI 对象传递一些参数来调用一个业务逻辑对象。 这个过程通常被描述为一个对象发送请求给另一个对象。

image-20200906194543388

GUI 层可以直接访问业务逻辑层。

命令模式建议 GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。

命令对象负责连接不同的 GUI 和业务逻辑对象。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 GUI 对象触发命令即可, 命令对象会自行处理所有细节工作。

image-20200906194622856

通过命令访问业务逻辑层。

下一步是让所有命令实现相同的接口。 该接口通常只有一个没有任何参数的执行方法, 让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。 此外还有额外的好处, 现在你能在运行时切换连接至发送者的命令对象, 以此改变发送者的行为。

你可能会注意到遗漏的一块拼图——请求的参数。 GUI 对象可以给业务层对象提供一些参数。 但执行命令方法没有任何参数, 所以我们如何将请求的详情发送给接收者呢? 答案是: 使用数据对命令进行预先配置, 或者让其能够自行获取数据。

image-20200906194649989

GUI 对象将命令委派给命令对象。

让我们回到文本编辑器。 应用命令模式后, 我们不再需要任何按钮子类来实现点击行为。 我们只需在按钮 Button 基类中添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令即可。

你需要为每个可能的操作实现一系列命令类, 并且根据按钮所需行为将命令和按钮连接起来。

其他菜单、 快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。 当用户与 GUI 元素交互时, 与其连接的命令将会被执行。 现在你很可能已经猜到了, 与相同操作相关的元素将会被连接到相同的命令, 从而避免了重复代码。

最后, 命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。 而这仅仅是命令模式所提供的一小部分好处!

真实世界类比

image-20200906194730609

在餐厅里点餐。

在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。 服务员来到厨房, 把订单贴在墙上。 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。

那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。

模式结构

image-20200906194804932

发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。

命令 (Command) 接口通常仅声明一个执行命令的方法。

具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。

接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

通用代码实现

public class CommandPatternDemo {
   
   public static void main(String[] args) {
      Command commandA = new CommandA();
      Command commandB = new CommandB();
      
      Invoker invoker = new Invoker();
      
      invoker.setCommand(commandA); 
      invoker.execute();
      
      invoker.setCommand(commandB);
      invoker.execute();
   }
   
   public interface Command {
      void execute();
   }
   
   public static class CommandA implements Command {
      
      @Override
      public void execute() {
         System.out.println("命令A的功能逻辑");  
      }
      
   }
   
   public static class CommandB implements Command {
      
      @Override
      public void execute() {
         System.out.println("命令B的功能逻辑"); 
      }
      
   }
   
   public static class Invoker {
      
      private Command command;
      
      public Command getCommand() {
         return command;
      }

      public void setCommand(Command command) {
         this.command = command;
      }

      public void execute() {
         System.out.println("一些别的逻辑A");
         command.execute();
         System.out.println("一些别的逻辑B");
      } 
      
   }
   
}

文字编辑器和撤销

使用示例: 命令模式在 Java 代码中很常见。 大部分情况下, 它被用于代替包含行为的参数化 UI 元素的回调函数, 此外还被用于对任务进行排序和记录操作历史记录等。

本例中的文字编辑器在每次用户与其互动时, 都会创建一个新的命令对象。 命令执行其行为后会被压入历史堆栈。

现在, 当程序执行撤销操作时, 它就需要从历史记录中取出最近执行的命令, 然后执行反向操作或者恢复由该命令保存的编辑器历史状态。

抽象基础命令

public abstract class Command {
    public Editor editor;
    private String backup;

    Command(Editor editor) {
        this.editor = editor;
    }

    void backup() {
        backup = editor.textField.getText();
    }

    public void undo() {
        editor.textField.setText(backup);
    }

    public abstract boolean execute();
}

将所选文字复制到剪贴板

public class CopyCommand extends Command {

    public CopyCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        editor.clipboard = editor.textField.getSelectedText();
        return false;
    }
}

从剪贴板粘贴文字

public class PasteCommand extends Command {

    public PasteCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;

        backup();
        editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
        return true;
    }
}

将文字剪切到剪贴板

public class CutCommand extends Command {

    public CutCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.textField.getSelectedText().isEmpty()) return false;

        backup();
        String source = editor.textField.getText();
        editor.clipboard = editor.textField.getSelectedText();
        editor.textField.setText(cutString(source));
        return true;
    }

    private String cutString(String source) {
        String start = source.substring(0, editor.textField.getSelectionStart());
        String end = source.substring(editor.textField.getSelectionEnd());
        return start + end;
    }
}

命令历史

public class CommandHistory {
    private Stack<Command> history = new Stack<>();

    public void push(Command c) {
        history.push(c);
    }

    public Command pop() {
        return history.pop();
    }

    public boolean isEmpty() { return history.isEmpty(); }
}

文字编辑器的 GUI

public class Editor {
    public JTextArea textField;
    public String clipboard;
    private CommandHistory history = new CommandHistory();

    public void init() {
        JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
        JPanel content = new JPanel();
        frame.setContentPane(content);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        textField = new JTextArea();
        textField.setLineWrap(true);
        content.add(textField);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        JButton ctrlC = new JButton("Ctrl+C");
        JButton ctrlX = new JButton("Ctrl+X");
        JButton ctrlV = new JButton("Ctrl+V");
        JButton ctrlZ = new JButton("Ctrl+Z");
        Editor editor = this;
        ctrlC.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CopyCommand(editor));
            }
        });
        ctrlX.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CutCommand(editor));
            }
        });
        ctrlV.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new PasteCommand(editor));
            }
        });
        ctrlZ.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                undo();
            }
        });
        buttons.add(ctrlC);
        buttons.add(ctrlX);
        buttons.add(ctrlV);
        buttons.add(ctrlZ);
        content.add(buttons);
        frame.setSize(450, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void executeCommand(Command command) {
        if (command.execute()) {
            history.push(command);
        }
    }

    private void undo() {
        if (history.isEmpty()) return;

        Command command = history.pop();
        if (command != null) {
            command.undo();
        }
    }
}

客户端代码

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.init();
    }
}

模式的应用场景

1、如果你需要通过操作来参数化对象, 可使用命令模式。

命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。

举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。

2、如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。

同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。

3、如果你想要实现操作回滚功能, 可使用命令模式。

尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。

为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。

其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。

JDK 中的命令模式示例

  • java.lang.Runnable 的所有实现

  • javax.swing.Action 的所有实现

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 你可以同时使用命令和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。

    • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。

    • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。

  • 原型模式可用于保存命令的历史记录。

  • 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

迭代器模式

意图

让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。

image-20200906200207060

问题

集合是编程中最常使用的数据类型之一。 尽管如此, 集合只是一组对象的容器而已。

image-20200906200014104

各种类型的集合。

大部分集合使用简单列表存储元素。 但有些集合还会使用栈、 树、 图和其他复杂的数据结构。

无论集合的构成方式如何, 它都必须提供某种访问元素的方式, 便于其他代码使用其中的元素。 集合应提供一种能够遍历元素的方式, 且保证它不会周而复始地访问同一个元素。

如果你的集合基于列表, 那么这项工作听上去仿佛很简单。 但如何遍历复杂数据结构 (例如树) 中的元素呢? 例如, 今天你需要使用深度优先算法来遍历树结构, 明天可能会需要广度优先算法; 下周则可能会需要其他方式 (比如随机存取树中的元素)。

image-20200906200243195

可通过不同的方式遍历相同的集合。

不断向集合中添加遍历算法会模糊其 “高效存储数据” 的主要职责。 此外, 有些算法可能是根据特定应用订制的, 将其加入泛型集合类中会显得非常奇怪。

另一方面, 使用多种集合的客户端代码可能并不关心存储数据的方式。 不过由于集合提供不同的元素访问方式, 你的代码将不得不与特定集合类进行耦合。

解决方案

迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象。

image-20200906200327998

迭代器可实现多种遍历算法。 多个迭代器对象可同时遍历同一个集合。

除实现自身算法外, 迭代器还封装了遍历操作的所有细节, 例如当前位置和末尾剩余元素的数量。 因此, 多个迭代器可以在相互独立的情况下同时访问集合。

迭代器通常会提供一个获取集合元素的基本方法。 客户端可不断调用该方法直至它不返回任何内容, 这意味着迭代器已经遍历了所有元素。

所有迭代器必须实现相同的接口。 这样一来, 只要有合适的迭代器, 客户端代码就能兼容任何类型的集合或遍历算法。 如果你需要采用特殊方式来遍历集合, 只需创建一个新的迭代器类即可, 无需对集合或客户端进行修改。

真实世界类比

image-20200906200505113

漫步罗马的不同方式。

你计划在罗马游览数天, 参观所有主要的旅游景点。 但在到达目的地后, 你可能会浪费很多时间绕圈子, 甚至找不到罗马斗兽场在哪里。

或者你可以购买一款智能手机上的虚拟导游程序。 这款程序非常智能而且价格不贵, 你想在景点待多久都可以。

第三种选择是用部分旅行预算雇佣一位对城市了如指掌的当地向导。 向导能根据你的喜好来安排行程, 为你介绍每个景点并讲述许多激动人心的故事。 这样的旅行可能会更有趣, 但所需费用也会更高。

所有这些选择(自由漫步、 智能手机导航或真人向导)都是这个由众多罗马景点组成的集合的迭代器。

模式结构

image-20200906200627875

迭代器 (Iterator) 接口声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等。

具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭代器可以相互独立地遍历同一集合。

集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器。

具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的集合代码在什么地方呢? 不用担心, 它也会在同一个类中。 只是这些细节对于实际模式来说并不重要, 所以我们将其省略了而已。

客户端 (Client) 通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代码使用各种不同的集合和迭代器。

客户端通常不会自行创建迭代器, 而是会从集合中获取。 但在特定情况下, 客户端可以直接创建一个迭代器 (例如当客户端需要自定义特殊迭代器时)。

迭代访问社交网络档案

使用示例: 该模式在 Java 代码中很常见。 许多框架和程序库都会使用它来提供遍历其集合的标准方式。

在本例中, 迭代器模式被用于在不向客户端代码暴露通信细节的情况下访问远程社交网络集合中的社交媒体档案。

定义档案接口

public interface ProfileIterator {
    boolean hasNext();

    Profile getNext();

    void reset();
}

在 Facebook 档案上实现迭代

public class FacebookIterator implements ProfileIterator {
    private Facebook facebook;
    private String type;
    private String email;
    private int currentPosition = 0;
    private List<String> emails = new ArrayList<>();
    private List<Profile> profiles = new ArrayList<>();

    public FacebookIterator(Facebook facebook, String type, String email) {
        this.facebook = facebook;
        this.type = type;
        this.email = email;
    }

    private void lazyLoad() {
        if (emails.size() == 0) {
            List<String> profiles = facebook.requestProfileFriendsFromFacebook(this.email, this.type);
            for (String profile : profiles) {
                this.emails.add(profile);
                this.profiles.add(null);
            }
        }
    }

    @Override
    public boolean hasNext() {
        lazyLoad();
        return currentPosition < emails.size();
    }

    @Override
    public Profile getNext() {
        if (!hasNext()) {
            return null;
        }

        String friendEmail = emails.get(currentPosition);
        Profile friendProfile = profiles.get(currentPosition);
        if (friendProfile == null) {
            friendProfile = facebook.requestProfileFromFacebook(friendEmail);
            profiles.set(currentPosition, friendProfile);
        }
        currentPosition++;
        return friendProfile;
    }

    @Override
    public void reset() {
        currentPosition = 0;
    }
}

在领英档案上实现迭代

public class LinkedInIterator implements ProfileIterator {
    private LinkedIn linkedIn;
    private String type;
    private String email;
    private int currentPosition = 0;
    private List<String> emails = new ArrayList<>();
    private List<Profile> contacts = new ArrayList<>();

    public LinkedInIterator(LinkedIn linkedIn, String type, String email) {
        this.linkedIn = linkedIn;
        this.type = type;
        this.email = email;
    }

    private void lazyLoad() {
        if (emails.size() == 0) {
            List<String> profiles = linkedIn.requestRelatedContactsFromLinkedInAPI(this.email, this.type);
            for (String profile : profiles) {
                this.emails.add(profile);
                this.contacts.add(null);
            }
        }
    }

    @Override
    public boolean hasNext() {
        lazyLoad();
        return currentPosition < emails.size();
    }

    @Override
    public Profile getNext() {
        if (!hasNext()) {
            return null;
        }

        String friendEmail = emails.get(currentPosition);
        Profile friendContact = contacts.get(currentPosition);
        if (friendContact == null) {
            friendContact = linkedIn.requestContactInfoFromLinkedInAPI(friendEmail);
            contacts.set(currentPosition, friendContact);
        }
        currentPosition++;
        return friendContact;
    }

    @Override
    public void reset() {
        currentPosition = 0;
    }
}

定义通用的社交网络接口

public interface SocialNetwork {
    ProfileIterator createFriendsIterator(String profileEmail);

    ProfileIterator createCoworkersIterator(String profileEmail);
}

Facebook

public class Facebook implements SocialNetwork {
    private List<Profile> profiles;

    public Facebook(List<Profile> cache) {
        if (cache != null) {
            this.profiles = cache;
        } else {
            this.profiles = new ArrayList<>();
        }
    }

    public Profile requestProfileFromFacebook(String profileEmail) {
        // Here would be a POST request to one of the Facebook API endpoints.
        // Instead, we emulates long network connection, which you would expect
        // in the real life...
        simulateNetworkLatency();
        System.out.println("Facebook: Loading profile '" + profileEmail + "' over the network...");

        // ...and return test data.
        return findProfile(profileEmail);
    }

    public List<String> requestProfileFriendsFromFacebook(String profileEmail, String contactType) {
        // Here would be a POST request to one of the Facebook API endpoints.
        // Instead, we emulates long network connection, which you would expect
        // in the real life...
        simulateNetworkLatency();
        System.out.println("Facebook: Loading '" + contactType + "' list of '" + profileEmail + "' over the network...");

        // ...and return test data.
        Profile profile = findProfile(profileEmail);
        if (profile != null) {
            return profile.getContacts(contactType);
        }
        return null;
    }

    private Profile findProfile(String profileEmail) {
        for (Profile profile : profiles) {
            if (profile.getEmail().equals(profileEmail)) {
                return profile;
            }
        }
        return null;
    }

    private void simulateNetworkLatency() {
        try {
            Thread.sleep(2500);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public ProfileIterator createFriendsIterator(String profileEmail) {
        return new FacebookIterator(this, "friends", profileEmail);
    }

    @Override
    public ProfileIterator createCoworkersIterator(String profileEmail) {
        return new FacebookIterator(this, "coworkers", profileEmail);
    }

}

领英

public class LinkedIn implements SocialNetwork {
    private List<Profile> contacts;

    public LinkedIn(List<Profile> cache) {
        if (cache != null) {
            this.contacts = cache;
        } else {
            this.contacts = new ArrayList<>();
        }
    }

    public Profile requestContactInfoFromLinkedInAPI(String profileEmail) {
        // Here would be a POST request to one of the LinkedIn API endpoints.
        // Instead, we emulates long network connection, which you would expect
        // in the real life...
        simulateNetworkLatency();
        System.out.println("LinkedIn: Loading profile '" + profileEmail + "' over the network...");

        // ...and return test data.
        return findContact(profileEmail);
    }

    public List<String> requestRelatedContactsFromLinkedInAPI(String profileEmail, String contactType) {
        // Here would be a POST request to one of the LinkedIn API endpoints.
        // Instead, we emulates long network connection, which you would expect
        // in the real life.
        simulateNetworkLatency();
        System.out.println("LinkedIn: Loading '" + contactType + "' list of '" + profileEmail + "' over the network...");

        // ...and return test data.
        Profile profile = findContact(profileEmail);
        if (profile != null) {
            return profile.getContacts(contactType);
        }
        return null;
    }

    private Profile findContact(String profileEmail) {
        for (Profile profile : contacts) {
            if (profile.getEmail().equals(profileEmail)) {
                return profile;
            }
        }
        return null;
    }

    private void simulateNetworkLatency() {
        try {
            Thread.sleep(2500);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public ProfileIterator createFriendsIterator(String profileEmail) {
        return new LinkedInIterator(this, "friends", profileEmail);
    }

    @Override
    public ProfileIterator createCoworkersIterator(String profileEmail) {
        return new LinkedInIterator(this, "coworkers", profileEmail);
    }
}

社交档案

public class Profile {
    private String name;
    private String email;
    private Map<String, List<String>> contacts = new HashMap<>();

    public Profile(String email, String name, String... contacts) {
        this.email = email;
        this.name = name;

        // Parse contact list from a set of "friend:email@gmail.com" pairs.
        for (String contact : contacts) {
            String[] parts = contact.split(":");
            String contactType = "friend", contactEmail;
            if (parts.length == 1) {
                contactEmail = parts[0];
            }
            else {
                contactType = parts[0];
                contactEmail = parts[1];
            }
            if (!this.contacts.containsKey(contactType)) {
                this.contacts.put(contactType, new ArrayList<>());
            }
            this.contacts.get(contactType).add(contactEmail);
        }
    }

    public String getEmail() {
        return email;
    }

    public String getName() {
        return name;
    }

    public List<String> getContacts(String contactType) {
        if (!this.contacts.containsKey(contactType)) {
            this.contacts.put(contactType, new ArrayList<>());
        }
        return contacts.get(contactType);
    }
}

消息发送应用

public class SocialSpammer {
    public SocialNetwork network;
    public ProfileIterator iterator;

    public SocialSpammer(SocialNetwork network) {
        this.network = network;
    }

    public void sendSpamToFriends(String profileEmail, String message) {
        System.out.println("\nIterating over friends...\n");
        iterator = network.createFriendsIterator(profileEmail);
        while (iterator.hasNext()) {
            Profile profile = iterator.getNext();
            sendMessage(profile.getEmail(), message);
        }
    }

    public void sendSpamToCoworkers(String profileEmail, String message) {
        System.out.println("\nIterating over coworkers...\n");
        iterator = network.createCoworkersIterator(profileEmail);
        while (iterator.hasNext()) {
            Profile profile = iterator.getNext();
            sendMessage(profile.getEmail(), message);
        }
    }

    public void sendMessage(String email, String message) {
        System.out.println("Sent message to: '" + email + "'. Message body: '" + message + "'");
    }
}

客户端代码

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    public static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        System.out.println("Please specify social network to target spam tool (default:Facebook):");
        System.out.println("1. Facebook");
        System.out.println("2. LinkedIn");
        String choice = scanner.nextLine();

        SocialNetwork network;
        if (choice.equals("2")) {
            network = new LinkedIn(createTestProfiles());
        }
        else {
            network = new Facebook(createTestProfiles());
        }

        SocialSpammer spammer = new SocialSpammer(network);
        spammer.sendSpamToFriends("anna.smith@bing.com",
                "Hey! This is Anna's friend Josh. Can you do me a favor and like this post [link]?");
        spammer.sendSpamToCoworkers("anna.smith@bing.com",
                "Hey! This is Anna's boss Jason. Anna told me you would be interested in [link].");
    }

    public static List<Profile> createTestProfiles() {
        List<Profile> data = new ArrayList<Profile>();
        data.add(new Profile("anna.smith@bing.com", "Anna Smith", "friends:mad_max@ya.com", "friends:catwoman@yahoo.com", "coworkers:sam@amazon.com"));
        data.add(new Profile("mad_max@ya.com", "Maximilian", "friends:anna.smith@bing.com", "coworkers:sam@amazon.com"));
        data.add(new Profile("bill@microsoft.eu", "Billie", "coworkers:avanger@ukr.net"));
        data.add(new Profile("avanger@ukr.net", "John Day", "coworkers:bill@microsoft.eu"));
        data.add(new Profile("sam@amazon.com", "Sam Kitting", "coworkers:anna.smith@bing.com", "coworkers:mad_max@ya.com", "friends:catwoman@yahoo.com"));
        data.add(new Profile("catwoman@yahoo.com", "Liza", "friends:anna.smith@bing.com", "friends:sam@amazon.com"));
        return data;
    }
}

模式的应用场景

1、当集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。

迭代器封装了与复杂数据结构进行交互的细节, 为客户端提供多个访问集合元素的简单方法。 这种方式不仅对客户端来说非常方便, 而且能避免客户端在直接与集合交互时执行错误或有害的操作, 从而起到保护集合的作用。

2、使用该模式可以减少程序中重复的遍历代码。

重要迭代算法的代码往往体积非常庞大。 当这些代码被放置在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降低其可维护性。 因此, 将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。

3、如果你希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。

该模式为集合和迭代器提供了一些通用接口。 如果你在代码中使用了这些接口, 那么将其他实现了这些接口的集合和迭代器传递给它时, 它仍将可以正常运行。

JDK 中的迭代器模式示例

  • java.util.Iterator 的所有实现 (还有 java.util.Scanner)。

  • java.util.Enumeration的所有实现

与其他模式的关系

  • 你可以使用迭代器模式来遍历组合模式树。

  • 你可以同时使用工厂方法模式和迭代器来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。

  • 你可以同时使用备忘录模式和迭代器来获取当前迭代器的状态, 并且在需要的时候进行回滚。

  • 可以同时使用访问者模式和迭代器来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。

中介者模式

意图

能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

image-20200906202649905

问题

假如你有一个创建和修改客户资料的对话框, 它由各种控件组成, 例如文本框 (Text­Field)、 复选框 (Checkbox) 和按钮 (Button) 等。

image-20200906202839657

用户界面中各元素间的关系会随程序发展而变得混乱。

某些表单元素可能会直接进行互动。 例如, 选中 “我有一只狗” 复选框后可能会显示一个隐藏文本框用于输入狗狗的名字。 另一个例子是提交按钮必须在保存数据前校验所有输入内容。

image-20200906202936330

元素间存在许多关联。 因此, 对某些元素进行修改可能会影响其他元素。

如果直接在表单元素代码中实现业务逻辑, 你将很难在程序其他表单中复用这些元素类。 例如, 由于复选框类与狗狗的文本框相耦合, 所以将无法在其他表单中使用它。 你要么使用渲染资料表单时用到的所有类, 要么一个都不用。

解决方案

中介者模式建议你停止组件之间的直接交流并使其相互独立。 这些组件必须调用特殊的中介者对象, 通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合。

在资料编辑表单的例子中, 对话框 (Dialog) 类本身将作为中介者, 其很可能已知自己所有的子元素, 因此你甚至无需在该类中引入新的依赖关系。

image-20200906203042429

UI 元素必须通过中介者对象进行间接沟通。

绝大部分重要的修改都在实际表单元素中进行。 让我们想想提交按钮。 之前, 当用户点击按钮后, 它必须对所有表单元素数值进行校验。 而现在它的唯一工作是将点击事件通知给对话框。 收到通知后, 对话框可以自行校验数值或将任务委派给各元素。 这样一来, 按钮不再与多个表单元素相关联, 而仅依赖于对话框类。

你还可以为所有类型的对话框抽取通用接口, 进一步削弱其依赖性。 接口中将声明一个所有表单元素都能使用的通知方法, 可用于将元素中发生的事件通知给对话框。 这样一来, 所有实现了该接口的对话框都能使用这个提交按钮了。

采用这种方式, 中介者模式让你能在单个中介者对象中封装多个对象间的复杂关系网。 类所拥有的依赖关系越少, 就越易于修改、 扩展或复用。

真实世界类比

image-20200906203128857

飞行器驾驶员之间不会通过相互沟通来决定下一架降落的飞机。 所有沟通都通过控制塔台进行。

飞行器驾驶员们在靠近或离开空中管制区域时不会直接相互交流。 但他们会与飞机跑道附近, 塔台中的空管员通话。 如果没有空管员, 驾驶员就需要留意机场附近的所有飞机, 并与数十位飞行员组成的委员会讨论降落顺序。 那恐怕会让飞机坠毁的统计数据一飞冲天吧。

塔台无需管制飞行全程, 只需在航站区加强管控即可, 因为该区域的决策参与者数量对于飞行员来说实在太多了。

模式结构

image-20200906203202235

组件 (Component) 是各种包含业务逻辑的类。 每个组件都有一个指向中介者的引用, 该引用被声明为中介者接口类型。 组件不知道中介者实际所属的类, 因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。

中介者 (Mediator) 接口声明了与组件交流的方法, 但通常仅包括一个通知方法。 组件可将任意上下文 (包括自己的对象) 作为该方法的参数, 只有这样接收组件和发送者类之间才不会耦合。

具体中介者 (Concrete Mediator) 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚至有时会对其生命周期进行管理。

组件并不知道其他组件的情况。 如果组件内发生了重要事件, 它只能通知中介者。 中介者收到通知后能轻易地确定发送者, 这或许已足以判断接下来需要触发的组件了。

对于组件来说, 中介者看上去完全就是一个黑箱。 发送者不知道最终会由谁来处理自己的请求, 接收者也不知道最初是谁发出了请求。

通用代码实现

public class MediatorPatternDemo {
   
   public static void main(String[] args) {
      Mediator mediator = new Mediator();
      
      ModuleA moduleA = new ModuleA(mediator);
      ModuleB moduleB = new ModuleB(mediator);
      ModuleC moduleC = new ModuleC(mediator);
      
      moduleA.execute();  
      moduleB.execute();  
      moduleC.execute();
   }
   
   public static class Mediator {
      
      private ModuleA moduleA;
      private ModuleB moduleB;
      private ModuleC moduleC;
      
      public ModuleA getModuleA() {
         return moduleA;
      }
      public void setModuleA(ModuleA moduleA) {
         this.moduleA = moduleA;
      }
      public ModuleB getModuleB() {
         return moduleB;
      }
      public void setModuleB(ModuleB moduleB) {
         this.moduleB = moduleB;
      }
      public ModuleC getModuleC() {
         return moduleC;
      }
      public void setModuleC(ModuleC moduleC) {
         this.moduleC = moduleC;
      }
      
      public void moduleAInvoke() {
         moduleB.execute("模块A通知中介者");  
         moduleC.execute("模块A通知中介者"); 
      }
      
      public void moduleBInvoke() {
         moduleA.execute("模块B通知中介者");  
         moduleC.execute("模块B通知中介者"); 
      }
      
      public void moduleCInvoke() {
         moduleA.execute("模块C通知中介者");  
         moduleB.execute("模块C通知中介者"); 
      }
      
   }
   
   public static class ModuleA {
      
      private Mediator mediator;
      
      public ModuleA(Mediator mediator) {
         this.mediator = mediator;
         this.mediator.setModuleA(this);
      }
      
      public void execute() {
         mediator.moduleAInvoke();
      }
      
      public void execute(String invoker) {
         System.out.println(invoker + "在调用模块A的功能");
      }
      
   }
   
   public static class ModuleB {
      
      private Mediator mediator;
      
      public ModuleB(Mediator mediator) {
         this.mediator = mediator;
         this.mediator.setModuleB(this); 
      }
      
      public void execute() {
         mediator.moduleBInvoke();
      }
      
      public void execute(String invoker) {
         System.out.println(invoker + "在调用模块B的功能");
      }
      
   }
   
   public static class ModuleC {
      
      private Mediator mediator;
      
      public ModuleC(Mediator mediator) {
         this.mediator = mediator;
         this.mediator.setModuleC(this);  
      }
      
      public void execute() {
         mediator.moduleCInvoke(); 
      }
      
      public void execute(String invoker) {
         System.out.println(invoker + "在调用模块C的功能");
      }
      
   }
   
}

笔记程序

使用示例: 中介者模式在 Java 代码中最常用于帮助程序 GUI 组件之间的通信。 在 MVC 模式中, 控制器是中介者的同义词。

本例展示了如何将许多 GUI 元素组织起来, 使其在中介者的帮助下无需相互依赖就能合作。

Colleague classes

/**
 * Common component interface.
 */
public interface Component {
    void setMediator(Mediator mediator);
    String getName();
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class AddButton extends JButton implements Component {
    private Mediator mediator;

    public AddButton() {
        super("Add");
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void fireActionPerformed(ActionEvent actionEvent) {
        mediator.addNewNote(new Note());
    }

    @Override
    public String getName() {
        return "AddButton";
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class DeleteButton extends JButton  implements Component {
    private Mediator mediator;

    public DeleteButton() {
        super("Del");
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void fireActionPerformed(ActionEvent actionEvent) {
        mediator.deleteNote();
    }

    @Override
    public String getName() {
        return "DelButton";
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class Filter extends JTextField implements Component {
    private Mediator mediator;
    private ListModel listModel;

    public Filter() {}

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void processComponentKeyEvent(KeyEvent keyEvent) {
        String start = getText();
        searchElements(start);
    }

    public void setList(ListModel listModel) {
        this.listModel = listModel;
    }

    private void searchElements(String s) {
        if (listModel == null) {
            return;
        }

        if (s.equals("")) {
            mediator.setElementsList(listModel);
            return;
        }

        ArrayList<Note> notes = new ArrayList<>();
        for (int i = 0; i < listModel.getSize(); i++) {
            notes.add((Note) listModel.getElementAt(i));
        }
        DefaultListModel<Note> listModel = new DefaultListModel<>();
        for (Note note : notes) {
            if (note.getName().contains(s)) {
                listModel.addElement(note);
            }
        }
        mediator.setElementsList(listModel);
    }

    @Override
    public String getName() {
        return "Filter";
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
@SuppressWarnings("unchecked")
public class List extends JList implements Component {
    private Mediator mediator;
    private final DefaultListModel LIST_MODEL;

    public List(DefaultListModel listModel) {
        super(listModel);
        this.LIST_MODEL = listModel;
        setModel(listModel);
        this.setLayoutOrientation(JList.VERTICAL);
        Thread thread = new Thread(new Hide(this));
        thread.start();
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    public void addElement(Note note) {
        LIST_MODEL.addElement(note);
        int index = LIST_MODEL.size() - 1;
        setSelectedIndex(index);
        ensureIndexIsVisible(index);
        mediator.sendToFilter(LIST_MODEL);
    }

    public void deleteElement() {
        int index = this.getSelectedIndex();
        try {
            LIST_MODEL.remove(index);
            mediator.sendToFilter(LIST_MODEL);
        } catch (ArrayIndexOutOfBoundsException ignored) {}
    }

    public Note getCurrentElement() {
        return (Note)getSelectedValue();
    }

    @Override
    public String getName() {
        return "List";
    }

    private class Hide implements Runnable {
        private List list;

        Hide(List list) {
            this.list = list;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                if (list.isSelectionEmpty()) {
                    mediator.hideElements(true);
                } else {
                    mediator.hideElements(false);
                }
            }
        }
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class SaveButton extends JButton implements Component {
    private Mediator mediator;

    public SaveButton() {
        super("Save");
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void fireActionPerformed(ActionEvent actionEvent) {
        mediator.saveChanges();
    }

    @Override
    public String getName() {
        return "SaveButton";
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class TextBox extends JTextArea implements Component {
    private Mediator mediator;

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void processComponentKeyEvent(KeyEvent keyEvent) {
        mediator.markNote();
    }

    @Override
    public String getName() {
        return "TextBox";
    }
}
/**
 * Concrete components don't talk with each other. They have only one
 * communication channel–sending requests to the mediator.
 */
public class Title extends JTextField implements Component {
    private Mediator mediator;

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    protected void processComponentKeyEvent(KeyEvent keyEvent) {
        mediator.markNote();
    }

    @Override
    public String getName() {
        return "Title";
    }
}

定义通用的中介者接口

/**
 * Common mediator interface.
 */
public interface Mediator {
    void addNewNote(Note note);
    void deleteNote();
    void getInfoFromList(Note note);
    void saveChanges();
    void markNote();
    void clear();
    void sendToFilter(ListModel listModel);
    void setElementsList(ListModel list);
    void registerComponent(Component component);
    void hideElements(boolean flag);
    void createGUI();
}

具体中介者

/**
 * Concrete mediator. All chaotic communications between concrete components
 * have been extracted to the mediator. Now components only talk with the
 * mediator, which knows who has to handle a request.
 */
public class Editor implements Mediator {
    private Title title;
    private TextBox textBox;
    private AddButton add;
    private DeleteButton del;
    private SaveButton save;
    private List list;
    private Filter filter;

    private JLabel titleLabel = new JLabel("Title:");
    private JLabel textLabel = new JLabel("Text:");
    private JLabel label = new JLabel("Add or select existing note to proceed...");
  
    /**
     * Here the registration of components by the mediator.
     */
    @Override
    public void registerComponent(Component component) {
        component.setMediator(this);
        switch (component.getName()) {
            case "AddButton":
                add = (AddButton)component;
                break;
            case "DelButton":
                del = (DeleteButton)component;
                break;
            case "Filter":
                filter = (Filter)component;
                break;
            case "List":
                list = (List)component;
                this.list.addListSelectionListener(listSelectionEvent -> {
                    Note note = (Note)list.getSelectedValue();
                    if (note != null) {
                        getInfoFromList(note);
                    } else {
                        clear();
                    }
                });
                break;
            case "SaveButton":
                save = (SaveButton)component;
                break;
            case "TextBox":
                textBox = (TextBox)component;
                break;
            case "Title":
                title = (Title)component;
                break;
        }
    }

    /**
     * Various methods to handle requests from particular components.
     */
    @Override
    public void addNewNote(Note note) {
        title.setText("");
        textBox.setText("");
        list.addElement(note);
    }

    @Override
    public void deleteNote() {
        list.deleteElement();
    }

    @Override
    public void getInfoFromList(Note note) {
        title.setText(note.getName().replace('*', ' '));
        textBox.setText(note.getText());
    }

    @Override
    public void saveChanges() {
        try {
            Note note = (Note) list.getSelectedValue();
            note.setName(title.getText());
            note.setText(textBox.getText());
            list.repaint();
        } catch (NullPointerException ignored) {}
    }

    @Override
    public void markNote() {
        try {
            Note note = list.getCurrentElement();
            String name = note.getName();
            if (!name.endsWith("*")) {
                note.setName(note.getName() + "*");
            }
            list.repaint();
        } catch (NullPointerException ignored) {}
    }

    @Override
    public void clear() {
        title.setText("");
        textBox.setText("");
    }

    @Override
    public void sendToFilter(ListModel listModel) {
        filter.setList(listModel);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void setElementsList(ListModel list) {
        this.list.setModel(list);
        this.list.repaint();
    }

    @Override
    public void hideElements(boolean flag) {
        titleLabel.setVisible(!flag);
        textLabel.setVisible(!flag);
        title.setVisible(!flag);
        textBox.setVisible(!flag);
        save.setVisible(!flag);
        label.setVisible(flag);
    }

    @Override
    public void createGUI() {
        JFrame notes = new JFrame("Notes");
        notes.setSize(960, 600);
        notes.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        JPanel left = new JPanel();
        left.setBorder(new LineBorder(Color.BLACK));
        left.setSize(320, 600);
        left.setLayout(new BoxLayout(left, BoxLayout.Y_AXIS));
        JPanel filterPanel = new JPanel();
        filterPanel.add(new JLabel("Filter:"));
        filter.setColumns(20);
        filterPanel.add(filter);
        filterPanel.setPreferredSize(new Dimension(280, 40));
        JPanel listPanel = new JPanel();
        list.setFixedCellWidth(260);
        listPanel.setSize(320, 470);
        JScrollPane scrollPane = new JScrollPane(list);
        scrollPane.setPreferredSize(new Dimension(275, 410));
        listPanel.add(scrollPane);
        JPanel buttonPanel = new JPanel();
        add.setPreferredSize(new Dimension(85, 25));
        buttonPanel.add(add);
        del.setPreferredSize(new Dimension(85, 25));
        buttonPanel.add(del);
        buttonPanel.setLayout(new FlowLayout());
        left.add(filterPanel);
        left.add(listPanel);
        left.add(buttonPanel);
        JPanel right = new JPanel();
        right.setLayout(null);
        right.setSize(640, 600);
        right.setLocation(320, 0);
        right.setBorder(new LineBorder(Color.BLACK));
        titleLabel.setBounds(20, 4, 50, 20);
        title.setBounds(60, 5, 555, 20);
        textLabel.setBounds(20, 4, 50, 130);
        textBox.setBorder(new LineBorder(Color.DARK_GRAY));
        textBox.setBounds(20, 80, 595, 410);
        save.setBounds(270, 535, 80, 25);
        label.setFont(new Font("Verdana", Font.PLAIN, 22));
        label.setBounds(100, 240, 500, 100);
        right.add(label);
        right.add(titleLabel);
        right.add(title);
        right.add(textLabel);
        right.add(textBox);
        right.add(save);
        notes.setLayout(null);
        notes.getContentPane().add(left);
        notes.getContentPane().add(right);
        notes.setResizable(false);
        notes.setLocationRelativeTo(null);
        notes.setVisible(true);
    }
}

笔记类

/**
 * Note class.
 */
public class Note {
    private String name;
    private String text;

    public Note() {
        name = "New note";
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getName() {
        return name;
    }

    public String getText() {
        return text;
    }

    @Override
    public String toString() {
        return name;
    }
}

初始化代码

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    public static void main(String[] args) {
        Mediator mediator = new Editor();

        mediator.registerComponent(new Title());
        mediator.registerComponent(new TextBox());
        mediator.registerComponent(new AddButton());
        mediator.registerComponent(new DeleteButton());
        mediator.registerComponent(new SaveButton());
        mediator.registerComponent(new List(new DefaultListModel()));
        mediator.registerComponent(new Filter());

        mediator.createGUI();
    }
}

模式的应用场景

1、当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。

该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。

2、当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。

应用中介者模式后, 每个组件不再知晓其他组件的情况。 尽管这些组件无法直接交流, 但它们仍可通过中介者对象进行间接交流。 如果你希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。

3、如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。

由于所有组件间关系都被包含在中介者中, 因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。

JDK 中介者模式的示例

  • java.util.Timer (所有 schedule­XXX() 方法)
  • java.util.concurrent.Executor#execute()
  • java.util.concurrent.ExecutorService ( invoke­XXX() 和 submit­() 方法)
  • java.util.concurrent.ScheduledExecutorService (所有 schedule­XXX() 方法)
  • java.lang.reflect.Method#invoke()

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 外观模式和中介者的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。

    • 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。

    中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。

    有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。

    当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。

    假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

备忘录模式

意图

允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

image-20200906204124320

问题

假如你正在开发一款文字编辑器应用程序。 除了简单的文字编辑功能外, 编辑器中还要有设置文本格式和插入内嵌图片等功能。

后来, 你决定让用户能撤销施加在文本上的任何操作。 这项功能在过去几年里变得十分普遍, 因此用户期待任何程序都有这项功能。 你选择采用直接的方式来实现该功能: 程序在执行任何操作前会记录所有的对象状态, 并将其保存下来。 当用户此后需要撤销某个操作时, 程序将从历史记录中获取最近的快照, 然后使用它来恢复所有对象的状态。

image-20200906205208107

程序在执行操作前保存所有对象的状态快照, 稍后可通过快照将对象恢复到之前的状态。

让我们来思考一下这些状态快照。 首先, 到底该如何生成一个快照呢? 很可能你会需要遍历对象的所有成员变量并将其数值复制保存。 但只有当对象对其内容没有严格访问权限限制的情况下, 你才能使用该方式。 不过很遗憾, 绝大部分对象会使用私有成员变量来存储重要数据, 这样别人就无法轻易查看其中的内容。

现在我们暂时忽略这个问题, 假设对象都像嬉皮士一样: 喜欢开放式的关系并会公开其所有状态。 尽管这种方式能够解决当前问题, 让你可随时生成对象的状态快照, 但这种方式仍存在一些严重问题。 未来你可能会添加或删除一些成员变量。 这听上去很简单, 但需要对负责复制受影响对象状态的类进行更改。

image-20200906205242064

如何复制对象的私有状态?

还有更多问题。 让我们来考虑编辑器 (Editor) 状态的实际 “快照”, 它需要包含哪些数据? 至少必须包含实际的文本、 光标坐标和当前滚动条位置等。 你需要收集这些数据并将其放入特定容器中, 才能生成快照。

你很可能会将大量的容器对象存储在历史记录列表中。 这样一来, 容器最终大概率会成为同一个类的对象。 这个类中几乎没有任何方法, 但有许多与编辑器状态一一对应的成员变量。 为了让其他对象能保存或读取快照, 你很可能需要将快照的成员变量设为公有。 无论这些状态是否私有, 其都将暴露一切编辑器状态。 其他类会对快照类的每个小改动产生依赖, 除非这些改动仅存在于私有成员变量或方法中, 而不会影响外部类。

我们似乎走进了一条死胡同: 要么会暴露类的所有内部细节而使其过于脆弱; 要么会限制对其状态的访问权限而无法生成快照。 那么, 我们还有其他方式来实现 “撤销” 功能吗?

解决方案

我们刚才遇到的所有问题都是封装 “破损” 造成的。 一些对象试图超出其职责范围的工作。 由于在执行某些行为时需要获取数据, 所以它们侵入了其他对象的私有空间, 而不是让这些对象来完成实际的工作。

备忘录模式将创建状态快照 (Snapshot) 的工作委派给实际状态的拥有者原发器 (Originator) 对象。 这样其他对象就不再需要从 “外部” 复制编辑器状态了, 编辑器类拥有其状态的完全访问权, 因此可以自行生成快照。

模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 除了创建备忘录的对象外, 任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互, 它们可以获取快照的元数据 (创建时间和操作名称等), 但不能获取快照中原始对象的状态。

image-20200906205318999

原发器拥有对备忘录的完全访问权限, 负责人则只能访问元数据。

这种限制策略允许你将备忘录保存在通常被称为负责人 (Caretakers) 的对象中。 由于负责人仅通过受限接口与备忘录互动, 故其无法修改存储在备忘录内部的状态。 同时, 原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复其以前的状态。

在文字编辑器的示例中, 我们可以创建一个独立的历史 (History) 类作为负责人。 编辑器每次执行操作前, 存储在负责人中的备忘录栈都会生长。 你甚至可以在应用的 UI 中渲染该栈, 为用户显示之前的操作历史。

当用户触发撤销操作时, 历史类将从栈中取回最近的备忘录, 并将其传递给编辑器以请求进行回滚。 由于编辑器拥有对备忘录的完全访问权限, 因此它可以使用从备忘录中获取的数值来替换自身的状态。

模式结构

基于嵌套类的实现

该模式的经典实现方式依赖于许多流行编程语言 (例如 C++、 C# 和 Java) 所支持的嵌套类。

image-20200906205357659

原发器 (Originator) 类可以生成自身状态的快照, 也可以在需要时通过快照恢复自身状态。

备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据。

负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何时恢复状态。

负责人通过保存备忘录栈来记录原发器的历史状态。 当原发器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复 (restoration) 方法。

在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发器就可访问备忘录的成员变量和方法, 即使这些方法被声明为私有。 另一方面, 负责人对于备忘录的成员变量和方法的访问权限非常有限: 它们只能在栈中保存备忘录, 而不能修改其状态。

封装更加严格的实现

如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式。

image-20200906205445706

这种实现方式允许存在多种不同类型的原发器和备忘录。 每种原发器都和其相应的备忘录类进行交互。 原发器和备忘录都不会将其状态暴露给其他类。

负责人此时被明确禁止修改存储在备忘录中的状态。 但负责人类将独立于原发器, 因为此时恢复方法被定义在了备忘录类中。

每个备忘录将与创建了自身的原发器连接。 原发器会将自己及状态传递给备忘录的构造函数。 由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能恢复其状态。

通用代码实现

public class MementoPatternDemo {
   
   public static void main(String[] args) {
      Originator originator = new Originator();
        // 准备好了中间数据
        originator.prepare();
        // 将中间数据保存到备忘录中去
        Memento memento = originator.createMemento();
        // 将备忘录保存到备忘录管理器中去
        Caretaker caretaker = new Caretaker();
        caretaker.saveMemento(memento);
        // 基于中间数据执行了A方法,但是此时中间数据已经改变了
        originator.executeA();
        // 从备忘录管理器中获取备忘录
        memento = caretaker.retrieveMemento();
        // 将备忘录中保存好的中间数据重新设置到原发器中去,就将中间数据恢复成了之前备忘的状态
        originator.setMemento(memento);
        // 接着再次执行方法B
        originator.executeB();
   }
   
   public interface Memento {
      
   }
   
   public static class Originator {
       
       private String state;
       
       public void prepare() {
           this.state = "中间数据";
       }
       
       public void executeA() {
           System.out.println("基于中间数据【" + state +"】执行了A方法的逻辑");
           // 将state所代表的中间数据做出了修改
           state += ",A方法的结果数据";
       }
       
       public void executeB() {
           System.out.println("基于中间数据【" + state +"】执行了B方法的逻辑");
           state += ",B方法的结果数据";
       }
       
       public Memento createMemento() {
           return new MementoImpl(state);
       }
       
       public void setMemento(Memento memento) {
           MementoImpl mementoImpl = (MementoImpl)memento;
           this.state = mementoImpl.getState();
       }
       
       private static class MementoImpl implements Memento {
           
           private String state;
           
           public MementoImpl(String state) {
               this.state = state;
           }
           
           public String getState() {
               return state;
           }
           
       }
       
   }
   
   public static class Caretaker {
       
       private Memento memento;
       
       public void saveMemento(Memento memento) {
           this.memento = memento;
       }
       
       public Memento retrieveMemento() {
           return this.memento;
       }
       
   }
   
}

状态的更新

public class MementoPattern {
    public static void main(String[] args) {
        Originator or = new Originator();
        Caretaker cr = new Caretaker();
        or.setState("S0");
        System.out.println("初始状态:" + or.getState());
        cr.setMemento(or.createMemento()); //保存状态      
        or.setState("S1");
        System.out.println("新的状态:" + or.getState());
        or.restoreMemento(cr.getMemento()); //恢复状态
        System.out.println("恢复状态:" + or.getState());
    }
}

//备忘录
class Memento {
    private String state;

    public Memento(String state) {
        this.state = state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}

//发起人
class Originator {
    private String state;

    public void setState(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    public Memento createMemento() {
        return new Memento(state);
    }

    public void restoreMemento(Memento m) {
        this.setState(m.getState());
    }
}

//管理者
class Caretaker {
    private Memento memento;

    public void setMemento(Memento m) {
        memento = m;
    }

    public Memento getMemento() {
        return memento;
    }
}

输出结果:

初始状态:S0
新的状态:S1
恢复状态:S0

模式的应用场景

1、当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。

备忘录模式允许你复制对象中的全部状态 (包括私有成员变量), 并将其独立于对象进行保存。 尽管大部分人因为 “撤销” 这个用例才记得该模式, 但其实它在处理事务 (比如需要在出现错误时回滚一个操作) 的过程中也必不可少。

2、当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

备忘录让对象自行负责创建其状态的快照。 任何其他对象都不能读取快照, 这有效地保障了数据的安全性。

JDK 中的备忘录模式示例

  • 所有 java.io.Serializable 的实现都可以模拟备忘录。
  • 所有 javax.faces.component.StateHolder 的实现。

与其他模式的关系

  • 你可以同时使用命令模式和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。

  • 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

观察者模式

允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

image-20200906210604469

问题

假如你有两种类型的对象: 顾客和商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

image-20200906211006352

前往商店和发送垃圾邮件

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。

image-20200906211049137

订阅机制允许对象订阅事件通知。

现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

image-20200906211124210

发布者调用订阅者对象中的特定通知方法来通知订阅者。

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有订阅者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

真实世界类比

image-20200906211157427

杂志和报纸订阅。

如果你订阅了一份杂志或报纸, 那就不需要再去报摊查询新出版的刊物了。 出版社 (即应用中的 “发布者”) 会在刊物出版后 (甚至提前) 直接将最新一期寄送至你的邮箱中。

出版社负责维护订阅者列表, 了解订阅者对哪些刊物感兴趣。 当订阅者希望出版社停止寄送新一期的杂志时, 他们可随时从该列表中退出。观察者模式结构

模式结构

image-20200906211239211

发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。

当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。

订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update更新方法。 该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。

具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。

订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。

客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

通用代码实现

public class ObserverPatternDemo {
   
   public static void main(String[] args) {
      Subject subject = new Subject(0); 
      
      Observer observer = new ConcreteObserver();
      subject.addObserver(observer); 
      
      subject.setState(1);
      subject.setState(2);  
   }
   
   public static class Subject extends Observable {
      
      private Integer state;
      
      public Subject(Integer state) {
         this.state = state;
      }
      
      public Integer getState() {
         return state;
      }
      public void setState(Integer state) {
         // 在这里状态就改变了
         this.state = state;
         // 通知关联的一些观察者,说我的状态变化了
         this.setChanged();
         this.notifyObservers();
      }
      
   }
   
   public static class ConcreteObserver implements Observer {

      @Override
      public void update(Observable o, Object arg) {
         Subject subject = (Subject) o;
         Integer state = subject.getState();
         System.out.println("目标对象的状态变化成:" + state);  
      }
      
   }
   
}

事件订阅

使用示例: 观察者模式在 Java 代码中很常见, 特别是在 GUI 组件中。 它提供了在不与其他对象所属类耦合的情况下对其事件做出反应的方式。

在本例中, 观察者模式在文本编辑器的对象之间建立了间接的合作关系。 每当编辑器(Editor) 对象改变时, 它都会通知其订阅者。 邮件通知监听器 (Email­Notification­Listener) 和日志开启监听器(Log­Open­Listener) 都将通过执行其基本行为来对这些通知做出反应。

订阅者类不与编辑器类相耦合, 且能在需要时在其他应用中复用。 编辑器类仅依赖于抽象订阅者接口。 这样就能允许在不改变编辑器代码的情况下添加新的订阅者类型。

基础发布者

public class EventManager {
    Map<String, List<EventListener>> listeners = new HashMap<>();

    public EventManager(String... operations) {
        for (String operation : operations) {
            this.listeners.put(operation, new ArrayList<>());
        }
    }

    public void subscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    public void notify(String eventType, File file) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.update(eventType, file);
        }
    }
}

具体发布者, 由其他对象追踪

public class Editor {
    public EventManager events;
    private File file;

    public Editor() {
        this.events = new EventManager("open", "save");
    }

    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }

    public void saveFile() throws Exception {
        if (this.file != null) {
            events.notify("save", file);
        } else {
            throw new Exception("Please open a file first.");
        }
    }
}

收到通知后发送邮件

public class EmailNotificationListener implements EventListener {
    private String email;

    public EmailNotificationListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

收到通知后在日志中记录一条消息

public class LogOpenListener implements EventListener {
    private File log;

    public LogOpenListener(String fileName) {
        this.log = new File(fileName);
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

初始化代码

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
        editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

        try {
            editor.openFile("test.txt");
            editor.saveFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

观察者模式的应用场景

1、当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。

当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。

观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。

2、 当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。

订阅列表是动态的, 因此订阅者可随时加入或离开该列表。

JDK 中的观察者模式示例

  • java.util.Observer/ java.util.Observable
  • java.util.EventListener 的所有实现
  • javax.servlet.http.HttpSessionBindingListener
  • javax.servlet.http.HttpSessionAttributeListener
  • javax.faces.event.PhaseListener

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。

    中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。

    有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。

    当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。

    假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

状态模式

意图

让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

image-20200906212155430

问题

状态模式与有限状态机的概念紧密相关。

image-20200906212255792

有限状态机。

其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。

你还可将该方法应用在对象上。 假如你有一个 文档 Document 类。 文档可能会处于 草稿 Draft 、 审阅中 Moderation 和已发布 Published 三种状态中的一种。 文档的 publish 发布方法在不同状态下的行为略有不同:

  • 处于草稿状态时, 它会将文档转移到审阅中状态。
  • 处于审阅中状态时, 如果当前用户是管理员, 它会公开发布文档。
  • 处于已发布状态时, 它不会进行任何操作。

image-20200906212413259

文档对象的全部状态和转移。

状态机通常由众多条件运算符 ( if 或 switch) 实现, 可根据对象的当前状态选择相应的行为。 “状态” 通常只是对象中的一组成员变量值。 即使你之前从未听说过有限状态机, 你也很可能已经实现过状态模式。

当我们逐步在 文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。

这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。

解决方案

状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。

原始对象被称为上下文(context), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。

image-20200906212518400

文档将工作委派给一个状态对象。

如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。 采用这种方式是有前提的: 所有状态类都必须遵循同样的接口, 而且上下文必须仅通过接口与这些对象进行交互。

这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。

真实世界类比

智能手机的按键和开关会根据设备当前状态完成不同行为:

  • 当手机处于解锁状态时, 按下按键将执行各种功能。
  • 当手机处于锁定状态时, 按下任何按键都将解锁屏幕。
  • 当手机电量不足时, 按下任何按键都将显示充电页面。

模式结构

image-20200906212617968

上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。

状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。

具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。

状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。

上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。

通用代码实现

public class StatePatternDemo {
   
   public static void main(String[] args) {
      Context context = new Context(new NewState());
      context.execute(1); 
      context.execute(2); 
      context.execute(3);
   }
   
   public interface State {
      void execute();
   }
   
   public static class NewState implements State {
      @Override
        public void execute() {
         System.out.println("执行销售出库单新建状态的逻辑");  
      }
   }
   
   public static class ApprovingState implements State {
      @Override
        public void execute() {
         System.out.println("执行销售出库单待审批状态的逻辑");  
      }
   }
   
   public static class ApprovedState implements State {
      @Override
        public void execute() {
         System.out.println("执行销售出库单已审批状态的逻辑");  
      }
   }
   
   public static class FinishedState implements State {
      @Override
        public void execute() {
         System.out.println("执行销售出库单已完成状态的逻辑");  
      }
   }
   
   public static class Context {
      
      private State state;
      
      public Context(State state) {
         this.state = state;
         this.state.execute();
      }
      
      public void execute(int stateType) {
         if(stateType == 1) {
            this.state = new ApprovingState();
            this.state.execute();
         } else if(stateType == 2) {
            this.state = new ApprovedState();
            this.state.execute();
         } else if(stateType == 3) {
            this.state = new FinishedState();
            this.state.execute();
         }
      }
      
   }
   
}

媒体播放器的接口

使用示例: 在 Java 语言中, 状态模式通常被用于将基于 switch 语句的大型状态机转换为对象。

在本例中, 状态模式允许媒体播放器根据当前的回放状态进行不同的控制行为。 播放器主类包含一个指向状态对象的引用, 它将完成播放器的绝大部分工作。 某些行为可能会用一个状态对象替换另一个状态对象, 改变播放器对用户交互的回应方式。

通用状态接口

/**
 * Common interface for all states.
 */
public abstract class State {
    Player player;

    /**
     * Context passes itself through the state constructor. This may help a
     * state to fetch some useful context data if needed.
     */
    State(Player player) {
        this.player = player;
    }

    public abstract String onLock();
    public abstract String onPlay();
    public abstract String onNext();
    public abstract String onPrevious();
}
/**
 * Concrete states provide the special implementation for all interface methods.
 */
public class LockedState extends State {

    LockedState(Player player) {
        super(player);
        player.setPlaying(false);
    }

    @Override
    public String onLock() {
        if (player.isPlaying()) {
            player.changeState(new ReadyState(player));
            return "Stop playing";
        } else {
            return "Locked...";
        }
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Ready";
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }
}
/**
 * They can also trigger state transitions in the context.
 */
public class ReadyState extends State {

    public ReadyState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        return "Locked...";
    }

    @Override
    public String onPlay() {
        String action = player.startPlayback();
        player.changeState(new PlayingState(player));
        return action;
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }
}
public class PlayingState extends State {

    PlayingState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        player.setCurrentTrackAfterStop();
        return "Stop playing";
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Paused...";
    }

    @Override
    public String onNext() {
        return player.nextTrack();
    }

    @Override
    public String onPrevious() {
        return player.previousTrack();
    }
}

播放器的主要代码

public class Player {
    private State state;
    private boolean playing = false;
    private List<String> playlist = new ArrayList<>();
    private int currentTrack = 0;

    public Player() {
        this.state = new ReadyState(this);
        setPlaying(true);
        for (int i = 1; i <= 12; i++) {
            playlist.add("Track " + i);
        }
    }

    public void changeState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setPlaying(boolean playing) {
        this.playing = playing;
    }

    public boolean isPlaying() {
        return playing;
    }

    public String startPlayback() {
        return "Playing " + playlist.get(currentTrack);
    }

    public String nextTrack() {
        currentTrack++;
        if (currentTrack > playlist.size() - 1) {
            currentTrack = 0;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public String previousTrack() {
        currentTrack--;
        if (currentTrack < 0) {
            currentTrack = playlist.size() - 1;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public void setCurrentTrackAfterStop() {
        this.currentTrack = 0;
    }
}

播放器的 GUI

public class UI {
    private Player player;
    private static JTextField textField = new JTextField();

    public UI(Player player) {
        this.player = player;
    }

    public void init() {
        JFrame frame = new JFrame("Test player");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel context = new JPanel();
        context.setLayout(new BoxLayout(context, BoxLayout.Y_AXIS));
        frame.getContentPane().add(context);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        context.add(textField);
        context.add(buttons);

        // Context delegates handling user's input to a state object. Naturally,
        // the outcome will depend on what state is currently active, since all
        // states can handle the input differently.
        JButton play = new JButton("Play");
        play.addActionListener(e -> textField.setText(player.getState().onPlay()));
        JButton stop = new JButton("Stop");
        stop.addActionListener(e -> textField.setText(player.getState().onLock()));
        JButton next = new JButton("Next");
        next.addActionListener(e -> textField.setText(player.getState().onNext()));
        JButton prev = new JButton("Prev");
        prev.addActionListener(e -> textField.setText(player.getState().onPrevious()));
        frame.setVisible(true);
        frame.setSize(300, 100);
        buttons.add(play);
        buttons.add(stop);
        buttons.add(next);
        buttons.add(prev);
    }
}

初始化代码

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    public static void main(String[] args) {
        Player player = new Player();
        UI ui = new UI(player);
        ui.init();
    }
}

模式的应用场景

1、如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。

模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。

2、如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。

状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。

3、当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。

状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。

JDK 中的状态模式示例

  • javax.faces.lifecycle.LifeCycle#execute() (由 Faces­Servlet控制: 行为依赖于当前 JSF 生命周期的阶段 (状态))

与其他模式的关系

  • 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

  • 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

策略模式

意图

它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。

image-20200906213422659

问题

一天, 你打算为游客们创建一款导游程序。 该程序的核心功能是提供美观的地图, 以帮助用户在任何城市中快速定位。

用户期待的程序新功能是自动路线规划: 他们希望输入地址后就能在地图上看到前往目的地的最快路线。

程序的首个版本只能规划公路路线。 驾车旅行的人们对此非常满意。 但很显然, 并非所有人都会在度假时开车。 因此你在下次更新时添加了规划步行路线的功能。 此后, 你又添加了规划公共交通路线的功能。

而这只是个开始。 不久后, 你又要为骑行者规划路线。 又过了一段时间, 你又要为游览城市中的所有景点规划路线。

image-20200906213531022

导游代码将变得非常臃肿。

尽管从商业角度来看, 这款应用非常成功, 但其技术部分却让你非常头疼: 每次添加新的路线规划算法后, 导游应用中主要类的体积就会增加一倍。 终于在某个时候, 你觉得自己没法继续维护这堆代码了。

无论是修复简单缺陷还是微调街道权重, 对某个算法进行任何修改都会影响整个类, 从而增加在已有正常运行代码中引入错误的风险。

此外, 团队合作将变得低效。 如果你在应用成功发布后招募了团队成员, 他们会抱怨在合并冲突的工作上花费了太多时间。 在实现新功能的过程中, 你的团队需要修改同一个巨大的类, 这样他们所编写的代码相互之间就可能会出现冲突。

解决方案

策略模式建议找出负责用许多不同方式完成特定任务的类, 然后将其中的算法抽取到一组被称为策略的独立类中。

名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用。 上下文并不执行任务, 而是将工作委派给已连接的策略对象。

上下文不负责选择符合任务需要的算法——客户端会将所需策略传递给上下文。 实际上, 上下文并不十分了解策略, 它会通过同样的通用接口与所有策略进行交互, 而该接口只需暴露一个方法来触发所选策略中封装的算法即可。

因此, 上下文可独立于具体策略。 这样你就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。

image-20200906213604693

路线规划策略。

在导游应用中, 每个路线规划算法都可被抽取到只有一个 build­Route 生成路线方法的独立类中。 该方法接收起点和终点作为参数, 并返回路线中途点的集合。

即使传递给每个路径规划类的参数一模一样, 其所创建的路线也可能完全不同。 主要导游类的主要工作是在地图上渲染一系列中途点, 不会在意如何选择算法。 该类中还有一个用于切换当前路径规划策略的方法, 因此客户端 (例如用户界面中的按钮) 可用其他策略替换当前选择的路径规划行为。

真实世界类比

image-20200906213643742

各种前往机场的出行策略

假如你需要前往机场。 你可以选择乘坐公共汽车、 预约出租车或骑自行车。 这些就是你的出行策略。 你可以根据预算或时间等因素来选择其中一种策略。

模式结构

image-20200906213712302

上下文 (Context) 维护指向具体策略的引用, 且仅通过策略接口与该对象进行交流。

策略 (Strategy) 接口是所有具体策略的通用接口, 它声明了一个上下文用于执行策略的方法。

具体策略 (Concrete Strategies) 实现了上下文所用算法的各种不同变体。

当上下文需要运行算法时, 它会在其已连接的策略对象上调用执行方法。 上下文不清楚其所涉及的策略类型与算法的执行方式。

客户端 (Client) 会创建一个特定策略对象并将其传递给上下文。 上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。

通用代码实现

public class StrategryPatternDemo {

   public static void main(String[] args) {
      int discountStyle = 1;
      
      DiscountCalculateStrategy strategy = DiscountCalculateStrategryFactory
            .getDiscountCalculateStrategy(discountStyle);
      
      Context context = new Context();
      context.setStrategy(strategy); 
      context.calculate();
   }
   
   public interface DiscountCalculateStrategy {
      void calculate();
   }
   
   public static class DiscountCalculateStrategyA implements DiscountCalculateStrategy {
      @Override
      public void calculate() {
         System.out.println("执行优惠计价方式1的复杂业务逻辑"); 
      }
   }
   
   public static class DiscountCalculateStrategyB implements DiscountCalculateStrategy {
      @Override
      public void calculate() {
         System.out.println("执行优惠计价方式2的复杂业务逻辑"); 
      }
   }
   
   public static class DiscountCalculateStrategyC implements DiscountCalculateStrategy {
      @Override
      public void calculate() {
         System.out.println("执行优惠计价方式3的复杂业务逻辑"); 
      }
   }
   
   public static class DiscountCalculateStrategyDefault implements DiscountCalculateStrategy {
      @Override
      public void calculate() {
         System.out.println("执行默认的优惠计价方式的复杂业务逻辑"); 
      }
   }
   
   public static class DiscountCalculateStrategryFactory {
      public static DiscountCalculateStrategy getDiscountCalculateStrategy(int discountStyle) {
         if(discountStyle == 1) {
            return new DiscountCalculateStrategyA();
         } else if(discountStyle == 2) {
            return new DiscountCalculateStrategyB();
         } else if(discountStyle == 3) { 
            return new DiscountCalculateStrategyC();
         } else {
            return new DiscountCalculateStrategyDefault();
         }
      }
      
   }
   
   public static class Context {
      
      private DiscountCalculateStrategy strategy;

      public DiscountCalculateStrategy getStrategy() {
         return strategy;
      }

      public void setStrategy(DiscountCalculateStrategy strategy) {
         this.strategy = strategy;
      }
      
      public void calculate() {
         strategy.calculate();
      }
      
   }
   
}

电子商务应用中的支付方法

使用示例: 策略模式在 Java 代码中很常见。 它经常在各种框架中使用, 能在不扩展类的情况下向用户提供改变其行为的方式。

Java 8 开始支持 lambda 方法, 它可作为一种替代策略模式的简单方式。

在本例中, 策略模式被用于在电子商务应用中实现各种支付方法。 客户选中希望购买的商品后需要选择一种支付方式: Paypal 或者信用卡。

具体策略不仅会完成实际的支付工作, 还会改变支付表单的行为, 并在表单中提供相应的字段来记录支付信息。

通用的支付方法接口

/**
 * Common interface for all strategies.
 */
public interface PayStrategy {
    boolean pay(int paymentAmount);
    void collectPaymentDetails();
}

使用 PayPal 支付

/**
 * Concrete strategy. Implements PayPal payment method.
 */
public class PayByPayPal implements PayStrategy {
    private static final Map<String, String> DATA_BASE = new HashMap<>();
    private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
    private String email;
    private String password;
    private boolean signedIn;

    static {
        DATA_BASE.put("amanda1985", "amanda@ya.com");
        DATA_BASE.put("qwerty", "john@amazon.eu");
    }

    /**
     * Collect customer's data.
     */
    @Override
    public void collectPaymentDetails() {
        try {
            while (!signedIn) {
                System.out.print("Enter the user's email: ");
                email = READER.readLine();
                System.out.print("Enter the password: ");
                password = READER.readLine();
                if (verify()) {
                    System.out.println("Data verification has been successful.");
                } else {
                    System.out.println("Wrong email or password!");
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private boolean verify() {
        setSignedIn(email.equals(DATA_BASE.get(password)));
        return signedIn;
    }

    /**
     * Save customer data for future shopping attempts.
     */
    @Override
    public boolean pay(int paymentAmount) {
        if (signedIn) {
            System.out.println("Paying " + paymentAmount + " using PayPal.");
            return true;
        } else {
            return false;
        }
    }

    private void setSignedIn(boolean signedIn) {
        this.signedIn = signedIn;
    }
}

使用信用卡支付

/**
 * Concrete strategy. Implements credit card payment method.
 */
public class PayByCreditCard implements PayStrategy {
    private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
    private CreditCard card;

    /**
     * Collect credit card data.
     */
    @Override
    public void collectPaymentDetails() {
        try {
            System.out.print("Enter the card number: ");
            String number = READER.readLine();
            System.out.print("Enter the card expiration date 'mm/yy': ");
            String date = READER.readLine();
            System.out.print("Enter the CVV code: ");
            String cvv = READER.readLine();
            card = new CreditCard(number, date, cvv);

            // Validate credit card number...

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * After card validation we can charge customer's credit card.
     */
    @Override
    public boolean pay(int paymentAmount) {
        if (cardIsPresent()) {
            System.out.println("Paying " + paymentAmount + " using Credit Card.");
            card.setAmount(card.getAmount() - paymentAmount);
            return true;
        } else {
            return false;
        }
    }

    private boolean cardIsPresent() {
        return card != null;
    }
}

信用卡类

/**
 * Dummy credit card class.
 */
public class CreditCard {
    private int amount;
    private String number;
    private String date;
    private String cvv;

    CreditCard(String number, String date, String cvv) {
        this.amount = 100_000;
        this.number = number;
        this.date = date;
        this.cvv = cvv;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public int getAmount() {
        return amount;
    }
}

订单类

/**
 * Order class. Doesn't know the concrete payment method (strategy) user has
 * picked. It uses common strategy interface to delegate collecting payment data
 * to strategy object. It can be used to save order to database.
 */
public class Order {
    private int totalCost = 0;
    private boolean isClosed = false;

    public void processOrder(PayStrategy strategy) {
        strategy.collectPaymentDetails();
        // Here we could collect and store payment data from the strategy.
    }

    public void setTotalCost(int cost) {
        this.totalCost += cost;
    }

    public int getTotalCost() {
        return totalCost;
    }

    public boolean isClosed() {
        return isClosed;
    }

    public void setClosed() {
        isClosed = true;
    }
}

客户端代码

/**
 * World first console e-commerce application.
 */
public class Demo {
    private static Map<Integer, Integer> priceOnProducts = new HashMap<>();
    private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private static Order order = new Order();
    private static PayStrategy strategy;

    static {
        priceOnProducts.put(1, 2200);
        priceOnProducts.put(2, 1850);
        priceOnProducts.put(3, 1100);
        priceOnProducts.put(4, 890);
    }

    public static void main(String[] args) throws IOException {
        while (!order.isClosed()) {
            int cost;

            String continueChoice;
            do {
                System.out.print("Please, select a product:" + "\n" +
                        "1 - Mother board" + "\n" +
                        "2 - CPU" + "\n" +
                        "3 - HDD" + "\n" +
                        "4 - Memory" + "\n");
                int choice = Integer.parseInt(reader.readLine());
                cost = priceOnProducts.get(choice);
                System.out.print("Count: ");
                int count = Integer.parseInt(reader.readLine());
                order.setTotalCost(cost * count);
                System.out.print("Do you wish to continue selecting products? Y/N: ");
                continueChoice = reader.readLine();
            } while (continueChoice.equalsIgnoreCase("Y"));

            if (strategy == null) {
                System.out.println("Please, select a payment method:" + "\n" +
                        "1 - PalPay" + "\n" +
                        "2 - Credit Card");
                String paymentMethod = reader.readLine();

                // Client creates different strategies based on input from
                // user, application configuration, etc.
                                                // пользовательских данных, конфигурации и прочих параметров.
                if (paymentMethod.equals("1")) {
                    strategy = new PayByPayPal();
                } else {
                    strategy = new PayByCreditCard();
                }
            }

            // Order object delegates gathering payment data to strategy
            // object, since only strategies know what data they need to
            // process a payment.
                                    // т.к. только стратегии знают какие данные им нужны для приёма
            // оплаты.
            order.processOrder(strategy);

            System.out.print("Pay " + order.getTotalCost() + " units or Continue shopping? P/C: ");
            String proceed = reader.readLine();
            if (proceed.equalsIgnoreCase("P")) {
                // Finally, strategy handles the payment.
                                                if (strategy.pay(order.getTotalCost())) {
                    System.out.println("Payment has been successful.");
                } else {
                    System.out.println("FAIL! Please, check your data.");
                }
                order.setClosed();
            }
        }
    }
}

模式的应用场景

1、当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。

策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。

2、当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。

策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。

3、如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。

策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。

4、当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。

策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。

JDK 中的策略模式示例

  • 对 java.util.Comparator#compare() 的调用来自 Collections#sort().

  • javax.servlet.http.HttpServlet: service­() 方法, 还有所有接受 Http­Servlet­Request 和 Http­Servlet­Response 对象作为参数的 do­XXX() 方法。

  • javax.servlet.Filter#doFilter()

与其他模式的关系

  • 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

  • 命令模式和策略看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。

    • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。

    • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。

  • 装饰模式可让你更改对象的外表, 策略则让你能够改变其本质。

  • 模板方法模式基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。

  • 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

模板方法模式

意图

它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

image-20200906214619925

问题

假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。

该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。

image-20200906214645073

数据挖掘类中包含许多重复代码。

一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗?

还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。

解决方案

模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。

让我们考虑如何在数据挖掘应用中实现上述方案。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。

image-20200906214904936

模板方法将算法分解为步骤, 并允许子类重写这些步骤, 而非重写实际的模板方法。

首先, 我们将所有步骤声明为 抽象类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。

现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。

正如你所看到的那样, 我们有两种类型的步骤:

  • 抽象步骤必须由各个子类来实现
  • 可选步骤已有一些默认实现, 但仍可在需要时进行重写

还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。

真实世界类比

image-20200906215031507

可对典型的建筑方案进行微调以更好地满足客户需求。

模板方法可用于建造大量房屋。 标准房屋建造方案中可提供几个扩展点, 允许潜在房屋业主调整成品房屋的部分细节。

每个建造步骤 (例如打地基、 建造框架、 建造墙壁和安装水电管线等) 都能进行微调, 这使得成品房屋会略有不同。

模式结构

image-20200906215231250

抽象类 (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为抽象类型, 也可以提供一些默认实现。

具体类 (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。

通用代码实现

public class TemplateMethodPatterDemo {

   public static void main(String[] args) {
      DiscountCalculator calculator1 = new DiscountCalculator1();
      calculator1.calculate();
      
      DiscountCalculator calculator2 = new DiscountCalculator2();
      calculator2.calculate();
      
      DiscountCalculator calculator3 = new DiscountCalculator3();
      calculator3.calculate();
   }
   
   public interface DiscountCalculator {
      void calculate();
   }
   
   public static abstract class AbstractDiscountCalculator implements DiscountCalculator {
      
      @Override
      public void calculate() {
         // 完成通用的计算逻辑
         commonCalculate();
         // 完成特殊的计算逻辑
         specificCalculate();
      }
      
      private void commonCalculate() {
         System.out.println("通用的计算逻辑,修改了一下");
      }
      
      protected abstract void specificCalculate();
      
   }
   
   public static class DiscountCalculator1 extends AbstractDiscountCalculator {
      @Override
      public void specificCalculate() {
         System.out.println("优惠计算器1的特殊计算逻辑");  
      }
   }
   
   public static class DiscountCalculator2 extends AbstractDiscountCalculator {
      @Override
      public void specificCalculate() {
         System.out.println("优惠计算器2的特殊计算逻辑");  
      }
   }
   
   public static class DiscountCalculator3 extends AbstractDiscountCalculator {
      @Override
      public void specificCalculate() {
         System.out.println("优惠计算器3的特殊计算逻辑");  
      }
   }
   
}

重写算法的标准步骤

使用示例: 模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。

在本例中, 模版方法模式定义了一个可与社交网络协作的算法。 与特定社交网络相匹配的子类将根据社交网络所提供的 API 来实现这些步骤。

基础社交网络类

/**
 * Base class of social network.
 */
public abstract class Network {
    String userName;
    String password;

    Network() {}

    /**
     * Publish the data to whatever network.
     */
    public boolean post(String message) {
        // Authenticate before posting. Every network uses a different
        // authentication method.
        if (logIn(this.userName, this.password)) {
            // Send the post data.
            boolean result =  sendData(message.getBytes());
            logOut();
            return result;
        }
        return false;
    }

    abstract boolean logIn(String userName, String password);
    abstract boolean sendData(byte[] data);
    abstract void logOut();
}

具体社交网络

/**
 * Class of social network
 */
public class Facebook extends Network {
    public Facebook(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public boolean logIn(String userName, String password) {
        System.out.println("\nChecking user's parameters");
        System.out.println("Name: " + this.userName);
        System.out.print("Password: ");
        for (int i = 0; i < this.password.length(); i++) {
            System.out.print("*");
        }
        simulateNetworkLatency();
        System.out.println("\n\nLogIn success on Facebook");
        return true;
    }

    public boolean sendData(byte[] data) {
        boolean messagePosted = true;
        if (messagePosted) {
            System.out.println("Message: '" + new String(data) + "' was posted on Facebook");
            return true;
        } else {
            return false;
        }
    }

    public void logOut() {
        System.out.println("User: '" + userName + "' was logged out from Facebook");
    }

    private void simulateNetworkLatency() {
        try {
            int i = 0;
            System.out.println();
            while (i < 10) {
                System.out.print(".");
                Thread.sleep(500);
                i++;
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}

另一个社交网络

/**
 * Class of social network
 */
public class Twitter extends Network {

    public Twitter(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public boolean logIn(String userName, String password) {
        System.out.println("\nChecking user's parameters");
        System.out.println("Name: " + this.userName);
        System.out.print("Password: ");
        for (int i = 0; i < this.password.length(); i++) {
            System.out.print("*");
        }
        simulateNetworkLatency();
        System.out.println("\n\nLogIn success on Twitter");
        return true;
    }

    public boolean sendData(byte[] data) {
        boolean messagePosted = true;
        if (messagePosted) {
            System.out.println("Message: '" + new String(data) + "' was posted on Twitter");
            return true;
        } else {
            return false;
        }
    }

    public void logOut() {
        System.out.println("User: '" + userName + "' was logged out from Twitter");
    }

    private void simulateNetworkLatency() {
        try {
            int i = 0;
            System.out.println();
            while (i < 10) {
                System.out.print(".");
                Thread.sleep(500);
                i++;
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}

客户端代码

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        Network network = null;
        System.out.print("Input user name: ");
        String userName = reader.readLine();
        System.out.print("Input password: ");
        String password = reader.readLine();

        // Enter the message.
        System.out.print("Input message: ");
        String message = reader.readLine();

        System.out.println("\nChoose social network for posting message.\n" +
                "1 - Facebook\n" +
                "2 - Twitter");
        int choice = Integer.parseInt(reader.readLine());

        // Create proper network object and send the message.
        if (choice == 1) {
            network = new Facebook(userName, password);
        } else if (choice == 2) {
            network = new Twitter(userName, password);
        }
        network.post(message);
    }
}

模式的应用场景

1、当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。

模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。

2、当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。

在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。

JDK 中的模板方法模式示例:

  • java.util.concurrent.locks.AbstractQueuedSynchronizer

  • java.io.InputStream、 java.io.OutputStream、 java.io.Reader 和 java.io.Writer 的所有非抽象方法。

  • java.util.AbstractList、 java.util.AbstractSet 和 java.util.AbstractMap 的所有非抽象方法。

  • javax.servlet.http.HttpServlet, 所有默认发送 HTTP 405 “方法不允许” 错误响应的 do­XXX() 方法。 你可随时对其进行重写。

与其他模式的关系

  • 工厂方法模式是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。

  • 模板方法基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。

访问者模式

意图

它能将算法与其所作用的对象隔离开来。

image-20200906220445215

问题

假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。 图像中的每个节点既能代表复杂实体 (例如一座城市), 也能代表更精细的对象 (例如工业区和旅游景点等)。 如果节点代表的真实对象之间存在公路, 那么这些节点就会相互连接。 在程序内部, 每个节点的类型都由其所属的类来表示, 每个特定的节点则是一个对象。

image-20200906220507186

将图像导出为 XML。

一段时间后, 你接到了实现将图像导出到 XML 文件中的任务。 这些工作最初看上去非常简单。 你计划为每个节点类添加导出函数, 然后递归执行图像中每个节点的导出函数。 解决方案简单且优雅: 使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。

但你不太走运, 系统架构师拒绝批准对已有节点类进行修改。 他认为这些代码已经是产品了, 不想冒险对其进行修改, 因为修改可能会引入潜在的缺陷。

image-20200906221059885

所有节点的类中都必须添加导出至 XML 文件的方法, 但如果在修改代码的过程中引入了任何缺陷, 那么整个程序都会面临风险。

此外, 他还质疑在节点类中包含导出 XML 文件的代码是否有意义。 这些类的主要工作是处理地理数据。 导出 XML 文件的代码放在这里并不合适。

还有另一个原因, 那就是在此项任务完成后, 营销部门很有可能会要求程序提供导出其他类型文件的功能, 或者提出其他奇怪的要求。 这样你很可能会被迫再次修改这些重要但脆弱的类。

解决方案

访问者模式建议将新行为放入一个名为访问者的独立类中, 而不是试图将其整合到已有类中。 现在, 需要执行操作的原始对象将作为参数被传递给访问者中的方法, 让方法能访问对象所包含的一切必要数据。

如果现在该操作能在不同类的对象上执行会怎么样呢? 比如在我们的示例中, 各节点类导出 XML 文件的实际实现很可能会稍有不同。 因此, 访问者类可以定义一组 (而不是一个) 方法, 且每个方法可接收不同类型的参数。

但我们究竟应该如何调用这些方法 (尤其是在处理整个图像方面) 呢? 这些方法的签名各不相同, 因此我们不能使用多态机制。 为了可以挑选出能够处理特定对象的访问者方法, 我们需要对它的类进行检查。 这是不是听上去像个噩梦呢?

你可能会问, 我们为什么不使用方法重载呢? 就是使用相同的方法名称, 但它们的参数不同。 不幸的是, 即使我们的编程语言 (例如 Java 和 C#) 支持重载也不行。 由于我们无法提前知晓节点对象所属的类, 所以重载机制无法执行正确的方法。 方法会将 节点基类作为输入参数的默认类型。

但是, 访问者模式可以解决这个问题。 它使用了一种名为双分派的技巧, 不使用累赘的条件语句也可下执行正确的方法。 与其让客户端来选择调用正确版本的方法, 不如将选择权委派给作为参数传递给访问者的对象。 由于该对象知晓其自身的类, 因此能更自然地在访问者中选出正确的方法。 它们会 “接收” 一个访问者并告诉其应执行的访问者方法。

现在, 如果我们抽取出所有访问者的通用接口, 所有已有的节点都能与我们在程序中引入的任何访问者交互。 如果需要引入与节点相关的某个行为, 你只需要实现一个新的访问者类即可。

真实世界类比

image-20200906221250837

优秀的保险代理人总能为不同类型的团体提供不同的保单。

假如有这样一位非常希望赢得新客户的资深保险代理人。 他可以拜访街区中的每栋楼, 尝试向每个路人推销保险。 所以, 根据大楼内组织类型的不同, 他可以提供专门的保单:

  • 如果建筑是居民楼, 他会推销医疗保险。
  • 如果建筑是银行, 他会推销失窃保险。
  • 如果建筑是咖啡厅, 他会推销火灾和洪水保险。

模式结构

image-20200906221510322

访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。

具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。

元素 (Element) 接口声明了一个方法来 “接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。

具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。 请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。

客户端 (Client) 通常会作为集合或其他复杂对象 (例如一个组合树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。

通用代码实现

public class VisitorPattern {
    public static void main(String[] args) {
        ObjectStructure os = new ObjectStructure();
        os.add(new ConcreteElementA());
        os.add(new ConcreteElementB());
        Visitor visitor = new ConcreteVisitorA();
        os.accept(visitor);
        System.out.println("------------------------");
        visitor = new ConcreteVisitorB();
        os.accept(visitor);
    }
}

//抽象访问者
interface Visitor {
    void visit(ConcreteElementA element);

    void visit(ConcreteElementB element);
}

//具体访问者A类
class ConcreteVisitorA implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者A访问-->" + element.operationA());
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者A访问-->" + element.operationB());
    }
}

//具体访问者B类
class ConcreteVisitorB implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者B访问-->" + element.operationA());
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者B访问-->" + element.operationB());
    }
}

//抽象元素类
interface Element {
    void accept(Visitor visitor);
}

//具体元素A类
class ConcreteElementA implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationA() {
        return "具体元素A的操作。";
    }
}

//具体元素B类
class ConcreteElementB implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationB() {
        return "具体元素B的操作。";
    }
}

//对象结构角色
class ObjectStructure {
    private List<Element> list = new ArrayList<Element>();

    public void accept(Visitor visitor) {
        Iterator<Element> i = list.iterator();
        while (i.hasNext()) {
            ((Element) i.next()).accept(visitor);
        }
    }

    public void add(Element element) {
        list.add(element);
    }

    public void remove(Element element) {
        list.remove(element);
    }
}

搭配组合模式

访问者(Visitor)模式同“组合模式”联用。因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如图下:

1589423486063

public class VisitorPatternDemo {
   
   public static void main(String[] args) {
      Department leafDept1 = new Department("叶子部门1"); 
      Department leafDept2 = new Department("叶子部门2");
      Department leafDept3 = new Department("叶子部门3"); 
      
      Department subDept1 = new Department("子部门1");
      subDept1.getChildren().add(leafDept1);
      subDept1.getChildren().add(leafDept2);
      
      Department subDept2 = new Department("子部门2"); 
      subDept2.getChildren().add(leafDept3);
      
      Department parentDept = new Department("父部门");
      parentDept.getChildren().add(subDept1);
      parentDept.getChildren().add(subDept2);
      
      Visitor removeVisitor = new RemoveVisitor();
      parentDept.accept(removeVisitor);
      
      Visitor updateStatusVisitor = new UpdateStatusVisitor("禁用");  
      parentDept.accept(updateStatusVisitor);
   }
   
   public static class Department {
      private String name;
      private List<Department> children = new ArrayList<Department>();
      
      public Department(String name) {
         super();
         this.name = name;
      }
      
      public String getName() {
         return name;
      }
      public void setName(String name) {
         this.name = name;
      }
      public List<Department> getChildren() {
         return children;
      }
      public void setChildren(List<Department> children) {
         this.children = children;
      }
      
      public void accept(Visitor visitor) {
         visitor.visit(this);
      }
      
   }
   
   public interface Visitor {
      void visit(Department dept);
   }
   
   public static class RemoveVisitor implements Visitor {
      @Override
        public void visit(Department dept) {
         if(dept.getChildren().size() > 0) {
            for(Department child : dept.getChildren()) {  
               child.accept(this);  
            }
         }
         System.out.println("删除部门【" + dept.getName() + "】");  
      }
      
   }  
   
   public static class UpdateStatusVisitor implements Visitor {
      private String status;
      @Override
        public void visit(Department dept) {
         if(dept.getChildren().size() > 0) {
            for(Department child : dept.getChildren()) {  
               child.accept(this);  
            }
         }
         System.out.println("将部门【" + dept.getName() + "】的状态修改为:" + status);  
      }
      
      public UpdateStatusVisitor(String status) {
         this.status = status;
      }
   }
   
}

模式的应用场景

1、如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。

访问者模式通过在访问者对象中为多个目标类提供相同操作的变体, 让你能在属于不同类的一组对象上执行同一操作。

2、可使用访问者模式来清理辅助行为的业务逻辑。

该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。

3、当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时, 可使用该模式。

你可将该行为抽取到单独的访问者类中, 只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。

JDK 中的访问者模式示例

  • javax.lang.model.element.AnnotationValue 和 Annotation­Value­Visitor
  • javax.lang.model.element.Element 和 Element­Visitor
  • javax.lang.model.type.TypeMirror 和 Type­Visitor
  • java.nio.file.FileVisitor 和 Simple­File­Visitor
  • javax.faces.component.visit.VisitContext 和 Visit­Callback

与其他模式的关系

  • 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

  • 你可以使用访问者对整个组合模式树执行操作。

  • 可以同时使用访问者和迭代器模式来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值