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

Facade

ให้ interface ที่​เรียบ​ง่าย​กว่า​สำหรับ​ระบบ​ย่อย​ที่​ซับซ้อน

Facade ใช้​เมื่อ​คุณ​ต้องการ interface ที่​เรียบ​ง่าย​กว่า สำหรับ​ระบบ​ย่อย​ของ​ระบบ​ที่​ซับซ้อน โดย​สร้าง interface ที่​แสดง​เฉพาะ​จุด​เชื่อม​ที่​จำเป็น​สำหรับ​ส่วน​ย่อย​ของ​ระบบ แทนที่​จะ​เปิด​ระบบ​ซับซ้อน​ทั้งหมด

ปัญหา​ที่ Facade แก้​คือ: เมื่อ​ระบบ​เติบโต​ขึ้น มัน​มัก​ประกอบ​ด้วย​หลาย class ที่​ต้อง​เรียก​ตาม​ลำดับ​ที่​ถูกต้อง, ต้อง​ตั้ง​ค่า configuration ที่​ยุ่งยาก, หรือ​ต้อง​รู้ความ​สัมพันธ์​ภายใน​ของ subsystem นั้น​อย่าง​ละเอียด — ถ้า​ปล่อย​ให้ client เรียก​ใช้ subsystem โดยตรง​ทุก​จุด code ของ client จะ​ผูก​ติด (coupling) กับ​รายละเอียด​ภายใน​ของ subsystem จน​แก้ไข​หรือ​อัปเกรด​ยาก

แนวคิด​หลัก​คือ​สร้าง class “หน้า​ตึก” (facade) ขึ้น​มา class หนึ่ง ที่​รู้​วิธี​ประสาน​งาน subsystem ทั้งหมด​แทน client แล้ว​เปิดเผย​เฉพาะ method ระดับ​สูง​ไม่​กี่​ตัว​ที่ client ต้องการ​จริง ๆ ส่วน​รายละเอียด​การ​เรียง​ลำดับ การ​จัดการ error หรือ​การ​แปลง​ข้อมูล​ระหว่าง subsystem จะ​ถูก​ซ่อน​ไว้​ใน​ตัว facade เอง

ข้อสังเกต​สำคัญ: ระบบ​ย่อย​ไม่รู้​ว่า​มี facade อยู่ — มี​การ​อ้างอิง​จาก facade ไป​ยัง​ระบบ​ย่อย แต่​ไม่มี​จาก​ระบบ​ย่อย​กลับ​มา (การ​พึ่งพา​เป็น​ทาง​เดียว​เสมอ) facade ยัง​ทำ​หน้าที่​เป็น​ประตู​เดียว​ได้​คล้าย Single Point of Enforcement และ​ช่วย​ลด​การ​ละเมิด Law of Demeter เพราะ client คุย​กับ object เดียว​แทนที่​จะ​ไล่​เรียก​ผ่าน​ห่วง​โซ่​ของ subsystem

Facade ไม่​ได้​เพิ่ม​ความ​สามารถ​ใหม่​ให้ subsystem — มัน​แค่ จัด​ระเบียบการ​เข้าถึง​สิ่ง​ที่​มี​อยู่​แล้ว ให้​สะดวก​ขึ้น subsystem ยัง​คง​เรียก​ใช้ได้​โดยตรง​สำหรับ client ที่​ต้องการ​ความ​ยืดหยุ่น​เต็ม​รูปแบบ (facade เป็น​ทาง​เลือก ไม่ใช่​การ​ปิด​กั้น)

