Day 28: Behavioral patterns - Visitor

目的

当一群相似结构的物件们,在执行相同方法时却有着不同实作内容,那可以将方法封装成独立物件。当需要增加新的方法时,不用改变物件的结构,只需要增加封装的方法就能使用。如此一来,让方法的扩充、修改变得容易许多。

说明

试想一个 RPG 的情境,建立一个职业物件,本身可以执行两个方法:

class Warrior {
  constructor() {
    this.hp = 35
    this.mp = 0
  }

  attack() {
    // 执行 Warrior 专属的 attack
  }

  defense() {
    // 执行 Warrior 专属的 attack
  }
}

现在,要建立一个拥有相同方法的新职业,其方法内的实作细节不同:

class Thief {
  constructor() {
    this.hp = 30
    this.mp = 0
  }

  attack() {
    // 执行 Thief 专属的 attack
  }

  defense() {
    // 执行 Thief 专属的 defense
  }
}

假如又需要新增一个职业,同时三个职业还要新增一个方法呢?

class Warrior {
  constructor() {
    this.hp = 35
    this.mp = 0
  }

  attack() {
    // 执行 Warrior 专属的 attack
  }

  defense() {
    // 执行 Warrior 专属的 defense
  }

  run() {
    // 执行 Warrior 专属的 run
  }
}

class Thief {
  constructor() {
    this.hp = 30
    this.mp = 0
  }

  attack() {
    // 执行 Thief 专属的 attack
  }

  defense() {
    // 执行 Thief 专属的 defense
  }

  run() {
    // 执行 Thief 专属的 run
  }
}

class BlackMage {
  constructor() {
    this.hp = 25
    this.mp = 10
  }

  attack() {
    // 执行 BlackMage 专属的 attack
  }

  defense() {
    // 执行 BlackMage 专属的 defense
  }

  run() {
    // 执行 BlackMage 专属的 run
  }
}

随着职业的增加、方法的增加,会变得越难管理。

仔细观察,职业的结构相似,差异在方法,那有没有一个模式,可以将方法封装起来,同时使用方法时,能够配合不同的职业而执行不同的内容呢?

这就是 Visitor 模式的由来。

作法是:

  1. 定义方法的虚拟层亲代,需要开规格、建立许多方法,每个方法负责对应一个职业物件。除此之外,方法跟物件的互动可以看作是「方法拜访物件」,所以方法也称作 Visitor。
  2. 建立职业物件的虚拟层亲代,需要定义一个方法(称作:Accept),负责跟方法互动。因为物件本身充满资料,所以称作 Element。
  3. 建立 Element 子代,实作 Accept 方法,实作内容是呼叫 Visitor 上职业专属的方法。
  4. 建立 Visitor 子代,实作每个职业在不同 Visitor 要执行的内容。

刚刚的 RPG 情境,发展到现在,可以制作成表格:

Warrior Thief BlackMage
attack AttackByWarrior AttackByThief AttackByBlackMage
defense DefenseByWarrior DefenseByThief DefenseByBlackMage
run RunByWarrior RunByThief RunByBlackMage
magic MagicByWarrior MagicByThief MagicByBlackMage

以下范例以「模拟简易 RPG」为核心,将制作:

  • 三个 Element 物件,分别是 Warrior、Thief 和 BlackMage。
  • 四个 Visitor 物件,分别是 attack、defense、run 和 magic。

UML 图

Visitor Pattern UML Diagram

使用 Java 实作

建立方法的虚拟层亲代物件:Action

public interface Action {
    public abstract void executeWarriorAction(Warrior element);

    public abstract void executeThiefAction(Thief element);

    public abstract void executeBlackMageAction(BlackMage element);
}

建立职业物件的虚拟层亲代:Character

public abstract class Character {
    protected String name;
    protected String job;
    protected int hp;
    protected int mp;
    protected int level;

    protected Character(String name, String job, int hp, int mp, int level) {
        this.name = name;
        this.job = job;
        this.hp = hp;
        this.mp = mp;
        this.level = level;
        System.out.println("The character " + this.name + " is created successfully");
    }

