iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0
Software Development

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

Day 22: Behavioral patterns - Mediator

目的

當系統內的物件們各自溝通的情況日益嚴重時,建立一個負責溝通的集中所,讓元件們不在「直接」,而是「間接」與他者溝通。

說明

通常在程式剛開發的時候,為了快速達到開發目的,可能會出現物件們直接與其他物件溝通,像是:

  • 物件 A 要求物件 B 的某個屬性增減。
  • 物件 B 要求物件 C 執行內建方法。
  • 物件 D 要通知物件 A、B、C 某個訊息。

隨著物件的數量增長,交互溝通的情況宛如人際連結的網絡般,越來越複雜,這在開發上絕對不是一件好事,日後如果有修改的需求,很容易變成上 Patch 的情況。

因此,可以建立一個負責溝通的物件,所有物件都要通過該物件才能完成溝通的工作,至少讓複雜、混亂的溝通關係簡化成星狀圖,

作法是:

  1. 建立系統內的物件親代,負責開規格,其中要建立一個關於聯繫 Mediator 的方法。
  2. 建立負責溝通的物件親代,負責開規格,關於系統內所有物件的互相溝通方法都要記錄下來。
  3. 建立系統內的物件子代,如果要影響他者,則在方法內跟 Mediator 溝通。除此之外,要建立其他物件會影響自身的方法。
  4. 建立負責溝通的物件子代,負責調度、實現目的。

以下範例以「自行車隊的行動」為核心製作。

UML 圖

Mediator Pattern UML Diagram

使用 Java 實作

系統內的物件親代:Player

public interface Player {
    void setMediator(Mediator mediator);

    String getName();

    String getJob();
}

負責溝通的物件親代:Mediator

public interface Mediator {
    void registerPlayer(Player player);

    void changeFormation(int type);

    void accelerate();

    void decelerate();

    void notifyTeamMembers(String message, Player requester);
}

系統內的物件子代:SprinterDomestiqueTimeTrialistClimbingSpecialist

public class Sprinter implements Player {
    private String name;
    private String job = "Sprinter";
    private Mediator mediator;

    public Sprinter(String name) {
        this.name = name;
    }

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

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

    @Override
    public String getJob() {
        return job;
    }

    public void teamChangeFormation(int type) {
        mediator.changeFormation(type);
    }

    public void changeFormation(int position) {
        System.out.println(job + " 收到通知,移動到車隊的第 " + position + " 個位置");
    }

    public void teamAccelerate() {
        mediator.accelerate();
    }

    public void personalAccelerate() {
        System.out.println("車手 " + name + " 收到通知,開始加速");
    }

    public void teamDecelerate() {
        mediator.decelerate();
    }

    public void personalDecelerate() {
        System.out.println("車手 " + name + " 收到通知,開始減速");
    }

    public void notifyTeamMembers(String message) {
        mediator.notifyTeamMembers(message, this);
    }

    public void receiveMessage(String message) {
        System.out.println("車手 " + name + " 收到: " + message);
    }
}

public class Domestique implements Player {
    private String name;
    private String job = "Domestique";
    private Mediator mediator;

    public Domestique(String name) {
        this.name = name;
    }

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

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

    @Override
    public String getJob() {
        return job;
    }

    public void teamChangeFormation(int type) {
        mediator.changeFormation(type);
    }

    public void changeFormation(int position) {
        System.out.println(job + " 收到通知,移動到車隊的第 " + position + " 個位置");
    }

    public void teamAccelerate() {
        mediator.accelerate();
    }

    public void personalAccelerate() {
        System.out.println("車手 " + name + " 收到通知,開始加速");
    }

    public void teamDecelerate() {
        mediator.decelerate();
    }

    public void personalDecelerate() {
        System.out.println("車手 " + name + " 收到通知,開始減速");
    }

    public void notifyTeamMembers(String message) {
        mediator.notifyTeamMembers(message, this);
    }

    public void receiveMessage(String message) {
        System.out.println("車手 " + name + " 收到: " + message);
    }
}

public class TimeTrialist implements Player {
    private String name;
    private String job = "TimeTrialist";
    private Mediator mediator;

    public TimeTrialist(String name) {
        this.name = name;
    }

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

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

    @Override
    public String getJob() {
        return job;
    }

    public void teamChangeFormation(int type) {
        mediator.changeFormation(type);
    }