classDiagram
    class Client {
    }
    class OrderFacade {
      -Inventory inventory
      -Payment payment
      -Shipping shipping
      +PlaceOrder(cart)
    }
    class Inventory {
      +Reserve(cart)
      +Release(cart)
    }
    class Payment {
      +Charge(amount)
      +Refund(amount)
    }
    class Shipping {
      +Dispatch(cart)
      +Track(orderId)
    }
    Client --> OrderFacade
    OrderFacade --> Inventory
    OrderFacade --> Payment
    OrderFacade --> Shipping
  • Client — code ที่​ต้องการ​ใช้งาน subsystem แต่​ไม่​อยาก​รู้​รายละเอียด​ภายใน
  • Facade (OrderFacade) — class เดียว​ที่ client เห็น รู้​ว่า​ต้อง​เรียก subsystem ไหน​ตาม​ลำดับ​ใด และ​แปลง request ของ client ให้​เป็นการ​เรียก​ที่ subsystem เข้าใจ
  • Subsystem classes (Inventory, Payment, Shipping) — class ที่​ทำงาน​จริง ซับซ้อน มี​สถานะ​และ​กฎ​ของ​ตัวเอง แต่​ไม่รู้จัก facade เลย จึง​ยัง​คง​ใช้งาน​แยก​ได้​อิสระ​โดย client อื่น​ที่​ไม่​ผ่าน facade

บาง​ครั้ง​ระบบ​ใหญ่​มาก​อาจ​มี additional facade หลาย​ตัว แบ่ง​ตาม use case ของ client แต่ละ​กลุ่ม เพื่อ​ไม่​ให้ facade ตัว​เดียว​บวม​จน​กลาย​เป็น god object เอง

เมื่อ client เรียก method ของ facade เพียง​ครั้ง​เดียว facade จะ​กระจาย​งาน​ไป​ยัง subsystem หลาย​ตัว​ตาม​ลำดับ​ที่​ถูกต้อง จัดการ​ผลลัพธ์​ระหว่าง​ทาง แล้ว​คืน​ผลลัพธ์​เดียว​กลับ​ไป​ให้ client — client ไม่​ต้อง​รู้​เลย​ว่า​เบื้องหลัง​มี​การ​เรียก​กี่​ขั้นตอน

sequenceDiagram
    participant C as Client
    participant F as OrderFacade
    participant I as Inventory
    participant P as Payment
    participant S as Shipping
    C->>F: PlaceOrder(cart)
    F->>I: Reserve(cart)
    I-->>F: reserved
    F->>P: Charge(cart.Total)
    P-->>F: charged
    F->>S: Dispatch(cart)
    S-->>F: dispatched
    F-->>C: OrderResult

จุด​สำคัญ​ของ​ลำดับ​นี้​คือ facade เป็น​ที่​เดียว​ที่​รู้​ว่า “ต้อง​จอง​สต๊อก​ก่อน​ตัด​เงิน แล้ว​ค่อย​ส่ง​ของ” — ถ้า​วัน​หนึ่ง​ลำดับ​ต้อง​เปลี่ยน (เช่น ต้อง​ตรวจ fraud ก่อน charge) จะ​แก้ไข​ที่​จุด​เดียว​ใน​ตัว facade โดย​ไม่​กระทบ client ที่​เรียก​ใช้​อยู่​ทั้งหมด และ​ไม่​กระทบ subsystem เดิม​เลย

