iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Software Development

也該是時候學學 Design Pattern 了系列 第 28

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 27: Behavioral patterns - Template Method
下一篇
Day 29: 23 + 1 個模式的總結
系列文
也該是時候學學 Design Pattern 了31

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-14 12:09:08

恭喜即將邁入完賽~/images/emoticon/emoticon08.gif

我要留言

立即登入留言