    public abstract void levelUp();

    public abstract void showCharacterInformation();

    public abstract void accept(Action visitor);
}

建立职业物件的子代:WarriorThiefBlackMage(Element 物件)

public class Warrior extends Character {
    public Warrior(String name) {
        super(name, "Warrior", 30, 0, 1);
    }

    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'I see, I come, I conquer' by Julius Caesar\n");
    }

    @Override
    public void accept(Action visitor) {
        visitor.executeWarriorAction(this);
    }

    @Override
    public void levelUp() {
        this.hp += 3;
        this.level += 1;
    }
}

public class Thief extends Character {
    public Thief(String name) {
        super(name, "Thief", 35, 0, 1);
    }

    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'Everything is permitted, Nothing is true.' by Assassin's Creed\n");
    }

    @Override
    public void accept(Action visitor) {
        visitor.executeThiefAction(this);
    }

    @Override
    public void levelUp() {
        this.hp += 2;
        this.level += 1;
    }
}

public class BlackMage extends Character {
    public BlackMage(String name) {
        super(name, "Black Mage", 35, 0, 1);
    }

    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'Knowledge is power, but using it wisely is the key.' by Khadgar\n");
    }

    @Override
    public void accept(Action visitor) {
        visitor.executeBlackMageAction(this);
    }

    @Override
    public void levelUp() {
        this.hp += 1;
        this.mp += 3;
        this.level += 1;
    }
}

建立方法的子代物件:AttackDefenseRunMagic(Visitor 物件)

public class Attack implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("Use sword to attack enemy");
    }

    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("Use dagger to stab enemy");
    }

    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("It's is wrong decision");
    }
}

public class Defense implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("Use shield to protect team members");
    }

    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("Try to dodge this attack");
    }

    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("Nothing to do");
    }
}

public class Run implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("The last one to run");
    }

    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("The fast one to run");
    }

    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("The slow one to run");
    }
}

public class Magic implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("There is no mana");
    }

    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("The is no mana");
    }

    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("Use fire ball!!!");
    }
}

测试,模拟 RPG 的角色同时执行相同的命令:RPGVisitorSample

public class RPGVisitorSample {
    public static void main(String[] args) {
        System.out.println("建立角色");
        List<Character> characters = new ArrayList<>();
        characters.add(new Warrior("Zest"));
        characters.add(new Thief("Sauber"));
        characters.add(new BlackMage("Fritz"));

        System.out.println("\n集体攻击");
        for (Character character : characters) {
            character.accept(new Attack());
        }

        System.out.println("\n集体防御");
        for (Character character : characters) {
            character.accept(new Defense());
        }

        System.out.println("\n集体使用魔法");
        for (Character character : characters) {
            character.accept(new Magic());
        }

        System.out.println("\n集体逃跑");
        for (Character character : characters) {
            character.accept(new Run());
        }
    }
}

使用 JavaScript 实作

建立方法的虚拟层亲代物件:Action

/** @abstract */
class Character {
  /**
   * @param {string} name
   * @param {string} job
   * @param {int} hp
   * @param {int} mp
   * @param {int} level
   */
  constructor(name, job, hp, mp, level) {
    this.name = name;
    this.job = job;
    this.hp = hp;
    this.mp = mp;
    this.level = level;
    console.log("The character " + this.name + " is created successfully");
  }

  /** @abstract */
  levelUp() { return; }

  /** @abstract */
  showCharacterInformation() { return; }

  /**
   * @abstract
   * @param {Action} visitor
   */
  accept(visitor) { return; }
}

建立职业物件的虚拟层亲代:Character

/** @abstract */
class Action {
  /**
   * @abstract
   * @param {Warrior} element
   */
  executeWarriorAction(element) { return; }

  /**
   * @abstract
   * @param {Thief} element
   */
  executeThiefAction(element) { return; }

