iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Software Development

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

Day 12: Structural patterns - Bridge

目的

將程式分離成服務與對外窗口(介面),當外界要使用時,呼叫窗口即可,服務的一切不用知道。

說明

起因是這樣子,假如現在有一個平台,且支援三個服務,最直覺的設計方式,便是設計出獨自連結服務的平台:
Simple Bridge Sample

隨著支援平台增加到三個,維持上一個設計,那會演變成:
Complex Bridge Sample

由此可見,該模式的數量公式為:平台數量 X 服務數量 = 模式總量

長遠來看,不是好的做法。

那不如把服務跟平台分離,服務歸服務,平台歸平台,換句話說,一個對內、一個對外,只要讓對外的能調用對內的,同時沒有錯誤產生,使用者自然覺得一切正常。

作法是:

  1. 建立一個服務的親代,負責開規格,方便對外窗口的呼叫,可以是 Abstract ClassInterface
  2. 建立服務子代,實作親代的規格細節。
  3. 建立窗口親代,負責開規格,方便使用者呼叫。
  4. 建立窗口子代,實作親代的規格細節。

UML 圖

Bridge Pattern UML Diagram

使用 Java 實作

服務親代:Train

public interface Train {
    public abstract String checkName();

    public abstract boolean isOnTime();

    public abstract void setTime(int min);

    public abstract void goToDestination();

    public abstract boolean getToiletStatus();

    public abstract void setToiletStatus(boolean using);

    public abstract void moveOn();

    public abstract void stop();

    public abstract void getFoodByTrolleyService();

    public abstract void timeNeedToArrive();

    public abstract void getEmergencies();
}

服務子代:LocalTrainPuyumaExpressTarokoExpress

public class LocalTrain implements Train {
    protected String name;
    protected boolean onTime;
    protected int delayMins;
    protected boolean toiletInUsing;
    protected boolean hasTrolleyService;

    public LocalTrain() {
        this.name = "區間車";
        this.onTime = true;
        this.delayMins = 0;
        this.toiletInUsing = false;
        this.hasTrolleyService = false;
    }

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

    @Override
    public boolean isOnTime() {
        return onTime;
    }

    @Override
    public void setTime(int min) {
        delayMins += min;
        onTime = (delayMins < 0);
    }

    @Override
    public void goToDestination() {
        System.out.println("抵達目的地");
    }

    @Override
    public boolean getToiletStatus() {
        return toiletInUsing;
    }

    @Override
    public void setToiletStatus(boolean using) {
        toiletInUsing = using;
    }

    @Override
    public void moveOn() {
        System.out.println("列車向前");
    }

    @Override
    public void stop() {
        System.out.println("列車停駛");
    }

    @Override
    public void getFoodByTrolleyService() {
        System.out.println("難過,沒有東西可買");

    }

    @Override
    public void timeNeedToArrive() {
        System.out.println("還需要 " + delayMins + "分鐘才能抵達");
    }

    @Override
    public void getEmergencies() {
        // 1 - 5
        int option = (int) (Math.random() * (6 - 1 + 1)) + 1;

        switch (option) {
            case 1:
                System.out.println("撞倒擅闖平交道的車");
                setTime(-50);
                stop();
                moveOn();
                timeNeedToArrive();
                goToDestination();
                break;
            case 2:
                System.out.println("肚子有點餓,想買東西");
                getFoodByTrolleyService();
                setTime(11);
                timeNeedToArrive();
                goToDestination();
                break;
            case 3:
                System.out.println("想上廁所但有人");
                setToiletStatus(true);
                getToiletStatus();
                setTime(-1);
                timeNeedToArrive();
                goToDestination();
                break;
            case 4:
                System.out.println("想上廁所");
                setToiletStatus(false);
                getToiletStatus();
                setTime(-7);
                timeNeedToArrive();
                goToDestination();
                break;
            case 5:
                System.out.println("一路順暢");
                timeNeedToArrive();
                goToDestination();
                break;
            case 6:
                System.out.println("座位沒了,只好站著");
                setTime(-12);
                timeNeedToArrive();
                goToDestination();
                break;
        }
    }
}

