0%

设计模式之行为型模式(十一):访问者模式

1. 访问者模式概述

  • 概念

    • 表示一个作用于某对象结构中的各元素的操作。它使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作
  • 特点

    • 访问者模式的核心思想:将数据的结构对数据的操作分离
    • 《设计模式》一书给访问者模式设计了一个“双重分派”的机制,而 包括 Java 在内的大多数语言只支持单分派,用单分派语言强行模拟出双重分派导致了访问者模式看起来比较复杂
    • 模拟双重分派:用重写方法的动态分配特性将重载方法模拟成动态分派,即在重写方法中调用重载方法,使得重载方法使用起来也像是动态分配的一样。避免使用 instanceOf + 强制类型转换的较丑陋的方式
  • Demo:自助餐程序 1.0

    • 餐厅:准备好各种食物,提供接收访问者的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Restaurant {
      private String lobster = "lobster";
      private String watermelon = "watermelon";
      private String steak = "steak";
      private String banana = "banana";

      public void welcome(IVisitor visitor) {
      visitor.chooseLobster(lobster);
      visitor.chooseWatermelon(watermelon);
      visitor.chooseSteak(steak);
      visitor.chooseBanana(banana);
      }
      }
    • 顾客接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public interface IVisitor {
      void chooseLobster(String lobster);

      void chooseWatermelon(String watermelon);

      void chooseSteak(String steak);

      void chooseBanana(String banana);
      }
    • 顾客 Aurora

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public class Aurora implements IVisitor {
      @Override
      public void chooseLobster(String lobster) {
      System.out.println("Aurora gets a " + lobster);
      }

      @Override
      public void chooseWatermelon(String watermelon) {
      System.out.println("Aurora gets a " + watermelon);
      }

      @Override
      public void chooseSteak(String steak) {
      System.out.println("Aurora doesn't like " + steak);
      }

      @Override
      public void chooseBanana(String banana) {
      System.out.println("Aurora doesn't like " + banana);
      }
      }
    • 客户端测试

      1
      2
      3
      4
      5
      6
      7
      8
      public class Client {
      @Test
      public void test() {
      Restaurant restaurant = new Restaurant();
      IVisitor Aurora = new Aurora();
      restaurant.welcome(Aurora);
      }
      }
    • 运行程序,输出如下

      1
      2
      3
      4
      Aurora gets a lobster
      Aurora gets a watermelon
      Aurora doesn't like steak
      Aurora doesn't like banana

2. 单分派与双重分派分析

  • Demo

    • Food 类

      1
      2
      3
      4
      5
      public class Food {
      public String name() {
      return "food";
      }
      }
    • Watermelon 类:继承 Food 类

      1
      2
      3
      4
      5
      6
      public class Watermelon extends Food {
      @Override
      public String name() {
      return "watermelon";
      }
      }
    • 测试代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class Client {
      @Test
      public void test() {
      Food food = new Watermelon();
      eat(food);
      }

      public void eat(Food food) {
      System.out.println("eat food: " + food.name());
      }

      public void eat(Watermelon watermelon) {
      System.out.println("eat watermelon" + watermelon.name());
      }
      }
    • 运行 test() 函数,输出如下

      1
      eat food: watermelon
  • 分析一:Java 对重写方法和重载方法的调用方式是不同的

    • 调用重写方法:与对象的运行时类型有关
    • 调用重载方法:只与方法签名中声明的参数类型有关,与对象运行时的具体类型无关
  • 分析二:在面向对象的编程语言中,我们将方法调用称之为分派。上面的 Java 测试代码运行时,经历了两次分派

    • 调用重载方法:选择调用 eat(Food food) 还是 eat(Watermelon watermelon)。虽然这里传入的这个参数的实际类型是 Watermelon,但这里会调用 eat(Food food),这是因为调用哪个重载方法是在编译期就确定了的,称之为静态分派
    • 调用重写方法:选择调用 Foodname 方法还是 Watermelonname 方法。这里会根据参数运行时的实际类型,调用 Watermelonname 方法,称之为动态分派
  • 定义:方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少个宗量,可以将分派分为单分派和多分派

    • 单分派:根据一个宗量就可以知道应该调用哪个方法
    • 多分派:需要根据多个宗量才能确定调用目标
  • 区别

    • 单分派:程序在选择重载方法和重写方法时,如果其中一种情况是动态分配、另一种情况是静态分派,则称之为单分派
    • 多分派:程序在选择重载方法和重写方法时,如果两种情况都是动态分配的,则称之为双重分派

