Bridge
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Bridge แยก abstraction ออกจาก implementation โดยให้แต่ละส่วนอยู่ในลำดับชั้น class ของตัวเอง เพื่อให้ทั้งสองเปลี่ยนแปลงและเติบโตได้อย่างอิสระต่อกันแทนที่จะทวีคูณรวมกัน
สมมติว่าคุณมีรูปทรง คือ วงกลม สี่เหลี่ยม และคุณต้องการวาดแต่ละอันด้วย rendering backend ที่ต่างกัน เช่น vector renderer และ raster renderer เส้นทางที่ง่ายที่สุดคือการ inheritance ได้แก่ VectorCircle, RasterCircle, VectorSquare, RasterSquare เพิ่มรูปทรงตัวที่สามหรือ backend ตัวที่สามแล้วจำนวน class จะระเบิดเป็นผลคูณของสองมิติ ที่แย่กว่านั้นคือตอนนี้ทุกรูปทรงต้องรู้จัก backend ทุกตัว
Bridge แยกผลคูณนั้นออกจากกัน ลำดับชั้นหนึ่งจำลอง abstraction (รูปทรงและการดำเนินการระดับสูงของตัวเอง) อีกลำดับชั้นหนึ่งจำลอง implementation (พื้นฐานของการ render) abstraction ถือ reference ไปยัง object ของ implementation และ delegate งานระดับล่างให้กับ object นั้น ตอนนี้รูปทรงและ renderer เปลี่ยนแปลงบนแกนที่อิสระต่อกัน คือ สามรูปทรงบวกสอง renderer หมายถึงห้า class ไม่ใช่หก และการเพิ่มฝั่งใดฝั่งหนึ่งก็ไม่ไปแตะอีกฝั่ง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Shape {
<<abstract>>
#renderer: Renderer
+draw()
}
class Circle {
+draw()
}
class Square {
+draw()
}
class Renderer {
<<interface>>
+drawCircle(r)
+drawRectangle(w, h)
}
class VectorRenderer
class RasterRenderer
Shape <|-- Circle
Shape <|-- Square
Shape o--> Renderer : delegates to
Renderer <|.. VectorRenderer
Renderer <|.. RasterRenderer - Abstraction (
Shape) — interface ระดับสูงที่ client ใช้ ถือ reference ไปยัง implementor และ delegate การดำเนินการพื้นฐานให้ implementor - Refined Abstraction (
Circle,Square) — รูปแบบที่เป็นรูปธรรมของ abstraction ซึ่งอาจเพิ่มการดำเนินการของตัวเอง - Implementor (
Renderer) — interface สำหรับการดำเนินการระดับล่างที่ abstraction พึ่งพา - Concrete Implementor (
VectorRenderer,RasterRenderer) — implementation จริงที่ abstraction สามารถจับคู่ด้วยได้ตอน runtime
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”รูปทรง delegate การวาดของตัวเองไปยัง renderer ที่เสียบเปลี่ยนได้ รูปทรงใด ๆ ทำงานได้กับ renderer ใด ๆ โดยเลือกตอนที่รูปทรงถูกสร้างขึ้น
// Implementor: the low-level drawing operations.interface Renderer { drawCircle(radius: number): string; drawRectangle(width: number, height: number): string;}
class VectorRenderer implements Renderer { drawCircle = (r: number) => `vector circle r=${r}`; drawRectangle = (w: number, h: number) => `vector rect ${w}x${h}`;}
class RasterRenderer implements Renderer { drawCircle = (r: number) => `raster pixels for circle r=${r}`; drawRectangle = (w: number, h: number) => `raster pixels for rect ${w}x${h}`;}
// Abstraction: holds a renderer and delegates to it.abstract class Shape { constructor(protected readonly renderer: Renderer) {} abstract draw(): string;}
class Circle extends Shape { constructor(renderer: Renderer, private readonly radius: number) { super(renderer); } draw = () => this.renderer.drawCircle(this.radius);}
console.log(new Circle(new VectorRenderer(), 5).draw()); // vector circle r=5console.log(new Circle(new RasterRenderer(), 5).draw()); // raster pixels for circle r=5from abc import ABC, abstractmethodfrom typing import Protocol
class Renderer(Protocol): def draw_circle(self, radius: float) -> str: ... def draw_rectangle(self, width: float, height: float) -> str: ...
class VectorRenderer: def draw_circle(self, radius: float) -> str: return f"vector circle r={radius}"
def draw_rectangle(self, width: float, height: float) -> str: return f"vector rect {width}x{height}"
class RasterRenderer: def draw_circle(self, radius: float) -> str: return f"raster pixels for circle r={radius}"
def draw_rectangle(self, width: float, height: float) -> str: return f"raster pixels for rect {width}x{height}"
class Shape(ABC): def __init__(self, renderer: Renderer) -> None: self._renderer = renderer
@abstractmethod def draw(self) -> str: ...
class Circle(Shape): def __init__(self, renderer: Renderer, radius: float) -> None: super().__init__(renderer) self._radius = radius
def draw(self) -> str: return self._renderer.draw_circle(self._radius)
print(Circle(VectorRenderer(), 5).draw()) # vector circle r=5print(Circle(RasterRenderer(), 5).draw()) # raster pixels for circle r=5package main
import "fmt"
// Implementor: the low-level drawing operations.type Renderer interface { DrawCircle(radius float64) string DrawRectangle(width, height float64) string}
type VectorRenderer struct{}
func (VectorRenderer) DrawCircle(r float64) string { return fmt.Sprintf("vector circle r=%g", r)}func (VectorRenderer) DrawRectangle(w, h float64) string { return fmt.Sprintf("vector rect %gx%g", w, h)}
type RasterRenderer struct{}
func (RasterRenderer) DrawCircle(r float64) string { return fmt.Sprintf("raster pixels for circle r=%g", r)}func (RasterRenderer) DrawRectangle(w, h float64) string { return fmt.Sprintf("raster pixels for rect %gx%g", w, h)}
// Abstraction: composes a renderer.type Circle struct { renderer Renderer radius float64}
func (c Circle) Draw() string { return c.renderer.DrawCircle(c.radius) }
func main() { fmt.Println(Circle{VectorRenderer{}, 5}.Draw()) // vector circle r=5 fmt.Println(Circle{RasterRenderer{}, 5}.Draw()) // raster pixels for circle r=5}// Implementor: the low-level drawing operations.trait Renderer { fn draw_circle(&self, radius: f64) -> String; fn draw_rectangle(&self, width: f64, height: f64) -> String;}
struct VectorRenderer;impl Renderer for VectorRenderer { fn draw_circle(&self, r: f64) -> String { format!("vector circle r={r}") } fn draw_rectangle(&self, w: f64, h: f64) -> String { format!("vector rect {w}x{h}") }}
struct RasterRenderer;impl Renderer for RasterRenderer { fn draw_circle(&self, r: f64) -> String { format!("raster pixels for circle r={r}") } fn draw_rectangle(&self, w: f64, h: f64) -> String { format!("raster pixels for rect {w}x{h}") }}
// Abstraction: holds a renderer and delegates to it.struct Circle<'a> { renderer: &'a dyn Renderer, radius: f64,}
impl Circle<'_> { fn draw(&self) -> String { self.renderer.draw_circle(self.radius) }}
fn main() { let circle = Circle { renderer: &VectorRenderer, radius: 5.0 }; println!("{}", circle.draw()); // vector circle r=5 let circle = Circle { renderer: &RasterRenderer, radius: 5.0 }; println!("{}", circle.draw()); // raster pixels for circle r=5}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: abstraction และ implementation เติบโตบนแกนที่อิสระต่อกัน หลีกเลี่ยงการระเบิดของ class แบบ combinatorial
- ข้อดี: คุณสามารถสลับ implementation ได้ตอน runtime เพราะ abstraction ถือเพียง reference เท่านั้น
- ข้อดี: แต่ละลำดับชั้นสามารถพัฒนา ทดสอบ และต่อยอดได้โดยไม่ต้องแตะอีกฝั่ง
- ข้อเสีย: pattern นี้เพิ่ม indirection และชิ้นส่วนที่เคลื่อนไหวมากขึ้นตั้งแต่แรก ซึ่งเกินจำเป็นเมื่อคุณมี implementation เพียงตัวเดียวเสมอ
- ข้อเสีย: การออกแบบ interface ทั้งสองให้ดีต้องอาศัยการมองการณ์ไกลว่ามิติใดที่เปลี่ยนแปลงจริง ๆ
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Adapter ทำให้ interface สองตัวที่มีอยู่แล้วทำงานร่วมกันได้ในภายหลัง ส่วน Bridge วางแผนไว้ตั้งแต่ต้นเพื่อให้สองลำดับชั้นวิวัฒน์แยกกัน
- Abstract Factory สามารถสร้างและจับคู่ implementor ที่ถูกต้องเข้ากับ abstraction ได้ มักถูกใช้ควบคู่ไปกับ Bridge
| Bridge | Adapter | Strategy | |
|---|---|---|---|
| จุดประสงค์ | แยก abstraction–implementation ตั้งแต่ต้น | แก้ interface ที่ไม่ตรงกัน (retro-fit) | สลับ algorithm ในขณะ run |
| เวลาที่ใช้ | ออกแบบล่วงหน้า | แก้ไข code เดิมที่มีอยู่แล้ว | เลือก algorithm ที่ต้องการ |
| dimension ที่ขยาย | abstraction + implementation (2 มิติ) | 1 ด้าน (adaptee) | 1 ด้าน (algorithm) |
| ตัวอย่าง | platform renderer, DB driver | legacy API wrapper | pricing rule, sort strategy |