iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Software Development

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

Day 15: Structural patterns - Facade

目的

建立一個對外的窗口(介面),負責提供特定功能,而功能背後如何運作?與哪些物件有所關聯?通通交給對外窗口來實踐。

說明

讓複雜的系統有一個對外的窗口,負責發號施令告知子系統該如何運作。

現實生活中,不少方便的事物對使用者來說都是簡單,背後卻有著複雜的步驟,例如:

  • 網購下單,經過下單成功、選定商品、裝箱、運送、到指定取貨點。
  • 股票下單,經過網路平台、下單、成交、告知繳納金額、取得繳納金額、完成交易。

換到程式上,可以分成三層:資料存取層、業務邏輯層、表示層,各層有各自的生態圈,如果放任彼此的聯繫方式,將導致過度耦合,未來的新增、修改將漸漸不容易。所以,適時地建立對外窗口,負責制定能提供的功能,各層之間依賴窗口溝通,進而簡化耦合度

在網頁開發上最有名的個案是 jQuery,提供一個簡單的介面,背後卻是非常複雜的運算。

jQuery運用Facade

實踐的作法是:

  1. 建立對外窗口,了解需求後建立相關功能,負責與子系統溝通。
  2. 他人要使用該系統時,一律向對外窗口溝通。

UML 圖

Facade Pattern UML Diagram

使用 Java 實作

子系統:DrinksVendingMachineMoneySystemShippingSystem

public class DrinksVendingMachine {
    private boolean isNormal;

    public DrinksVendingMachine() {
        isNormal = true;
    }

    public void welcome() {
        if (isNormal) {
            System.out.println("歡迎使用本機器");
            System.out.println("請投入硬幣或紙鈔");
        } else {
            System.out.println("機器故障,請聯絡廠商");
        }
    }

    public int getCoin() throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        return Integer.parseInt(br.readLine());
    }

    public void displayMoney(int money) {
        System.out.println("已投入 " + money + " 元");
        System.out.println("系統亮起可購買飲料");
    }

    public String chooseDrink() throws IOException {
        System.out.println("請選擇飲料");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        return br.readLine();
    }

    public void takeDrink(String drink) {
        System.out.println("客戶已經取出" + drink);
    }

    public void getError() {
        isNormal = false;
    }
}

public class MoneySystem {
    private int currentMoney;

    public MoneySystem() {
        currentMoney = 0;
    }

    public void insertCoin(int value) {
        currentMoney += value;
    }

    public int showCurrentMoney() {
        return currentMoney;
    }
}

public class ShippingSystem {
    private String drinkName;

    public ShippingSystem() {
        drinkName = "";
    }

    public void setUpChooseDrink(String drink) {
        drinkName = drink;
    }

    public void openGate() {
        System.out.println("販賣機底下出口已開啟");
    }

    public void shipping() {
        System.out.println("運作" + drinkName + "的輸送帶");
        System.out.println(drinkName + "往下掉入出口");
        System.out.println("發出撞擊聲");
        System.out.println(drinkName + "可樂已抵達取出口");
    }

    public String getChosenDrink() {
        return drinkName;
    }
}

對外窗口:VendingMachineFacade

import java.io.IOException;

public class VendingMachineFacade {
    private DrinksVendingMachine dvm;
    private MoneySystem ms;
    private ShippingSystem ss;

    public VendingMachineFacade() {
        dvm = new DrinksVendingMachine();
        ms = new MoneySystem();
        ss = new ShippingSystem();
    }

    public void useIt() throws IOException {
        dvm.welcome();
        ms.insertCoin(dvm.getCoin());
        dvm.displayMoney(ms.showCurrentMoney());
        ss.setUpChooseDrink(dvm.chooseDrink());
        ss.shipping();
        dvm.takeDrink(ss.getChosenDrink());
    }
}

測試:DrinkFacadeSample

import java.io.IOException;

public class DrinkFacadeSample {
    public static void main(String[] args) throws IOException {
        VendingMachineFacade machine = new VendingMachineFacade();
        machine.useIt();
    }
}

使用 JavaScript 實作

設定環境,能夠讀取終端機的輸入文字

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const getUserInput = () => {
  return new Promise((resolve, reject) => {
    rl.question("", (input) => {
      resolve(input);
    });
  });
};

子系統:DrinksVendingMachineMoneySystemShippingSystem

class DrinksVendingMachine {
  constructor() {
    this.isNormal = true;
  }

  welcome() {
    if (this.isNormal) {
      console.log("歡迎使用本機器");
      console.log("請投入硬幣或紙鈔");
    } else {
      console.log("機器故障,請聯絡廠商");
    }
  }

  getCoin() {
    return getUserInput();
  }

  displayMoney(money) {
    console.log("已投入 " + money + " 元");
    console.log("系統亮起可購買飲料");
  }

  chooseDrink() {
    console.log("請選擇飲料");
    return getUserInput();
  }

  takeDrink(drink) {
    console.log("客戶已經取出" + drink);
  }

  getError() {
    this.isNormal = false;
  }
}

class MoneySystem {
  constructor() {
    this.currentMoney = 0;
  }

  insertCoin(value) {
    this.currentMoney += value;
  }

  showCurrentMoney() {
    return this.currentMoney;
  }
}

class ShippingSystem {
  constructor() {
    this.drinkName = "";
  }

  setUpChooseDrink(drink) {
    this.drinkName = drink;
  }

  openGate() {
    console.log("販賣機底下出口已開啟");
  }

  shipping() {
    console.log("運作" + this.drinkName + "的輸送帶");
    console.log(this.drinkName + "往下掉入出口");
    console.log("發出撞擊聲");
    console.log(this.drinkName + "已抵達取出口");
  }

  getChosenDrink() {
    return this.drinkName;
  }
}

對外窗口:VendingMachineFacade

class VendingMachineFacade {
  constructor() {
    this.dvm = new DrinksVendingMachine();
    this.ms = new MoneySystem();
    this.ss = new ShippingSystem();
  }

  async useIt() {
    this.dvm.welcome();
    this.ms.insertCoin(parseInt(await this.dvm.getCoin()));
    this.dvm.displayMoney(this.ms.showCurrentMoney());
    this.ss.setUpChooseDrink(await this.dvm.chooseDrink());
    this.ss.shipping();
    this.dvm.takeDrink(this.ss.getChosenDrink());
    rl.close();
  }
}

測試:drinkFacadeSample

const drinkFacadeSample = async () => {
  const machine = new VendingMachineFacade();
  await machine.useIt();
}

drinkFacadeSample();

總結

Facade 有趣在於,日常開發上經常使用,只是沒有仔細思索過這樣做的好處,因此在閱讀相關文章時,除了腦中有經驗可以馬上連結之外,還能重新看待當初開發的過程,細細品味當時有沒有盡力做好?是否有些對外窗口沒有好好處理?再者,閱讀自己的專案之外,閱讀 jQuery 的原始檔,慢慢能從中看出一些設計上的巧思,不再是當年那個剛踏入程式開發的自己了,同時對自己的成長感到開心。

明天將介紹 Structural patterns 的第六個模式:Flyweight 模式。


上一篇
Day 14: Structural patterns - Decorator
下一篇
Day 16: Structural patterns - Flyweight
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言