// Subsystem 1: จัดการสต๊อกสินค้า มีกฎภายในของตัวเอง
public class Inventory
{
public void Reserve(Cart cart)
{
foreach (var item in cart.Items)
{
// ตรวจสอบและล็อกสต๊อกไว้ชั่วคราว
Console.WriteLine($"จองสินค้า {item.Sku} จำนวน {item.Quantity}");
}
}
public void Release(Cart cart)
{
Console.WriteLine("คืนสต๊อกที่จองไว้ (ใช้เมื่อ rollback)");
}
}
// Subsystem 2: จัดการการชำระเงิน
public class Payment
{
public string Charge(decimal amount)
{
Console.WriteLine($"เรียกเก็บเงินจำนวน {amount:C}");
return "txn-12345"; // transaction id
}
public void Refund(string transactionId)
{
Console.WriteLine($"คืนเงินสำหรับ transaction {transactionId}");
}
}
// Subsystem 3: จัดการการจัดส่ง
public class Shipping
{
public void Dispatch(Cart cart)
{
Console.WriteLine("สร้างใบจัดส่งและมอบหมายให้ courier");
}
}
// Facade: จุดเชื่อมเดียวที่ client เรียก — ซ่อน 3 subsystem ไว้เบื้องหลัง
public class OrderFacade
{
private readonly Inventory _inventory = new();
private readonly Payment _payment = new();
private readonly Shipping _shipping = new();
public OrderResult PlaceOrder(Cart cart)
{
// ลำดับขั้นตอนที่ถูกต้องถูกซ่อนไว้ใน method เดียวนี้
_inventory.Reserve(cart);
string transactionId;
try
{
transactionId = _payment.Charge(cart.Total);
}
catch (PaymentException)
{
// ถ้าชำระเงินไม่สำเร็จ ต้องคืนสต๊อกที่จองไว้ — logic ประสานงานแบบนี้
// คือสิ่งที่ facade มีไว้ซ่อน ไม่ให้ client ต้องรู้
_inventory.Release(cart);
throw;
}
_shipping.Dispatch(cart);
return new OrderResult(transactionId, "สั่งซื้อสำเร็จ");
}
}
// Client เห็นแค่ interface เดียว ไม่ต้องรู้จัก Inventory, Payment, Shipping เลย
var facade = new OrderFacade();
var result = facade.PlaceOrder(cart);
  • เมื่อ subsystem มี​หลาย class ที่ client ส่วน​ใหญ่​ต้อง​เรียก​ร่วม​กัน​ตาม​ลำดับ​ที่​ตายตัว
  • เมื่อ​ต้องการ​ลด coupling ระหว่าง code ของ client กับ​รายละเอียด​ภายใน​ของ library หรือ framework ภายนอก เพื่อ​ให้​อัปเกรด​หรือ​เปลี่ยน implementation ใน​อนาคต​ได้​ง่าย​ขึ้น
  • เมื่อ​กำลัง​ทำ refactor ระบบ​เดิม (legacy) แบบ​ค่อย​เป็น​ค่อย​ไป — สร้าง facade ที่​มี interface แบบ​ที่ “อยาก​ให้​เป็น” ก่อน แล้ว​ค่อย ๆ ย้าย logic เข้าไป​ที​ละ​ส่วน​โดย​ไม่​กระทบ client (แนวทาง​แบบ strangler fig)
  • เมื่อ​ต้องการ​จุด​เดียว​สำหรับ enforce กติกา​หรือ validation ก่อน​เข้าถึง subsystem หลาย​ตัว
  • เมื่อ​ต้องการ​ให้​ทดสอบ​ง่าย​ขึ้น เพราะ​ทดสอบ​ผ่าน facade จุด​เดียว​แทนที่​จะ mock subsystem หลาย​ตัว​ใน​ทุก​จุด​ที่​เรียก​ใช้
  • เมื่อ client บาง​กลุ่ม​ต้องการ​ความ​ยืดหยุ่น​หรือ fine-grained control เหนือ subsystem แต่ละ​ตัว​จริง ๆ — การ​บังคับ​ผ่าน facade เดียว​จะ​จำกัด​ความ​สามารถ​ของ​พวก​เขา​โดย​ไม่​จำเป็น (แก้​ได้​ด้วย​การ​เปิด​ให้​เข้าถึง subsystem ตรง​ได้​ควบคู่​ไป​กับ facade)
  • เมื่อ subsystem เรียบ​ง่าย​อยู่​แล้ว การ​เพิ่ม facade จะ​เป็น​เพียง indirection ที่​ไม่​ได้​ลด​ความ​ซับซ้อน​จริง
  • เมื่อ​พยายาม​ยัด​ทุก feature ของ​ทุก client เข้าไป​ใน facade ตัว​เดียว จน​มัน​บวม​กลาย​เป็น god object ที่​ผูก​กับ​ทุก​อย่าง​ใน​ระบบ (ควร​แยก​เป็น​หลาย facade ตาม use case แทน)
  • เมื่อ facade เริ่ม​มี business logic ของ​ตัวเอง​มาก​ขึ้น​เรื่อย ๆ แทนที่​จะ​แค่​ประสาน​งาน — นั่น​เป็น​สัญญาณ​ว่า logic นั้น​ควร​อยู่​ใน domain layer ไม่ใช่​ใน facade