    public void changeFormation(int position) {
        System.out.println(job + " 收到通知,移動到車隊的第 " + position + " 個位置");
    }

    public void teamAccelerate() {
        mediator.accelerate();
    }

    public void personalAccelerate() {
        System.out.println("車手 " + name + " 收到通知,開始加速");
    }

    public void teamDecelerate() {
        mediator.decelerate();
    }

    public void personalDecelerate() {
        System.out.println("車手 " + name + " 收到通知,開始減速");
    }

    public void notifyTeamMembers(String message) {
        mediator.notifyTeamMembers(message, this);
    }

    public void receiveMessage(String message) {
        System.out.println("車手 " + name + " 收到: " + message);
    }
}

public class ClimbingSpecialist implements Player {
    private String name;
    private String job = "ClimbingSpecialist";
    private Mediator mediator;

    public ClimbingSpecialist(String name) {
        this.name = name;
    }

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

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

    @Override
    public String getJob() {
        return job;
    }

    public void teamChangeFormation(int type) {
        mediator.changeFormation(type);
    }

    public void changeFormation(int position) {
        System.out.println(job + " 收到通知,移動到車隊的第 " + position + " 個位置");
    }

    public void teamAccelerate() {
        mediator.accelerate();
    }

    public void personalAccelerate() {
        System.out.println("車手 " + name + " 收到通知,開始加速");
    }

    public void teamDecelerate() {
        mediator.decelerate();
    }

    public void personalDecelerate() {
        System.out.println("車手 " + name + " 收到通知,開始減速");
    }

    public void notifyTeamMembers(String message) {
        mediator.notifyTeamMembers(message, this);
    }

    public void receiveMessage(String message) {
        System.out.println("車手 " + name + " 收到: " + message);
    }
}

負責溝通的物件子代:PitStop(Mediator 物件)

public class PitStop implements Mediator {
    private Sprinter sprinter;
    private Domestique domestique;
    private TimeTrialist timeTrialist;
    private ClimbingSpecialist climbingSpecialist;

    @Override
    public void registerPlayer(Player player) {
        player.setMediator(this);

        switch (player.getJob()) {
            case "Sprinter":
                sprinter = (Sprinter) player;
                break;
            case "Domestique":
                domestique = (Domestique) player;
                break;
            case "TimeTrialist":
                timeTrialist = (TimeTrialist) player;
                break;
            case "ClimbingSpecialist":
                climbingSpecialist = (ClimbingSpecialist) player;
                break;
        }

    }

    @Override
    public void changeFormation(int type) {
        switch (type) {
            case 1:
                sprinter.changeFormation(1);
                domestique.changeFormation(2);
                timeTrialist.changeFormation(3);
                climbingSpecialist.changeFormation(4);
                break;
            case 2:
                domestique.changeFormation(1);
                timeTrialist.changeFormation(2);
                climbingSpecialist.changeFormation(3);
                sprinter.changeFormation(4);
                break;
            case 3:
                climbingSpecialist.changeFormation(1);
                domestique.changeFormation(2);
                sprinter.changeFormation(3);
                timeTrialist.changeFormation(4);
                break;
            case 4:
                timeTrialist.changeFormation(1);
                climbingSpecialist.changeFormation(2);
                sprinter.changeFormation(3);
                domestique.changeFormation(4);
                break;
        }

        System.out.println("");
    }

    @Override
    public void accelerate() {
        sprinter.personalAccelerate();
        domestique.personalAccelerate();
        timeTrialist.personalAccelerate();
        climbingSpecialist.personalAccelerate();
        System.out.println("");
    }

    @Override
    public void decelerate() {
        sprinter.personalDecelerate();
        domestique.personalDecelerate();
        timeTrialist.personalDecelerate();
        climbingSpecialist.personalDecelerate();
        System.out.println("");
    }

    @Override
    public void notifyTeamMembers(String message, Player requester) {
        String finalMessage = "車手: " + requester.getName() + " 通知: " + message;

        switch (requester.getJob()) {
            case "Sprinter":
                domestique.receiveMessage(finalMessage);
                timeTrialist.receiveMessage(finalMessage);
                climbingSpecialist.receiveMessage(finalMessage);
                break;
            case "Domestique":
                sprinter.receiveMessage(finalMessage);
                timeTrialist.receiveMessage(finalMessage);
                climbingSpecialist.receiveMessage(finalMessage);
                break;
            case "TimeTrialist":
                sprinter.receiveMessage(finalMessage);
                domestique.receiveMessage(finalMessage);
                climbingSpecialist.receiveMessage(finalMessage);
                break;
            case "ClimbingSpecialist":
                sprinter.receiveMessage(finalMessage);
                domestique.receiveMessage(finalMessage);
                timeTrialist.receiveMessage(finalMessage);
                break;
        }

        System.out.println("");
    }

}