public class PuyumaExpress implements Train {
    protected String name;
    protected boolean onTime;
    protected int delayMins;
    protected boolean toiletInUsing;
    protected boolean hasTrolleyService;

    public PuyumaExpress() {
        this.name = "普悠瑪";
        this.onTime = true;
        this.delayMins = 0;
        this.toiletInUsing = false;
        this.hasTrolleyService = true;
    }

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

    @Override
    public boolean isOnTime() {
        return onTime;
    }

    @Override
    public void setTime(int min) {
        delayMins += min;
        onTime = (delayMins < 0);
    }

    @Override
    public void goToDestination() {
        System.out.println("很快抵達目的地");
    }

    @Override
    public boolean getToiletStatus() {
        return toiletInUsing;
    }

    @Override
    public void setToiletStatus(boolean using) {
        toiletInUsing = using;
    }

    @Override
    public void moveOn() {
        System.out.println("列車向前");
    }

    @Override
    public void stop() {
        System.out.println("列車停駛");
    }

    @Override
    public void getFoodByTrolleyService() {
        if (hasTrolleyService) {
            System.out.println("服務員用推車販售食物");
        } else {
            System.out.println("難過,沒有東西可買");
        }

    }

    @Override
    public void timeNeedToArrive() {
        System.out.println("還需要 " + delayMins + "分鐘才能抵達");
    }

    @Override
    public void getEmergencies() {
        // 1 - 4
        int option = (int) (Math.random() * (4 - 1 + 1)) + 1;

        switch (option) {
            case 1:
                System.out.println("肚子有點餓,想買東西");
                getFoodByTrolleyService();
                setTime(-3);
                timeNeedToArrive();
                goToDestination();
                break;
            case 2:
                System.out.println("上下車花費太多時間");
                setTime(-15);
                moveOn();
                timeNeedToArrive();
                goToDestination();
                break;
            case 3:
                System.out.println("想上廁所");
                setToiletStatus(false);
                setTime(12);
                getToiletStatus();
                timeNeedToArrive();
                goToDestination();
                break;
            case 4:
                System.out.println("一路順暢");
                timeNeedToArrive();
                goToDestination();
                break;
        }
    }
}

public class TarokoExpress implements Train {
    protected String name;
    protected boolean onTime;
    protected int delayMins;
    protected boolean toiletInUsing;
    protected boolean hasTrolleyService;

    public TarokoExpress() {
        this.name = "太魯閣";
        this.onTime = true;
        this.delayMins = 0;
        this.toiletInUsing = false;
        this.hasTrolleyService = true;
    }

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

    @Override
    public boolean isOnTime() {
        return onTime;
    }

    @Override
    public void setTime(int min) {
        delayMins += min;
        onTime = (delayMins < 0);
    }

    @Override
    public void goToDestination() {
        System.out.println("開心地抵達目的地");
    }

    @Override
    public boolean getToiletStatus() {
        return toiletInUsing;
    }

    @Override
    public void setToiletStatus(boolean using) {
        toiletInUsing = using;
    }

    @Override
    public void moveOn() {
        System.out.println("列車向前");
    }

    @Override
    public void stop() {
        System.out.println("列車停駛");
    }

    @Override
    public void getFoodByTrolleyService() {
        if (hasTrolleyService) {
            System.out.println("服務員用推車販售食物");
        } else {
            System.out.println("難過,沒有東西可買");
        }

    }

    @Override
    public void timeNeedToArrive() {
        System.out.println("還需要 " + delayMins + "分鐘才能抵達");
    }

    @Override
    public void getEmergencies() {
        // 1 - 4
        int option = (int) (Math.random() * (4 - 1 + 1)) + 1;

        switch (option) {
            case 1:
                System.out.println("因為平交道上障礙物所以停車");
                setTime(-50);
                stop();
                moveOn();
                timeNeedToArrive();
                goToDestination();
                break;
            case 2:
                System.out.println("肚子有點餓,想買東西");
                getFoodByTrolleyService();
                setTime(220);
                timeNeedToArrive();
                goToDestination();
                break;
            case 3:
                System.out.println("想上廁所但有人");
                setToiletStatus(true);
                getToiletStatus();
                setTime(1);
                timeNeedToArrive();
                goToDestination();
                break;
            case 4:
                System.out.println("一路順暢");
                timeNeedToArrive();
                goToDestination();
                break;
        }
    }
}

