深入淺出設計模式(Design Pattern)-門面模式(8-Facade-Pattern)

Ben
生活的各種可能性
12 min readJan 31, 2024
photo by Beto Galetto

這篇要來介紹一個跟轉接器模式一樣是修改介面的模式,門面模式(Facade Pattern)。

門面模式(Facade Pattern)

定義:這個模式為一個子系統裡面的一組介面提供一個統一的介面。門面定義了高階的介面,可讓子系統更容易使用。

我們來看看我們這次的情境:我們這次要打造一個家庭劇院,我們有許多的設備,例如串流播放器、投影系統、自動螢幕、環繞音響,以及一台爆米花機,接著我們把所有設備都裝好了,準備開始享受我們的家庭劇院。

但在開始之前我們還要做一些事才能看電影:

  1. 打開爆米花機
  2. 開始爆米花
  3. 調暗電燈
  4. 把螢幕拉下來
  5. 打開投影機
  6. 將投影機的輸入設成串流播放器
  7. 將投影器設成寬螢幕模式
  8. 打開擴音機
  9. 將擴音機設成串流播放器輸入
  10. 將擴音機設成環繞音效
  11. 將擴音機的音量調到5
  12. 開啟串流播放器
  13. 開始放電影

下方我們將它轉成類別,並調用它們的方法來示意上方的過程(code不能直接使用)

我們需要打開爆米花機
popper.on();
popper.pop();

調暗燈光至10%
lights.dim(10);

將螢幕放下
screen.down();

打開投影機,並設置寬螢幕模式來播放
projector.on();
projector.setInput(player);
projector.wideScreenMode();

打開擴音機,將它設成串流播放器、環繞音響模式,並將音量調至5
amp.on();
amp.setStreamingPlayer(player);
amp.setSurroundSound();
amp.setVolume(5);

打開串流播放器,播放電影!!
player.on();
player.play(movie);

上方的範例中我們需要用到6個不同的類別之外,別忘了我們可能還會有其它的操作需求,例如:

  • 看完電影後,該怎麼把所有設備關閉?難道要反向做一次所有的事情嗎?
  • 如果你只想聽音樂,也要這麼麻煩嗎?
  • 如果你要升級系統,你可能要重新學習一套操作流程。

這樣實在太複雜了,這不是我們想要的。

接著我們來看看門面模式怎麼幫我們解決這個問題,以下是這次的範例

// 家庭劇院的門面:它接受所需要的子系統以及定義高階的使用介面(也能單獨使用各種子系統的功能),避免用戶端與子系統的耦合。
class HomeTheaterFacade {
amp: Amplifier;
tuner: Tuner;
player: StremingPlayer;
projector: Projector;
screen: TheaterScreen;
lights: TheaterLights;
popper: PopcornPopper;

// 建構式接受需要的子系統類別實例
constructor(
amp: Amplifier,
tuner: Tuner,
player: StremingPlayer,
projector: Projector,
screen: TheaterScreen,
lights: TheaterLights,
popper: PopcornPopper
) {
this.amp = amp;
this.tuner = tuner;
this.player = player;
this.projector = projector;
this.screen = screen;
this.lights = lights;
this.popper = popper;
}

// watchMovie: 高階的使用介面
public watchMovie(movie: string): void {
console.log("Get ready to watch movie...");
this.popper.on();
this.popper.pop();
this.lights.dim(10);
this.screen.down();
this.projector.on();
this.projector.wideScreenMode();
this.amp.setStreamingPlayer();
this.amp.setSourroundSound();
this.amp.setVolume(5);
this.player.on();
this.player.play(movie)
}

// endMovie:高階的使用介面
public endMovie(): void {
console.log("Shutting movie theater down...");
this.popper.off();
this.lights.on();
this.screen.up();
this.projector.off();
this.amp.off();
this.player.stop();
this.player.off();
}
}

class Amplifier {
public on(): void {
console.log("amp on");
}

public off(): void {
console.log("amp off");
}

public setStreamingPlayer(): void {
console.log(`set player`);
}

public setStereoSound(): void {
console.log("set stereo sound");
}

public setSourroundSound(): void {
console.log("set sourround sound");
}

public setTuner(): void {
console.log("set tuner");
}

public setVolume(val: number): void {
console.log(`volume is ${val}`);
}

public toString(): void {}
}

class Tuner {
public on(): void {
console.log("tuner on");
}

public off(): void {
console.log("tuner off");
}

public setAm(): void {
console.log("set tuner is am");
}

public setFm(): void {
console.log("set tuner is fm");
}

public setFrequency(): void {
console.log("set tuner frequency");
}

public toString(): void {}
}

