Facade
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Facade ให้ interface เดียวที่รวมเป็นหนึ่งเดียวกับชุดของ interface ใน subsystem มอบจุดเข้าใช้งานที่ง่ายตัวเดียวให้ client แทนที่จะบังคับให้พวกเขาต้องจัดการ class ที่ทำงานร่วมกันจำนวนมากด้วยตัวเอง
subsystem ที่ทำงานได้เยอะมักมีชิ้นส่วนเคลื่อนไหวเต็มไปหมด แค่จะแปลงไฟล์วิดีโอสักไฟล์ คุณอาจต้องใช้ decoder, audio mixer, ตัวเลือก codec, ตัวคำนวณ bitrate และ writer แต่ละตัวมีการตั้งค่า กฎการจัดลำดับ และความซับซ้อนของตัวเอง
client ที่แค่อยาก “เปลี่ยนไฟล์นี้เป็น MP4” ไม่ควรต้องมาเรียนรู้ทั้งห้า class สร้าง instance ให้ถูกลำดับ แล้วต่อ output เข้าหากันเอง ความรู้ระดับนั้นทำให้ไส้ในของ subsystem รั่วไปถึงผู้เรียกทุกราย และทำให้ subsystem แก้ยากขึ้นไปอีก
Facade คือ class ตัวเดียวที่จำท่าเต้นทั้งชุดนี้ได้ โดยเปิด method เล็ก ๆ ที่เน้นงานออกมา เช่น convert(file, format) แล้วข้างในค่อยสร้างและสั่งงาน object ของ subsystem ตามลำดับที่ถูกต้อง ฝั่ง client จึงขึ้นต่อ facade อย่างเดียว
class ของ subsystem ยังเรียกตรงได้เต็มที่สำหรับคนที่ต้องการควบคุมละเอียด แต่เคสทั่วไปเหลือแค่การเรียกครั้งเดียว และเวลา refactor subsystem คุณก็แก้ที่ facade จุดเดียว ไม่ต้องไล่แก้จุดเรียกใช้เป็นร้อย
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Client
class VideoConverter {
+convert(file, format) string
}
class Decoder {
+decode(file) string
}
class AudioMixer {
+mix(stream) string
}
class Encoder {
+encode(stream, format) string
}
Client --> VideoConverter : uses
VideoConverter --> Decoder
VideoConverter --> AudioMixer
VideoConverter --> Encoder - Facade (
VideoConverter) — class เดียวที่ client คุยด้วย รู้วิธีประสานงาน subsystem ทั้งหมด - Subsystem classes (
Decoder,AudioMixer,Encoder) — ตัวทำงานจริง ไม่รู้จัก facade เลย และยังเรียกใช้ตรง ๆ ได้ - Client — เรียก facade และยังคงถูกแยกออกจากโครงสร้างของ subsystem
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”facade VideoConverter เปลี่ยน request บรรทัดเดียวให้เป็นลำดับที่ถูกต้องของการเรียก decode, mix และ encode
class Decoder { decode = (file: string) => `raw(${file})`;}class AudioMixer { mix = (stream: string) => `mixed(${stream})`;}class Encoder { encode = (stream: string, format: string) => `${stream}.${format}`;}
// Facade: one method hides the subsystem choreography.class VideoConverter { private decoder = new Decoder(); private mixer = new AudioMixer(); private encoder = new Encoder();
convert(file: string, format: string): string { const raw = this.decoder.decode(file); const mixed = this.mixer.mix(raw); return this.encoder.encode(mixed, format); }}
const converter = new VideoConverter();console.log(converter.convert('clip.avi', 'mp4')); // mixed(raw(clip.avi)).mp4class Decoder: def decode(self, file: str) -> str: return f"raw({file})"
class AudioMixer: def mix(self, stream: str) -> str: return f"mixed({stream})"
class Encoder: def encode(self, stream: str, fmt: str) -> str: return f"{stream}.{fmt}"
class VideoConverter: def __init__(self) -> None: self._decoder = Decoder() self._mixer = AudioMixer() self._encoder = Encoder()
def convert(self, file: str, fmt: str) -> str: raw = self._decoder.decode(file) mixed = self._mixer.mix(raw) return self._encoder.encode(mixed, fmt)
converter = VideoConverter()print(converter.convert("clip.avi", "mp4")) # mixed(raw(clip.avi)).mp4package main
import "fmt"
type decoder struct{}
func (decoder) decode(file string) string { return "raw(" + file + ")" }
type audioMixer struct{}
func (audioMixer) mix(stream string) string { return "mixed(" + stream + ")" }
type encoder struct{}
func (encoder) encode(stream, format string) string { return stream + "." + format }
// VideoConverter is the facade over the unexported subsystem.type VideoConverter struct { d decoder m audioMixer e encoder}
func (vc VideoConverter) Convert(file, format string) string { raw := vc.d.decode(file) mixed := vc.m.mix(raw) return vc.e.encode(mixed, format)}
func main() { vc := VideoConverter{} fmt.Println(vc.Convert("clip.avi", "mp4")) // mixed(raw(clip.avi)).mp4}struct Decoder;impl Decoder { fn decode(&self, file: &str) -> String { format!("raw({file})") }}
struct AudioMixer;impl AudioMixer { fn mix(&self, stream: &str) -> String { format!("mixed({stream})") }}
struct Encoder;impl Encoder { fn encode(&self, stream: &str, format: &str) -> String { format!("{stream}.{format}") }}
// Facade: hides the subsystem behind one method.struct VideoConverter { decoder: Decoder, mixer: AudioMixer, encoder: Encoder,}
impl VideoConverter { fn new() -> Self { VideoConverter { decoder: Decoder, mixer: AudioMixer, encoder: Encoder } } fn convert(&self, file: &str, format: &str) -> String { let raw = self.decoder.decode(file); let mixed = self.mixer.mix(&raw); self.encoder.encode(&mixed, format) }}
fn main() { let converter = VideoConverter::new(); println!("{}", converter.convert("clip.avi", "mp4")); // mixed(raw(clip.avi)).mp4}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: ย่อ subsystem หลาย class ให้เหลือจุดเข้าใช้งานที่ง่ายตัวเดียวสำหรับกรณีทั่วไป
- ข้อดี: แยก client ออกจากภายในของ subsystem ดังนั้น subsystem จึงสามารถ refactor ได้อย่างอิสระ
- ข้อดี: ผู้เรียกใช้ขั้นสูงยังเข้าถึง subsystem โดยตรงได้ facade เป็นทางเลือก ไม่ใช่กำแพง
- ข้อเสีย: facade อาจไหลไปเป็น god object ที่รู้มากเกินไป ถ้าคุณเอาแต่กอง feature ใส่เข้าไปเรื่อย ๆ
- ข้อเสีย: อาจบังความยืดหยุ่นที่มีประโยชน์ไว้ ทำให้ client เลือกทางง่าย ทั้งที่งานจริงต้องการทางที่ควบคุมได้ละเอียดกว่า
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Adapter เปลี่ยน interface ของ object หนึ่งให้ตรงกับตัวที่คาดหวัง ส่วน Facade คิดค้น interface ที่เรียบง่ายตัวใหม่เอี่ยมครอบ object จำนวนมาก
- Abstract Factory สามารถอยู่เบื้องหลัง facade เพื่อสร้าง object ของ subsystem ที่ facade ประสานงาน
| Facade | Adapter | Mediator | |
|---|---|---|---|
| จุดประสงค์ | ให้ interface ง่ายต่อ subsystem ที่ซับซ้อน | แก้ interface ที่ไม่ตรงกันระหว่างสองฝั่ง | ประสาน object หลายตัวให้สื่อสารกัน |
| จำนวน class ที่ห่อ | หลาย class | หนึ่ง class (adaptee) | หลาย class (peers) |
| ซ่อน subsystem | ใช่ (เลือกได้) | ไม่ (เพียงแปลง interface) | ไม่ (ยังสื่อสารผ่าน mediator) |
| ตัวอย่าง | axios, AWS SDK | legacy API wrapper | event bus, chat room |