窗口親代:Traveler

public interface Traveler {
    public abstract void checkTicket(Train train);

    public abstract void getJourney();
}

窗口子代:SingleTravelerFamilyTravelerForeignTraveler

public class SingleTraveler implements Traveler {
    private String name;
    private Train ticket;

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

    @Override
    public void checkTicket(Train train) {
        this.ticket = train;
        System.out.println("我是 " + name + ",確認車種為:" + ticket.checkName());
    }

    @Override
    public void getJourney() {
        System.out.println("本人 " + name + " 的旅途即將開始");
        ticket.getEmergencies();
    }
}

public class FamilyTraveler implements Traveler {
    private String name;
    private int children;
    private Train ticket;

    public FamilyTraveler(String name, int childrenCount) {
        this.name = name;
        this.children = childrenCount;
    }

    @Override
    public void checkTicket(Train train) {
        this.ticket = train;
        System.out.println("我是 " + name + ",確認車種為:" + ticket.checkName());
    }

    @Override
    public void getJourney() {
        System.out.println("與 " + children + " 個孩子的旅途即將開始");
        ticket.getEmergencies();
    }
}

public class ForeignTraveler implements Traveler {
    private String name;
    private String country;
    private Train ticket;

    public ForeignTraveler(String name, String country) {
        this.name = name;
        this.country = country;
    }

    @Override
    public void checkTicket(Train train) {
        this.ticket = train;
        System.out.println("My name is " + name + ",the train is:" + ticket.checkName());
    }

    @Override
    public void getJourney() {
        System.out.println("I miss my country: " + country + " , but the new journey is so adorable");
        ticket.getEmergencies();
    }
}

測試:JourneyBridgePatternSample

public class JourneyBridgePatternSample {
    public static void main(String[] args) {
        System.out.println("---一人的旅程開始---");
        System.out.println("這次的車種是:區間車");
        SingleTraveler singleTraveler = new SingleTraveler("維特");
        singleTraveler.checkTicket(new LocalTrain());
        singleTraveler.getJourney();

        System.out.println("\n---下一組是家庭的旅程---");
        System.out.println("車種為:普悠瑪");
        FamilyTraveler familyTraveler = new FamilyTraveler("羅傑", 3);
        familyTraveler.checkTicket(new PuyumaExpress());
        familyTraveler.getJourney();

        System.out.println("\n---最後是外國人的旅程---");
        System.out.println("雖然想家,仍把握機會搭乘:太魯閣");
        ForeignTraveler foreignTraveler = new ForeignTraveler("David", "US");
        foreignTraveler.checkTicket(new TarokoExpress());
        foreignTraveler.getJourney();
    }
}

使用 JavaScript 實作

服務親代:Train

/** @interface */
class Train {
  constructor(name, hasTrolleyService) {
    this.name = name;
    this.onTime = true;
    this.delayMins = 0;
    this.toiletInUsing = false;
    this.hasTrolleyService = hasTrolleyService;
  }
  /** @abstract */
  checkName() { return; }
  /** @abstract */
  isOnTime() { return; }
  /** @abstract */
  setTime(min) { return; }
  /** @abstract */
  goToDestination() { return; }
  /** @abstract */
  getToiletStatus() { return; }
  /** @abstract */
  setToiletStatus(using) { return; }
  /** @abstract */
  moveOn() { return; }
  /** @abstract */
  stop() { return; }
  /** @abstract */
  getFoodByTrolleyService() { return; }
  /** @abstract */
  timeNeedToArrive() { return; }
  /** @abstract */
  getEmergencies() { return; }
}

服務子代:LocalTrainPuyumaExpressTarokoExpress

class LocalTrain extends Train {
  constructor() {
    super("區間車", false);
  }

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

  /** @override */
  isOnTime() {
    return this.onTime;
  }

  /** @override */
  setTime(min) {
    this.delayMins += min;
    this.onTime = (this.delayMins < 0);
  }

  /** @override */
  goToDestination() {
    console.log("抵達目的地");
  }