  /**
   * @abstract
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) { return; }
}

建立职业物件的子代:WarriorThiefBlackMage(Element 物件)

class Warrior extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Warrior", 30, 0, 1);
  }

  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'I see, I come, I conquer' by Julius Caesar\n");
  }

  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeWarriorAction(this);
  }

  /** @override */
  levelUp() {
    this.hp += 3;
    this.level += 1;
  }
}

class Thief extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Thief", 35, 0, 1);
  }

  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'Everything is permitted, Nothing is true.' by Assassin's Creed\n");
  }

  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeThiefAction(this);
  }

  /** @override */
  levelUp() {
    this.hp += 2;
    this.level += 1;
  }
}

class BlackMage extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Black Mage", 35, 0, 1);
  }

  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'Knowledge is power, but using it wisely is the key.' by Khadgar\n");
  }

  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeBlackMageAction(this);
  }

  /** @override */
  levelUp() {
    this.hp += 1;
    this.mp += 3;
    this.level += 1;
  }
}

建立方法的子代物件:AttackDefenseRunMagic(Visitor 物件)

class Attack extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("Use sword to attack enemy");
  }

  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("Use dagger to stab enemy");
  }

  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("It's is wrong decision");
  }
}

class Defense extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("Use shield to protect team members");
  }

  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("Try to dodge this attack");
  }

  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("Nothing to do");
  }
}

class Run extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("The last one to run");
  }

  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("The fast one to run");
  }

  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("The slow one to run");
  }
}

class Magic extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("There is no mana");
  }

  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("The is no mana");
  }

  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("Use fire ball!!!");
  }
}

测试,模拟 RPG 的角色同时执行相同的命令:rpgVisitorSample

const rpgVisitorSample = () => {
  console.log("建立角色");
  /** @type {Character[]} */
  let characters = [];
  characters.push(new Warrior("Zest"));
  characters.push(new Thief("Sauber"));
  characters.push(new BlackMage("Fritz"));

  console.log("\n集体攻击");
  for (const character of characters) {
    character.accept(new Attack());
  }

  console.log("\n集体防御");
  for (const character of characters) {
    character.accept(new Defense());
  }

  console.log("\n集体使用魔法");
  for (const character of characters) {
    character.accept(new Magic());
  }

  console.log("\n集体逃跑");
  for (const character of characters) {
    character.accept(new Run());
  }
};

rpgVisitorSample();

总结

Visitor 模式的前提是「多个拥有相同资料结构的物件,在相同名称的方法内执行不同内容的程序码」,换句话说,必须在物件的资料结构以及方法都十分清楚时才能套用,这两个条件缺乏任一都无法使用此方法。

也因为如此,能够实作的场合不多,当程序码因为扩充方法而开始混乱时才有登场的机会。

这边简单列出模式的优缺点:

  • 优点:新增方法变得简单。
  • 缺点:不得任意变动资料结构,一旦变动有可能让方法失效。

最後补充一点,因为 Visitor 与 Element 互相依赖,技术上称作 Double Dispatch

已经混乱到需要使用 Visitor 模式了

所以的模式都介绍完毕,明天将总结各个模式。


<<:  Day 29:653. Two Sum IV - Input is a BST

>>:  [Day28]约束规则、更改结构实作

Powershell 入门参数属性(1)

对于 Powershell 脚本的参数,我们可以通过一些属性来限制参数。 今天我们就来看看,怎么通过...

数字谎言

前言 在这个蔬菜是有机的、水果都会甜、衣服耐洗不缩水、满街百年创始老店、车子很省油、房…的社会当中,...

Day14:【TypeScript 学起来】Interfaces(介面) 笔记整理

终於来到 interface,觉得这个算是颇重要的一趴,让我们看下去。这大概是我最认真做笔记的一篇...

[Day 25] Reactive Programming - Spring WebFlux(R2DBC)

前言 在上一个范例中,是写死回传的内容,显然在现实生活中应该是不会有公司让你可以这样做的,而当我们的...

[Day19] SCSS 学习笔记

写 CSS 的时候常常会有些设定是重复出现的,SASS(SCSS)是一个方便的预处理器,提供了变数、...