class StremingPlayer {
public on(): void {
console.log("player on");
}

public off(): void {
console.log("player off");
}

public pause(): void {
console.log("player pause");
}

public play(movie: string): void {
console.log(`play movie: ${movie}`);
}

public setSurroundAudio(): void {
console.log("set player surround");
}

public setTwoChannelAudio(): void {
console.log("set player two channel");
}

public stop(): void {
console.log("player stop");
}

public toString(): void {}
}

class Projector {
public on(): void {
console.log("projector on");
}

public off(): void {
console.log("projector off");
}

public tvMode(): void {
console.log("set projector mode: tv");
}

public wideScreenMode(): void {
console.log("set projector mode: screen");
}

public toString(): void {}
}

class TheaterLights {
public on(): void {
console.log("lights on");
}

public off(): void {
console.log("lights off");
}

public dim(val: number): void {
console.log(`light dim is ${val}`);
}
}

class TheaterScreen {
public up(): void {
console.log("screen up");
}

public down(): void {
console.log("screen down");
}

public toString(): void {}
}

class PopcornPopper {
public on(): void {
console.log("popper on");
}

public off(): void {
console.log("popper off");
}

public pop(): void {
console.log("poping corns");
}
}

// 實例化子系統類別
const amp = new Amplifier();
const tuner = new Tuner();
const player = new StremingPlayer();
const projector = new Projector();
const therterScreen = new TheaterScreen();
const lights = new TheaterLights();
const popper = new PopcornPopper();

// 實例化家庭劇院
const homeTheaterFacade = new HomeTheaterFacade(
amp, tuner, player, projector,
therterScreen, lights, popper,
)

homeTheaterFacade.watchMovie("Inception");
console.log("---------------------------");
homeTheaterFacade.endMovie();

HomeTheaterFacade(class):我們有個家庭劇院的class,它也是我們的門面,它負責提供更合理的介面,讓複雜的系統更容易使用,而如果你需要直接使用子系統的功能時,你仍然可以使用它們。而在它的建構式中會接收多個子系統(amp、tuner、player、projector、screen、lights、popper)的實例參數並存到類別的屬性中。

Amp、Tuner、Player、Projector、Screen、Lights、Popper(class):而子系統裡則會提供它們自己的操作方法。

我們來看一下它們的類別圖

類別關係示意圖

HomeTheater負責整合子系統以及提供自己的介面給Client端使用,而我們的子系統們也可以互相引用彼此來做更複雜的互動。

為了使用門面模式,我們要建立一個類別,來統一並簡化一組屬於某些子系統的複雜類別。與許多模式不同的是,門面非常簡單,沒有令人費解的抽象,但是這無損於它的威力。

門面模式可以避免用戶端與子系統之間的緊密耦合,同時它也能幫助你遵守一條物件導向原則

設計原則:最少知識原則: 只與最接近的朋友交談。

它的意思是,在設計系統時,你要注意每一個物件與多少個類別互動,以及它們是怎麼和那些類別互動的。

這條原則可以防止你讓大量的類別耦合在一起,導致你在系統的某個部分進行的修改影響其他部分。當你讓許多類別之間有大量的依賴關係時,它就是一個脆弱的系統,需要花很多成本來維護,而且會因為過度複雜而讓人難以理解。

這條原則提供一些方針幫助我們在設計時能夠遵守:在任何物件的任何方法裡,你都只能呼叫屬於這些東西的方法,例如

  • 該物件本身
  • 用參數傳給該物件的方法
  • 該方法建立的或實例化的任何物件
  • 該物件的任何組件

我們來實際看看他們的差異

不遵守這條原則:我們對另一個物件發出請求,這樣就會多認識一個物件(依賴關係)
public getTemp() {
const thermometer = station.getTermometer();
return thermometer.getTemperature();
}

遵守這條原則:我們直接要求物件替我們發出請求(我們不需要認識它)
public getTemp() {
return station.getTemperature();
}

重點提示

  • 當你需要統一與簡化一個大型的介面或一組複雜的介面時,那就使用門面。
  • 門面可以解開用戶端與複雜子系統之間的耦合。
  • 製作門面需要將門面與它的子系統組合起來,並使用委託來執行門面的工作。
  • 你可以為一個子系統實作不止一個門面。
  • 門面藉由包裝一組物件來簡化工作。

結語

在門面模式中我們又認識一條新的設計原則:最少知識原則,這條原則幫助我們在設計時避免依賴關係,造成將來許多的問題。而門面模式中的介面則漂亮的幫我們的用戶端和子系統端之間解開了耦合,並且提供一個簡化的介面給用戶端使用,讓彼此能以最少知識原則的方式去做互動。

下一篇要介紹:樣板方法模式(Template Method Pattern)

這篇就到這邊瞜,如果有任何問題歡迎留言給我,喜歡的話也別忘了clap起來唷。

--

--