  /** @override */
  getToiletStatus() {
    return this.toiletInUsing;
  }

  /** @override */
  setToiletStatus(using) {
    this.toiletInUsing = using;
  }

  /** @override */
  moveOn() {
    console.log("列車向前");
  }

  /** @override */
  stop() {
    console.log("列車停駛");
  }

  /** @override */
  getFoodByTrolleyService() {
    console.log("難過,沒有東西可買");

  }

  /** @override */
  timeNeedToArrive() {
    console.log("還需要 " + this.delayMins + "分鐘才能抵達");
  }

  /** @override */
  getEmergencies() {
    // 1 - 5
    const option = Math.floor(Math.random() * (6 - 1 + 1)) + 1;

    switch (option) {
      case 1:
        console.log("撞倒擅闖平交道的車");
        this.setTime(-50);
        this.stop();
        this.moveOn();
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 2:
        console.log("肚子有點餓,想買東西");
        this.getFoodByTrolleyService();
        this.setTime(11);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 3:
        console.log("想上廁所但有人");
        this.setToiletStatus(true);
        this.getToiletStatus();
        this.setTime(-1);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 4:
        console.log("想上廁所");
        this.setToiletStatus(false);
        this.getToiletStatus();
        this.setTime(-7);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 5:
        console.log("一路順暢");
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 6:
        console.log("座位沒了,只好站著");
        this.setTime(-12);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
    }
  }
}

class PuyumaExpress extends Train {
  constructor() {
    super("普悠瑪", true);
  }

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

  /** @override */
  isOnTime() {
    return this.onTime;
  }

  /** @override */
  setTime(min) {
    this.delayMins += min;
    this.onTime = (this.delayMins < 0);
  }

  /** @override */
  goToDestination() {
    console.log("很快抵達目的地");
  }

  /** @override */
  getToiletStatus() {
    return this.toiletInUsing;
  }

  /** @override */
  setToiletStatus(using) {
    this.toiletInUsing = using;
  }

  /** @override */
  moveOn() {
    console.log("列車向前");
  }

  /** @override */
  stop() {
    console.log("列車停駛");
  }

  /** @override */
  getFoodByTrolleyService() {
    if (this.hasTrolleyService) {
      console.log("服務員用推車販售食物,買了雞腿便當");
    } else {
      console.log("難過,沒有東西可買");
    }

  }

  /** @override */
  timeNeedToArrive() {
    console.log("還需要 " + this.delayMins + "分鐘才能抵達");
  }

  /** @override */
  getEmergencies() {
    // 1 - 4
    const option = Math.floor(Math.random() * (4 - 1 + 1)) + 1;

    switch (option) {
      case 1:
        console.log("肚子有點餓,想買東西");
        this.getFoodByTrolleyService();
        this.setTime(-3);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 2:
        console.log("上下車花費太多時間");
        this.setTime(-15);
        this.moveOn();
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 3:
        console.log("想上廁所");
        this.setToiletStatus(false);
        this.setTime(12);
        this.getToiletStatus();
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 4:
        console.log("一路順暢");
        this.timeNeedToArrive();
        this.goToDestination();
        break;
    }
  }
}

class TarokoExpress extends Train {
  constructor() {
    super("太魯閣", true);
  }

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

  /** @override */
  isOnTime() {
    return this.onTime;
  }

  /** @override */
  setTime(min) {
    this.delayMins += min;
    this.onTime = (this.delayMins < 0);
  }

  /** @override */
  goToDestination() {
    console.log("開心地抵達目的地");
  }

  /** @override */
  getToiletStatus() {
    return this.toiletInUsing;
  }

  /** @override */
  setToiletStatus(using) {
    this.toiletInUsing = using;
  }

  /** @override */
  moveOn() {
    console.log("列車向前");
  }

  /** @override */
  stop() {
    console.log("列車停駛");
  }

  /** @override */
  getFoodByTrolleyService() {
    if (this.hasTrolleyService) {
      console.log("服務員用推車販售食物,買了排骨便當");
    } else {
      console.log("難過,沒有東西可買");
    }

  }

  /** @override */
  timeNeedToArrive() {
    console.log("還需要 " + this.delayMins + "分鐘才能抵達");
  }