3. 模拟双重分派的访问者模式 Demo

  • Demo:自助餐程序 2.0

    • 父类 Food

      1
      2
      3
      public abstract class Food {
      public abstract String name();
      }
    • 龙虾 Lobster:继承 Food

      1
      2
      3
      4
      5
      6
      public class Lobster extends Food {
      @Override
      public String name() {
      return "lobster";
      }
      }

      Lobster

    • 西瓜 Watermelon:继承 Food

      1
      2
      3
      4
      5
      6
      public class Watermelon extends Food {
      @Override
      public String name() {
      return "watermelon";
      }
      }

      Watermelon

    • 牛排 Steak:继承 Food

      1
      2
      3
      4
      5
      6
      public class Steak extends Food {
      @Override
      public String name() {
      return "steak";
      }
      }

      Steak

    • 香蕉 Banana:继承 Food

      1
      2
      3
      4
      5
      6
      public class Banana extends Food {
      @Override
      public String name() {
      return "banana";
      }
      }

      Banana

    • 顾客接口 IVisitor

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public interface IVisitor {
      void chooseFood(Lobster lobster);

      void chooseFood(Watermelon watermelon);

      void chooseFood(Steak steak);

      void chooseFood(Banana banana);
      }
    • 餐厅类 Restaurant

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      class Restaurant {

      // 准备当天的食物
      private List<Food> prepareFoods() {
      List<Food> foods = new ArrayList<>();
      // 简单模拟,每种食物添加 10 份
      for (int i = 0; i < 10; i++) {
      foods.add(new Lobster());
      foods.add(new Watermelon());
      foods.add(new Steak());
      foods.add(new Banana());
      }
      return foods;
      }

      // 欢迎顾客来访
      public void welcome(IVisitor visitor) {
      // 获取当天的食物
      List<Food> foods = prepareFoods();
      // 将食物依次提供给顾客选择
      for (Food food : foods) {
      // 由于单分派机制,此处无法编译通过
      visitor.chooseFood(food);
      }
      }
      }
  • 分析

    • 看起来很美好,实际上,visitor.chooseFood(food); 这一行是无法通过编译的,原因就是 Java 是单分派编程语言
    • 虽然每种食物都继承自 Food 类,但由于接口中没有 chooseFood(Food food); 这个重载方法,所以这一行会报错:"Cannot resolve method chooseFood"
  • 方案 1:采用 instanceOf + 强制类型转换

    • 代码

      1
      2
      3
      4
      5
      6
      // 通过 instanceOf 判断具体子类型,再强制向下转型
      if (food instanceof Lobster) visitor.chooseFood((Lobster) food);
      else if (food instanceof Watermelon) visitor.chooseFood((Watermelon) food);
      else if (food instanceof Steak) visitor.chooseFood((Steak) food);
      else if (food instanceof Banana) visitor.chooseFood((Banana) food);
      else throw new IllegalArgumentException("Unsupported type of food.");
    • 分析:可行,在某些开源代码中就是这么做的,但强制类型转换的方法既冗长又不符合开闭原则,较丑陋

  • 方案 2《设计模式》中推荐的非常巧妙的做法:在重写方法中调用重载方法,即用重写包装重载,使重载具有动态分配的特性

    • 父类 Food:添加 accept(Visitor visitor) 抽象方法

      1
      2
      3
      4
      5
      6
      public abstract class Food {
      public abstract String name();

      // Food 中添加 accept 方法,接收访问者
      public abstract void accept(IVisitor visitor);
      }
    • 具体子类:重写实现抽象方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Lobster extends Food {
      @Override
      public String name() {
      return "lobster";
      }

      @Override
      public void accept(IVisitor visitor) {
      visitor.chooseFood(this);
      }
      }
    • 修改餐厅类 Restaurant:将就餐者的代码由重载方法 visitor.chooseFood(food); 改为了重写方法 food.accept(visitor);

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      class Restaurant {

      // 准备当天的食物
      private List<Food> prepareFoods() {
      List<Food> foods = new ArrayList<>();
      // 简单模拟,每种食物添加 10 份
      for (int i = 0; i < 10; i++) {
      foods.add(new Lobster());
      foods.add(new Watermelon());
      foods.add(new Steak());
      foods.add(new Banana());
      }
      return foods;
      }

      // 欢迎顾客来访
      public void welcome(IVisitor visitor) {
      // 获取当天的食物
      List<Food> foods = prepareFoods();
      // 将食物依次提供给顾客选择
      for (Food food : foods) {
      // 由于重写方法是动态分派的,所以这里会调用具体子类的 accept 方法,
      food.accept(visitor);
      }
      }
      }
    • 顾客 Aurora 类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public class Aurora implements IVisitor {

      @Override
      public void chooseFood(Lobster lobster) {
      System.out.println("Aurora gets a " + lobster.name());
      }

      @Override
      public void chooseFood(Watermelon watermelon) {
      System.out.println("Aurora gets a " + watermelon.name());
      }

      @Override
      public void chooseFood(Steak steak) {
      System.out.println("Aurora doesn't like " + steak.name());
      }

      @Override
      public void chooseFood(Banana banana) {
      System.out.println("Aurora doesn't like " + banana.name());
      }
      }
    • 顾客 Kevin 类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public class Kevin implements IVisitor {

      @Override
      public void chooseFood(Lobster lobster) {
      System.out.println("Kevin doesn't like " + lobster.name());
      }

      @Override
      public void chooseFood(Watermelon watermelon) {
      System.out.println("Kevin doesn't like " + watermelon.name());
      }

      @Override
      public void chooseFood(Steak steak) {
      System.out.println("Kevin gets a " + steak.name());
      }

      @Override
      public void chooseFood(Banana banana) {
      System.out.println("Kevin gets a " + banana.name());
      }
      }
    • 客户端测试

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class Client {
      @Test
      public void test() {
      Restaurant restaurant = new Restaurant();
      IVisitor Aurora = new Aurora();
      IVisitor Kevin = new Kevin();
      restaurant.welcome(Aurora);
      restaurant.welcome(Kevin);
      }
      }
    • 运行程序,输出如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      Aurora gets a lobster
      Aurora gets a watermelon
      Aurora doesn't like steak
      Aurora doesn't like banana
      ... 输出 10 遍
      Kevin doesn't like lobster
      Kevin doesn't like watermelon
      Kevin gets a steak
      Kevin gets a banana
      ... 输出 10 遍

4. 重载方法和重写方法的区别

区别点 重载方法 重写方法
参数列表 必须修改 不能修改
返回类型 可以修改 不能修改
异常类型 可以修改 可以减小或缩小范围,不能增加或扩大范围
访问限制 可以修改 可以减少限制,不能扩大限制
-------------------- 本文结束感谢您的阅读 --------------------