測試,建立車隊成員後,模擬溝通情境:BicycleRacingMediatorSample

public class BicycleRacingMediatorSample {

    public static void main(String[] args) {
        Mediator mediator = new PitStop();
        Sprinter sprinter = new Sprinter("Roger");
        Domestique domestique = new Domestique("Victor");
        TimeTrialist timeTrialist = new TimeTrialist("David");
        ClimbingSpecialist climbingSpecialist = new ClimbingSpecialist("Eric");

        /** 註冊隊友 */
        mediator.registerPlayer(sprinter);
        mediator.registerPlayer(domestique);
        mediator.registerPlayer(timeTrialist);
        mediator.registerPlayer(climbingSpecialist);

        /** 要求加速 */
        sprinter.teamAccelerate();
        domestique.teamAccelerate();

        /** 要求減速 */
        timeTrialist.teamDecelerate();
        climbingSpecialist.teamChangeFormation(2);

        /** 通知隊友 */
        domestique.notifyTeamMembers("前方有大彎道,向右");

        /** 最後衝刺 */
        sprinter.teamChangeFormation(1);
        sprinter.notifyTeamMembers("抵達終點線");
    }

}

使用 JavaScript 實作

系統內的物件親代:Player

/** @interface */
class Player {
  /** @param {Mediator} mediator */
  setMediator(mediator) { return };

  getName() { return ""; }

  getJob() { return ""; }
}

負責溝通的物件親代:Mediator

/** @interface */
class Mediator {
  /** @param {Player} player */
  registerPlayer(player) { return; }

  /** @param {number} type */
  changeFormation(type) { return; }

  accelerate() { return; }

  decelerate() { return; }

  /**
   * @param {string} message
   * @param {Player} requester
   */
  notifyTeamMembers(message, requester) { return; }
}

系統內的物件子代:SprinterDomestiqueTimeTrialistClimbingSpecialist

class Sprinter extends Player {
  constructor(name) {
    super();
    /** @type {string} */
    this.name = name;
    this.job = "Sprinter";
    /** @type {Mediator} */
    this.mediator = null;
  }

  /**
   * @override
   * @param {Mediator} mediator
  */
  setMediator(mediator) {
    this.mediator = mediator;
  }

  /** @override */
  getName() {
    return this.name;
  }

  /** @override */
  getJob() {
    return this.job;
  }

  /** @param {number} type */
  teamChangeFormation(type) {
    this.mediator.changeFormation(type);
  }

  /** @param {number} position */
  changeFormation(position) {
    console.log(this.job + " 收到通知,移動到車隊的第 " + position + " 個位置");
  }

  teamAccelerate() {
    this.mediator.accelerate();
  }

  personalAccelerate() {
    console.log("車手 " + this.name + " 收到通知,開始加速");
  }

  teamDecelerate() {
    this.mediator.decelerate();
  }

  personalDecelerate() {
    console.log("車手 " + this.name + " 收到通知,開始減速");
  }

  /** @param {string} message */
  notifyTeamMembers(message) {
    this.mediator.notifyTeamMembers(message, this);
  }

  /** @param {string} message */
  receiveMessage(message) {
    console.log("車手 " + this.name + " 收到: " + message);
  }
}

class Domestique extends Player {

  constructor(name) {
    super();
    /** @type {string} */
    this.name = name;
    this.job = "Domestique";
    /** @type {Mediator} */
    this.mediator = null;
  }

  /**
   * @override
   * @param {Mediator} mediator
  */
  setMediator(mediator) {
    this.mediator = mediator;
  }

  /** @override */
  getName() {
    return this.name;
  }

  /** @override */
  getJob() {
    return this.job;
  }

  /** @param {number} type */
  teamChangeFormation(type) {
    this.mediator.changeFormation(type);
  }

  /** @param {number} position */
  changeFormation(position) {
    console.log(this.job + " 收到通知,移動到車隊的第 " + position + " 個位置");
  }

