Bridge
แยก abstraction ออกจาก implementation ให้ทั้งสองเปลี่ยนแปลงอิสระ
ลองนึกภาพว่าต้องออกแบบ class รูปทรง (Shape) ที่มีทั้ง Circle และ Square แล้วแต่ละรูปต้องวาดได้บนหลาย renderer เช่น Raster กับ Vector ถ้าใช้ inheritance ตรง ๆ จะได้ RasterCircle, VectorCircle, RasterSquare, VectorSquare — เพิ่มรูปทรงใหม่ 1 แบบ หรือ renderer ใหม่ 1 แบบ จำนวน class จะโตแบบ m×n ทันที นี่คือปัญหาที่เรียกว่า class explosion ซึ่งเกิดเมื่อ class หนึ่งต้องแปรผันไปตามมิติ (dimension) ของการเปลี่ยนแปลงมากกว่าหนึ่งแกนพร้อมกัน
Bridge แก้ปัญหานี้ด้วยแนวคิดหลักของ Gang of Four (GoF) คือ “decouple an abstraction from its implementation so that the two can vary independently” — แยกลำดับชั้น (hierarchy) ออกเป็นสองฝั่งที่เป็นอิสระต่อกัน:
- Abstraction — สิ่งที่ client มองเห็นและเรียกใช้ ถือ control logic ระดับสูง
- Implementor — interface ของรายละเอียดการทำงานจริง ที่มีได้หลายแบบ (concrete implementor)
Abstraction ไม่ได้ inherit จาก implementation แต่ ถือ reference ไปยัง implementor แล้ว delegate งานที่เจาะจงลงไป — ความสัมพันธ์แบบ composition (“has-a”) นี้เองที่เป็น “สะพาน” เชื่อมสองฝั่งเข้าด้วยกัน ทำให้เพิ่ม abstraction ใหม่หรือ implementor ใหม่ได้อิสระ โดยจำนวน class โตแบบ m+n แทนที่จะเป็น m×n และยังสลับ implementor ได้ตอน runtime ด้วย (ไม่ต้อง compile ใหม่)
Bridge สนับสนุน Dependency Inversion Principle (Abstraction พึ่งพา interface ของ Implementor ไม่พึ่ง concrete class) และ Open-Closed Principle (เพิ่ม abstraction หรือ implementor ใหม่ได้โดยไม่แก้ code เดิม) เหมาะเมื่อมีมิติของการเปลี่ยนแปลงหลายแกนพร้อมกัน เช่น ชนิดข้อความ × ช่องทางส่ง, ชนิดรายงาน × รูปแบบ file หรือ business logic × platform
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Notification {
<<abstract>>
#IChannel channel
+Notify(text) void
}
class Alert {
+Notify(text) void
}
class Reminder {
+Notify(text) void
}
class IChannel {
<<interface>>
+Send(msg) void
}
class EmailChannel {
+Send(msg) void
}
class SmsChannel {
+Send(msg) void
}
class PushChannel {
+Send(msg) void
}
Notification o-- IChannel
Notification <|-- Alert
Notification <|-- Reminder
IChannel <|.. EmailChannel
IChannel <|.. SmsChannel
IChannel <|.. PushChannel
สังเกตว่ามีลำดับชั้นสองฝั่งที่แยกกันเด็ดขาด:
- ฝั่ง Abstraction (
Notification→Alert/Reminder) แตกแขนงตามแกน “ชนิดของการแจ้งเตือน” - ฝั่ง Implementor (
IChannel→EmailChannel/SmsChannel/PushChannel) แตกแขนงตามแกน “ช่องทางส่ง”
เส้น o-- คือความสัมพันธ์แบบ aggregation ที่ Notification ถือ IChannel ไว้เป็นสมาชิก — นี่คือ “สะพาน” ที่ชื่อ pattern สื่อถึง เพิ่มชนิดการแจ้งเตือนใหม่ (เช่น Digest) หรือช่องทางใหม่ (เช่น SlackChannel) ก็ทำได้อิสระ โดยไม่ต้องแตะ code อีกฝั่งเลย
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”เมื่อ client เรียก Notify() บน Alert (ซึ่งเป็น Abstraction) มันจะไม่ลงมือส่งข้อความเอง แต่ delegate งานต่อไปให้ IChannel ที่ตัวเองถืออยู่ ผ่าน Channel.Send(...) — ตัว Alert จึงไม่จำเป็นต้องรู้เลยว่าปลายทางเป็นอีเมล, SMS หรือ push notification
sequenceDiagram
participant Client
participant Alert as Alert : Notification
participant Channel as EmailChannel : IChannel
Client->>Alert: Notify("server down")
Alert->>Alert: format message
Alert->>Channel: Send(formatted message)
Channel-->>Alert: ok
Alert-->>Client: return
ขั้นตอนสำคัญ:
- Client สร้าง concrete implementor (เช่น
EmailChannel) แล้ว inject เข้าไปใน constructor ของ Abstraction — นี่คือจุดที่ผูก “สะพาน” เข้าด้วยกัน มักทำผ่าน Dependency Injection - Client เรียก method ระดับสูงบน Abstraction เช่น
Notify(text) - Abstraction จัดการ logic ของตัวเอง (เช่น จัดรูปแบบข้อความ, ตัดสินใจว่าจะแจ้งไหม) แล้วค่อย delegate งานเจาะจง platform ไปให้ Implementor ผ่าน interface ที่ตกลงกันไว้
- Concrete Implementor ทำงานจริง (ยิง SMTP, เรียก SMS gateway ฯลฯ) — รายละเอียดนี้ Abstraction ไม่รับรู้และไม่สนใจ
เพราะ Abstraction คุยกับ Implementor ผ่าน interface เท่านั้น เราจึงสลับ implementor ได้ตอน runtime (เช่น อ่านค่าจาก config ว่าจะส่งอีเมลหรือ SMS) โดยไม่ต้องแก้หรือ compile ฝั่ง Abstraction ใหม่เลย
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// Implementor — สัญญาที่ concrete channel ทุกตัวต้องทำตามpublic interface IChannel{ void Send(string message);}
// Concrete Implementor แต่ละแบบ แยกอิสระจากฝั่ง Abstraction โดยสิ้นเชิงpublic class EmailChannel : IChannel{ private readonly string _smtpHost; public EmailChannel(string smtpHost) => _smtpHost = smtpHost;
public void Send(string message) { // ในของจริงจะยิงผ่าน SMTP client ไปที่ _smtpHost Console.WriteLine($"[Email via {_smtpHost}] {message}"); }}
public class SmsChannel : IChannel{ private readonly string _gatewayKey; public SmsChannel(string gatewayKey) => _gatewayKey = gatewayKey;
public void Send(string message) => Console.WriteLine($"[SMS] {message}");}
public class PushChannel : IChannel{ public void Send(string message) => Console.WriteLine($"[Push] {message}");}
// Abstraction — ถือ implementor ไว้ แล้ว delegate งานเจาะจงไปให้public abstract class Notification{ protected readonly IChannel Channel;
protected Notification(IChannel channel) => Channel = channel;
// control logic ระดับสูง แต่ละ subclass กำหนดรูปแบบข้อความของตัวเอง public abstract void Notify(string text);}
// RefinedAbstraction: แปรผันตาม "ชนิด" ของการแจ้งเตือน — อิสระจากช่องทางpublic class Alert : Notification{ public Alert(IChannel channel) : base(channel) { }
public override void Notify(string text) => Channel.Send($"[ALERT] {text}");}
public class Reminder : Notification{ public Reminder(IChannel channel) : base(channel) { }
public override void Notify(string text) => Channel.Send($"[Reminder] {text}");}
// การใช้งาน: ผสม abstraction ใดก็ได้ เข้ากับ implementor ใดก็ได้ ตอน runtimepublic class Program{ public static void Main() { IChannel channel = LoadChannelFromConfig(); // เลือกช่องทางตอนรัน
Notification alert = new Alert(channel); alert.Notify("disk usage เกิน 90%");
Notification reminder = new Reminder(new PushChannel()); reminder.Notify("ประชุม 15 นาทีข้างหน้า"); }
private static IChannel LoadChannelFromConfig() => new EmailChannel("smtp.internal");}เพิ่มชนิดการแจ้งเตือนใหม่ (เช่น DigestNotification) หรือช่องทางใหม่ (เช่น SlackChannel) ก็แค่เพิ่ม class ใหม่1 class ในฝั่งของมัน — ไม่ต้องแตะ code เดิมเลยแม้แต่บรรทัดเดียว ตรงตาม Open-Closed Principle
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- เมื่อ class มีมิติของการเปลี่ยนแปลงมากกว่าหนึ่งแกน และ inheritance ตรง ๆ จะทำให้เกิด class explosion (m×n class)
- เมื่อต้องการสลับ implementation ตอน runtime เช่น เปลี่ยน storage backend, renderer, หรือช่องทางส่งข้อความตาม config
- เมื่อต้อง share implementation เดียวกันระหว่าง abstraction หลายตัว โดยไม่อยากผูกด้วย inheritance
- เมื่อทั้ง abstraction และ implementation ควรขยายได้ด้วย subclass ของแต่ละฝั่งอย่างอิสระ
- เมื่อต้องซ่อนรายละเอียด platform-specific จาก client ทั้งหมด (cross-platform GUI, driver, protocol adapter)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- เมื่อมีมิติของการเปลี่ยนแปลงเพียงแกนเดียว — ใช้ inheritance ธรรมดาหรือ Strategy ก็เพียงพอแล้ว
- เมื่อ class มีความ cohesive สูงและไม่มีทีท่าว่าจะแตกแขนงเพิ่ม — การแยก hierarchy ล่วงหน้าเป็นการเพิ่ม abstraction โดยไม่จำเป็น ขัดกับ YAGNI
- เมื่อทีมยังไม่เจอความจำเป็นต้องสลับ implementation ตอน runtime หรือยังไม่มีสัญญาณของการขยายหลายแกนจริง ๆ — อย่ารีบ generalize ก่อนมีหลักฐาน
- เมื่อความซับซ้อนที่เพิ่มขึ้น (2 hierarchy, indirection เพิ่มขึ้น) ไม่คุ้มกับความยืดหยุ่นที่ได้ ในระบบเล็กที่ไม่ค่อยเปลี่ยนแปลง
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | เพิ่ม abstraction หรือ implementor ใหม่ได้อิสระ โดยไม่กระทบอีกฝั่ง (m+n แทน m×n) |
| ข้อดี | สลับ implementation ได้ตอน runtime ผ่าน composition |
| ข้อดี | ซ่อนรายละเอียด platform-specific จาก client — สอดคล้อง DIP |
| ข้อดี | แต่ละ hierarchy ทดสอบและพัฒนาแยกกันได้ (Single Responsibility ต่อฝั่ง) |
| ข้อเสีย | เพิ่มความซับซ้อนของ code — มี2 hierarchy และ indirection มากขึ้นสำหรับ use case ที่ไม่ซับซ้อนพอ |
| ข้อเสีย | ออกแบบยากตั้งแต่ต้น เพราะต้องมองเห็นแกนของการเปลี่ยนแปลงล่วงหน้าให้ถูก |
| ข้อเสีย | ถ้าใช้กับ class ที่ cohesive สูงและไม่ค่อยเปลี่ยน จะเป็นการเพิ่ม abstraction ที่ไม่คุ้มค่า |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Dependency Inversion Principle — หลักการที่ Bridge นำไปใช้โดยตรง (พึ่ง interface ของ Implementor)
- Open-Closed Principle — เพิ่ม abstraction/implementor ใหม่ได้โดยไม่แก้ code เดิม
- Strategy — โครงสร้างคล้ายกันมาก (composition + delegate) แต่ Strategy เน้นสลับ “algorithm” หนึ่งตัวแปร ส่วน Bridge เน้นแยก2 hierarchy คู่ขนาน
- Adapter — ทั้งคู่ห่อ object อื่นไว้ แต่ Adapter ใช้แก้ interface ที่เข้ากันไม่ได้ของ code ที่มีอยู่แล้ว ส่วน Bridge ออกแบบแยก2 hierarchy ไว้ตั้งแต่ต้น
- Abstract Factory — มักใช้คู่กับ Bridge เพื่อสร้าง concrete implementor ที่เข้ากันได้โดยที่ client ไม่ต้องรู้ชนิดจริง
- Dependency Injection — วิธีปฏิบัติที่ใช้ผูก Abstraction เข้ากับ Implementor ที่เลือกไว้