ข้ามไปยังเนื้อหา

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
ลำดับชั้นของรูปทรงเชื่อม (bridge) ไปยังลำดับชั้นของ renderer ที่แยกออกมา
  • 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=5
console.log(new Circle(new RasterRenderer(), 5).draw()); // raster pixels for circle r=5
  • ข้อดี: abstraction และ implementation เติบโตบนแกนที่อิสระต่อกัน หลีกเลี่ยงการระเบิดของ class แบบ combinatorial
  • ข้อดี: คุณสามารถสลับ implementation ได้ตอน runtime เพราะ abstraction ถือเพียง reference เท่านั้น
  • ข้อดี: แต่ละลำดับชั้นสามารถพัฒนา ทดสอบ และต่อยอดได้โดยไม่ต้องแตะอีกฝั่ง
  • ข้อเสีย: pattern นี้เพิ่ม indirection และชิ้นส่วนที่เคลื่อนไหวมากขึ้นตั้งแต่แรก ซึ่งเกินจำเป็นเมื่อคุณมี implementation เพียงตัวเดียวเสมอ
  • ข้อเสีย: การออกแบบ interface ทั้งสองให้ดีต้องอาศัยการมองการณ์ไกลว่ามิติใดที่เปลี่ยนแปลงจริง ๆ
  • Adapter ทำให้ interface สองตัวที่มีอยู่แล้วทำงานร่วมกันได้ในภายหลัง ส่วน Bridge วางแผนไว้ตั้งแต่ต้นเพื่อให้สองลำดับชั้นวิวัฒน์แยกกัน
  • Abstract Factory สามารถสร้างและจับคู่ implementor ที่ถูกต้องเข้ากับ abstraction ได้ มักถูกใช้ควบคู่ไปกับ Bridge
BridgeAdapterStrategy
จุดประสงค์แยก abstraction–implementation ตั้งแต่ต้นแก้ interface ที่ไม่ตรงกัน (retro-fit)สลับ algorithm ในขณะ run
เวลาที่ใช้ออกแบบล่วงหน้าแก้ไข code เดิมที่มีอยู่แล้วเลือก algorithm ที่ต้องการ
dimension ที่ขยายabstraction + implementation (2 มิติ)1 ด้าน (adaptee)1 ด้าน (algorithm)
ตัวอย่างplatform renderer, DB driverlegacy API wrapperpricing rule, sort strategy
Bridge pattern แยกอะไรออกจากกัน?
Bridge ป้องกันปัญหาอะไรเมื่อเทียบกับ inheritance ธรรมดา?
ในตัวอย่าง Renderer มีบทบาทอะไร?
Bridge ต่างจาก Adapter อย่างไรในแง่เจตนา?