  teamAccelerate() {
    this.mediator.accelerate();
  }

  personalAccelerate() {
    console.log("車手 " + this.name + " 收到通知,開始加速");
  }

  teamDecelerate() {
    this.mediator.decelerate();
  }

  personalDecelerate() {
    console.log("車手 " + this.name + " 收到通知,開始減速");
  }

  /** @param {string} message */
  notifyTeamMembers(message) {
    this.mediator.notifyTeamMembers(message, this);
  }

  /** @param {string} message */
  receiveMessage(message) {
    console.log("車手 " + this.name + " 收到: " + message);
  }
}

class TimeTrialist extends Player {
  constructor(name) {
    super();
    /** @type {string} */
    this.name = name;
    this.job = "TimeTrialist";
    /** @type {Mediator} */
    this.mediator = null;
  }

  /**
   * @override
   * @param {Mediator} mediator
  */
  setMediator(mediator) {
    this.mediator = mediator;
  }

  /** @override */
  getName() {
    return this.name;
  }

  /** @override */
  getJob() {
    return this.job;
  }

  /** @param {number} type */
  teamChangeFormation(type) {
    this.mediator.changeFormation(type);
  }

  /** @param {number} position */
  changeFormation(position) {
    console.log(this.job + " 收到通知,移動到車隊的第 " + position + " 個位置");
  }

  teamAccelerate() {
    this.mediator.accelerate();
  }

  personalAccelerate() {
    console.log("車手 " + this.name + " 收到通知,開始加速");
  }

  teamDecelerate() {
    this.mediator.decelerate();
  }

  personalDecelerate() {
    console.log("車手 " + this.name + " 收到通知,開始減速");
  }

  /** @param {string} message */
  notifyTeamMembers(message) {
    this.mediator.notifyTeamMembers(message, this);
  }

  /** @param {string} message */
  receiveMessage(message) {
    console.log("車手 " + this.name + " 收到: " + message);
  }
}

class ClimbingSpecialist extends Player {
  constructor(name) {
    super();
    /** @type {string} */
    this.name = name;
    this.job = "ClimbingSpecialist";
    /** @type {Mediator} */
    this.mediator = null;
  }

  /**
   * @override
   * @param {Mediator} mediator
  */
  setMediator(mediator) {
    this.mediator = mediator;
  }

  /** @override */
  getName() {
    return this.name;
  }

  /** @override */
  getJob() {
    return this.job;
  }

  /** @param {number} type */
  teamChangeFormation(type) {
    this.mediator.changeFormation(type);
  }

  /** @param {number} position */
  changeFormation(position) {
    console.log(this.job + " 收到通知,移動到車隊的第 " + position + " 個位置");
  }

  teamAccelerate() {
    this.mediator.accelerate();
  }

  personalAccelerate() {
    console.log("車手 " + this.name + " 收到通知,開始加速");
  }

  teamDecelerate() {
    this.mediator.decelerate();
  }

  personalDecelerate() {
    console.log("車手 " + this.name + " 收到通知,開始減速");
  }

  /** @param {string} message */
  notifyTeamMembers(message) {
    this.mediator.notifyTeamMembers(message, this);
  }

  /** @param {string} message */
  receiveMessage(message) {
    console.log("車手 " + this.name + " 收到: " + message);
  }
}

負責溝通的物件子代:PitStop(Mediator 物件)

class PitStop extends Mediator {
  constructor() {
    super();
    /** @type {Sprinter} */
    this.sprinter = null;
    /** @type {Domestique} */
    this.domestique = null;
    /** @type {TimeTrialist} */
    this.timeTrialist = null;
    /** @type {ClimbingSpecialist} */
    this.climbingSpecialist = null;
  }

  /**
   * @override
   * @param {Player} player
   */
  registerPlayer(player) {
    player.setMediator(this);

    switch (player.getJob()) {
      case "Sprinter":
        this.sprinter = player;
        break;
      case "Domestique":
        this.domestique = player;
        break;
      case "TimeTrialist":
        this.timeTrialist = player;
        break;
      case "ClimbingSpecialist":
        this.climbingSpecialist = player;
        break;
    }

  }