ด้านรายละเอียด
ข้อดีลด coupling ระหว่าง client กับ​รายละเอียด​ภายใน​ของ subsystem
ข้อดีทำให้ code ฝั่ง client อ่าน​ง่าย​ขึ้น เพราะ​เรียก method เดียว​แทน​หลาย​ขั้นตอน
ข้อดีเปลี่ยน​หรือ​อัปเกรด subsystem ภายใน​ได้​โดย​ไม่​กระทบ client ตราบ​ใด​ที่ signature ของ facade ไม่​เปลี่ยน
ข้อดีเป็น​จุด​เริ่มต้น​ที่​ดี​สำหรับ refactor ระบบ​เดิม​แบบ​ค่อย​เป็น​ค่อย​ไป
ข้อ​เสียเสี่ยง​กลาย​เป็น god object ที่​ผูก​กับ​ทุก class ใน application ถ้า​ไม่​ระวัง​ขอบเขต
ข้อ​เสียเพิ่ม​ชั้น indirection อีก​ชั้น​หนึ่ง ซึ่ง​อาจ​ไม่​จำเป็น​ถ้า subsystem เรียบ​ง่าย​อยู่​แล้ว
ข้อ​เสียถ้า client ต้องการ​ความ​สามารถ​ที่ facade ไม่​ได้​เปิด​ให้ ต้อง​แก้ facade หรือ bypass มัน​ไป​เรียก subsystem ตรง ๆ
  • Adapter — คล้าย Facade ตรง​ที่​ห่อ interface อื่น​ไว้ แต่ Adapter มี​ไว้​เพื่อ​แปลง interface ที่​เข้า​กัน​ไม่​ได้​ให้​ใช้งาน​ร่วม​กัน​ได้ ไม่ใช่​เพื่อ​ทำให้​เรียบ​ง่าย​ขึ้น
  • Bridge — แยก abstraction ออก​จาก implementation เพื่อ​ให้​ทั้ง​สอง​ฝั่ง​เปลี่ยนแปลง​อิสระ​จาก​กัน ต่าง​จาก Facade ที่​มุ่ง​ลด​ความ​ซับซ้อน​ของ​การ​เรียก​ใช้
  • Composite — จัด​กลุ่ม object เป็น​โครงสร้าง​ต้นไม้​ที่​เรียก​ใช้​ผ่าน interface เดียวกัน มัก​ใช้​ร่วม​กับ Facade ได้
  • Mediator — คล้าย Facade ตรง​ที่​ประสาน​งาน​หลาย object แต่ Mediator เน้น​ความ​สัมพันธ์​แบบ​สอง​ทาง​ระหว่าง object ที่​รู้จัก mediator นั้น ต่าง​จาก subsystem ของ Facade ที่​ไม่รู้จัก facade เลย
  • Single Point of Enforcement — Facade เป็น​เทคนิค​หนึ่ง​ที่​ช่วย​สร้าง​ประตู​ทาง​เดียว​สำหรับ enforce กติกา​ก่อน​เข้าถึง​ระบบ​ย่อย
  • Law of Demeter — Facade ช่วย​ลด​การ​ละเมิด​กฎ​นี้ เพราะ client คุย​กับ object เดียว​แทนที่​จะ​ไล่​เรียก​ผ่าน​ห่วง​โซ่​ของ subsystem