  /** @override */
  getEmergencies() {
    // 1 - 4
    const option = Math.floor(Math.random() * (4 - 1 + 1)) + 1;

    switch (option) {
      case 1:
        console.log("因為平交道上障礙物所以停車");
        this.setTime(-50);
        this.stop();
        this.moveOn();
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 2:
        console.log("肚子有點餓,想買東西");
        this.getFoodByTrolleyService();
        this.setTime(20);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 3:
        console.log("想上廁所但有人");
        this.setToiletStatus(true);
        this.getToiletStatus();
        this.setTime(1);
        this.timeNeedToArrive();
        this.goToDestination();
        break;
      case 4:
        console.log("一路順暢");
        this.timeNeedToArrive();
        this.goToDestination();
        break;
    }
  }
}

窗口親代:Traveler

/** @interface */
class Traveler {
  constructor(name) {
    this.name = name;
    this.ticket = null;
  }
  /** @abstract */
  checkTicket(train) { return; }
  /** @abstract */
  getJourney() { return; }
}

窗口子代:SingleTravelerFamilyTravelerForeignTraveler

class SingleTraveler extends Traveler {
  constructor(name) {
    super(name);
  }

  /** @override */
  checkTicket(train) {
    this.ticket = train;
    console.log("我是 " + this.name + ",確認車種為:" + this.ticket.checkName());
  }

  /** @override */
  getJourney() {
    console.log("本人 " + this.name + " 的旅途即將開始");
    this.ticket.getEmergencies();
  }
}

class FamilyTraveler extends Traveler {
  constructor(name, childrenCount) {
    super(name);
    this.children = childrenCount;
  }

  /** @override */
  checkTicket(train) {
    this.ticket = train;
    console.log("我是 " + this.name + ",確認車種為:" + this.ticket.checkName());
  }

  /** @override */
  getJourney() {
    console.log("與 " + this.children + " 個孩子的旅途即將開始");
    this.ticket.getEmergencies();
  }
}

class ForeignTraveler extends Traveler {
  constructor(name, country) {
    super(name);
    this.country = country;
  }

  /** @override */
  checkTicket(train) {
    this.ticket = train;
    console.log("My name is " + this.name + ",the train is:" + this.ticket.checkName());
  }

  /** @override */
  getJourney() {
    console.log("I miss my country: " + this.country + " , but the new journey is so adorable");
    this.ticket.getEmergencies();
  }
}

測試:journeyBridgePatternSample

const journeyBridgePatternSample = () => {
  console.log("---一人的旅程開始---");
  console.log("這次的車種是:區間車");
  const singleTraveler = new SingleTraveler();
  singleTraveler.checkTicket(new LocalTrain());
  singleTraveler.getJourney();

  console.log("\n---下一組是家庭的旅程---");
  console.log("車種為:普悠瑪");
  const familyTraveler = new FamilyTraveler("羅傑", 3);
  familyTraveler.checkTicket(new PuyumaExpress());
  familyTraveler.getJourney();

  console.log("\n---最後是外國人的旅程---");
  console.log("雖然想家,仍把握機會搭乘:太魯閣");
  const foreignTraveler = new ForeignTraveler("David", "US");
  foreignTraveler.checkTicket(new TarokoExpress());
  foreignTraveler.getJourney();
};

journeyBridgePatternSample();

總結

Bridge 算是在開發上很常使用的模式,在於分離,讓物件們可以專責在自己的功能上。過往的開發經驗上,時常使用該模式,但省略使用虛擬層開規格這步驟。經由這次的學習,了解先寫下規格後,方便之後開發服務時知道哪些功能要開發,而對外窗口在呼叫上也能省略再三確認的麻煩。

缺點也是十分明顯:

  • 隨著程式碼的複雜度上升後,虛擬層要怎麼開?
  • 服務實作時,如何同時維持規格以及內部的商業邏輯?是否會過度複雜化?

這些仰賴經驗來判斷,講多了還是親自面對專案時,才能慢慢思考出合適的做法。

明天將介紹 Structural patterns 的第三個模式:Composite 模式。


上一篇
Day 11: Structural patterns - Adapter
下一篇
Day 13: Structural patterns - Composite
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言