  /**
   * @override
   * @param {number} type
   */
  changeFormation(type) {
    switch (type) {
      case 1:
        this.sprinter.changeFormation(1);
        this.domestique.changeFormation(2);
        this.timeTrialist.changeFormation(3);
        this.climbingSpecialist.changeFormation(4);
        break;
      case 2:
        this.domestique.changeFormation(1);
        this.timeTrialist.changeFormation(2);
        this.climbingSpecialist.changeFormation(3);
        this.sprinter.changeFormation(4);
        break;
      case 3:
        this.climbingSpecialist.changeFormation(1);
        this.domestique.changeFormation(2);
        this.sprinter.changeFormation(3);
        this.timeTrialist.changeFormation(4);
        break;
      case 4:
        this.timeTrialist.changeFormation(1);
        this.climbingSpecialist.changeFormation(2);
        this.sprinter.changeFormation(3);
        this.domestique.changeFormation(4);
        break;
    }

    console.log("");
  }

  /** @override */
  accelerate() {
    this.sprinter.personalAccelerate();
    this.domestique.personalAccelerate();
    this.timeTrialist.personalAccelerate();
    this.climbingSpecialist.personalAccelerate();
    console.log("");
  }

  /** @override */
  decelerate() {
    this.sprinter.personalDecelerate();
    this.domestique.personalDecelerate();
    this.timeTrialist.personalDecelerate();
    this.climbingSpecialist.personalDecelerate();
    console.log("");
  }

  /**
   * @override
   * @param {string} message
   * @param {Player} requester
   */
  notifyTeamMembers(message, requester) {
    const finalMessage = "車手: " + requester.getName() + " 通知: " + message;

    switch (requester.getJob()) {
      case "Sprinter":
        this.domestique.receiveMessage(finalMessage);
        this.timeTrialist.receiveMessage(finalMessage);
        this.climbingSpecialist.receiveMessage(finalMessage);
        break;
      case "Domestique":
        this.sprinter.receiveMessage(finalMessage);
        this.timeTrialist.receiveMessage(finalMessage);
        this.climbingSpecialist.receiveMessage(finalMessage);
        break;
      case "TimeTrialist":
        this.sprinter.receiveMessage(finalMessage);
        this.domestique.receiveMessage(finalMessage);
        this.climbingSpecialist.receiveMessage(finalMessage);
        break;
      case "ClimbingSpecialist":
        this.sprinter.receiveMessage(finalMessage);
        this.domestique.receiveMessage(finalMessage);
        this.timeTrialist.receiveMessage(finalMessage);
        break;
    }

    console.log("");
  }
}

測試,建立車隊成員後,模擬溝通情境:BicycleRacingMediatorSample

const bicycleRacingMediatorSample = () => {
  const mediator = new PitStop();
  const sprinter = new Sprinter("Roger");
  const domestique = new Domestique("Victor");
  const timeTrialist = new TimeTrialist("David");
  const climbingSpecialist = new ClimbingSpecialist("Eric");

  /** 註冊隊友 */
  mediator.registerPlayer(sprinter);
  mediator.registerPlayer(domestique);
  mediator.registerPlayer(timeTrialist);
  mediator.registerPlayer(climbingSpecialist);

  /** 要求加速 */
  sprinter.teamAccelerate();
  domestique.teamAccelerate();

  /** 要求減速 */
  timeTrialist.teamDecelerate();
  climbingSpecialist.teamChangeFormation(2);

  /** 通知隊友 */
  domestique.notifyTeamMembers("前方有大彎道,向右");

  /** 最後衝刺 */
  sprinter.teamChangeFormation(1);
  sprinter.notifyTeamMembers("抵達終點線");
};

bicycleRacingMediatorSample();

總結

起初,我以為 Mediator 模式跟 React 或是 Redux 的中心式管理 Data 相似,實則完全不同。因為 Mediator 模式的中間人不負責儲存變數,只負責「傳遞」,物件之間互相影響後的狀態如何,中間人都不會知道。

該模式玩味之處還有一點,讓所有物件都依賴中間人,造成該中間人成為超級重要的 God Class,變相增加物件的依賴性,這會在往後成為開發上的障礙。

因此,此模式適合在確定物件之間有著高度依賴性,並且往後的變動不多、不大,那就可以安心使用了。

明天將介紹 Behavioural patterns 的第六個模式:Memento 模式。


上一篇
Day 21: Behavioral patterns - Iterator
下一篇
Day 23: Behavioral patterns - Memento
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言