Dependency Inversion Principle
module ระดับสูงและระดับต่ำ ควรพึ่งพา abstraction ทั้งคู่
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Dependency Inversion Principle (DIP) เป็นตัว “D” ตัวสุดท้ายใน SOLID ซึ่ง Robert C. Martin เสนอไว้ในบทความ The Dependency Inversion Principle (C++ Report, 1996) และขยายความต่อในหนังสือ Agile Software Development, Principles, Patterns, and Practices หลักการนี้ประกอบด้วยกฎสองข้อ:
- module ระดับสูง (high-level module) ไม่ควรพึ่งพา module ระดับต่ำ (low-level module) ทั้งคู่ควรพึ่งพา abstraction
- Abstraction ไม่ควรพึ่งพารายละเอียด (detail) — รายละเอียดต่างหากที่ควรพึ่งพา abstraction
คำว่า “inversion” (การพลิกกลับ) หมายถึงการพลิกทิศทางการพึ่งพาที่มักเกิดขึ้นตามธรรมชาติใน code ปกติเมื่อ class A เรียกใช้ method ของ class B และ B เรียกใช้ method ของ C การพึ่งพา ณ compile-time ก็จะไหลไปในทิศทางเดียวกับการเรียกใช้งานตอน runtime คือ A → B → C ผลคือหาก C เปลี่ยน B และ A ก็อาจต้องเปลี่ยนตาม (transitive coupling)
DIP แก้ปัญหานี้ด้วยการแทรก abstraction (โดยทั่วไปคือ interface หรือ abstract class) ไว้ระหว่าง module ระดับสูงกับระดับต่ำ แล้วให้ module ระดับสูงเป็นเจ้าของ abstraction นั้น ไม่ใช่ module ระดับต่ำ เมื่อทำเช่นนี้ ทิศทางการพึ่งพา ณ compile-time จะ “พลิก” กลับ: module ระดับต่ำ (รายละเอียด/detail) จะต้อง implement interface ที่ module ระดับสูงกำหนดไว้ ในขณะที่ลำดับการเรียกใช้งานจริงตอน runtime ยังคงเหมือนเดิม
ข้อควรระวังที่สำคัญ (ตามที่ Martin Fowler เน้นย้ำใน DIP in the Wild): DIP ไม่เท่ากับ Dependency Injection หรือ Inversion of Control แม้ทั้งสามมักถูกใช้ร่วมกัน — Dependency Injection คือ เทคนิค ว่าจะส่ง dependency เข้าไปใน object อย่างไร ส่วน DIP คือ กฎการออกแบบ ว่าฝั่งไหนควรเป็นเจ้าของ abstraction และรายละเอียดควรพึ่งพาอะไร นอกจากนี้ abstraction ที่ดีตาม DIP ไม่ใช่แค่ “ใส่ interface ทับของเดิม” — interface ต้องสะท้อนภาษาของ domain ฝั่งผู้เรียกใช้ ไม่ใช่รายละเอียดทางเทคนิคของฝั่ง implementation (เช่น interface ที่มี method อย่าง GetAutoCommit หรือ CreateStatement ยังคงเป็น abstraction ที่ผูกกับรายละเอียดของฐานข้อมูลอยู่ดี แม้จะเป็น interface ก็ตาม)
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ลด coupling ที่ไหลเป็นทอด ๆ (transitive coupling) — เมื่อ UI สร้าง instance ของ Business Logic Layer (BLL) ตรง ๆ และ BLL ก็สร้าง instance ของ Data Access Layer (DAL) ตรง ๆ อีกที ทั้งระบบจะ tightly coupled กับฐานข้อมูลไปจนถึงชั้น UI จำไว้ว่า “New is Glue” — การเรียก
newโดยตรง (หรือเรียก static method) คือกาวที่ผูกทุกชั้นเข้าด้วยกัน - ทำให้ทดสอบได้ (testability) — เมื่อ module ระดับสูงพึ่งพา interface แทนที่จะพึ่งพา class จริง เราสามารถแทนที่ implementation ด้วย test double หรือ mock ได้โดยไม่ต้องแตะฐานข้อมูลจริง
- เปิดทางให้ plugin (plugin architecture) — เพราะ module ระดับต่ำ implement interface ที่ module ระดับสูงกำหนด เราจึงสลับ implementation ได้อย่างอิสระ (เช่น เปลี่ยนจาก SQL Server ไป in-memory store) โดยไม่กระทบตรรกะทางธุรกิจเลย
- เป็นแกนของสถาปัตยกรรมแบบ domain-centric — DIP คือกลไกเบื้องหลังที่ทำให้ Clean Architecture, Hexagonal Architecture, Onion Architecture และ Domain-Driven Design ทำงานได้ ชั้น domain/application เป็นผู้กำหนด interface (เช่น port หรือ Repository) ส่วนชั้น infrastructure เป็นผู้ implement — ทำให้ dependency ทุกเส้นชี้ เข้าหา domain ไม่ใช่ออกจาก domain
- สัมพันธ์กับหลักการอื่นใน SOLID — Robert Martin ระบุว่า DIP เป็นการรวมกันในระดับ first-class ของ Open/Closed Principle (ขยายพฤติกรรมผ่าน implementation ใหม่โดยไม่แก้ code เดิม) กับ Liskov Substitution Principle (implementation ใด ๆ ของ abstraction ต้องแทนที่กันได้อย่างปลอดภัย)
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ก่อน — tightly coupled: OrderProcessor (module ระดับสูง) สร้าง instance ของ SqlOrderRepository (module ระดับต่ำ) ขึ้นมาตรง ๆ ด้วย new การพึ่งพาไหลจากตรรกะทางธุรกิจลงไปสู่รายละเอียดของฐานข้อมูลโดยตรง ทดสอบ OrderProcessor แยกจากฐานข้อมูลจริงไม่ได้ และเปลี่ยน storage technology ไม่ได้โดยไม่แก้ OrderProcessor
// รายละเอียดระดับต่ำ ผูกกับ SQL โดยตรงpublic class SqlOrderRepository{ public void Save(Order order) { // เปิด connection แล้ว INSERT ลงตาราง Orders }}
// module ระดับสูง แต่กลับพึ่งพารายละเอียดระดับต่ำโดยตรงpublic class OrderProcessor{ private readonly SqlOrderRepository _repository = new SqlOrderRepository();
public void Process(Order order) { // ตรรกะทางธุรกิจ ... _repository.Save(order); }}หลัง — พึ่งพา abstraction ที่ module ระดับสูงเป็นเจ้าของ: OrderProcessor กำหนด interface IOrderRepository เอง (abstraction นี้อยู่ในชั้นเดียวกับตรรกะทางธุรกิจ ไม่ใช่ในชั้น data access) แล้วรับ implementation ผ่าน constructor ตาม Explicit Dependencies Principle ส่วน SqlOrderRepository ซึ่งเป็นรายละเอียด ก็แค่ implement interface นั้น ทิศทาง compile-time ของ SqlOrderRepository จึงพลิกกลับมาชี้เข้าหาชั้นตรรกะทางธุรกิจ
// abstraction ถูกกำหนดและเป็นเจ้าของโดยชั้นตรรกะทางธุรกิจpublic interface IOrderRepository{ void Save(Order order);}
// module ระดับสูง พึ่งพาเฉพาะ abstraction ไม่มี new และไม่มี static callpublic class OrderProcessor{ private readonly IOrderRepository _repository;
public OrderProcessor(IOrderRepository repository) { _repository = repository; }
public void Process(Order order) { // ตรรกะทางธุรกิจ ... _repository.Save(order); }}
// รายละเอียดระดับต่ำ implement interface ที่ระดับสูงกำหนดไว้public class SqlOrderRepository : IOrderRepository{ public void Save(Order order) { // เปิด connection แล้ว INSERT ลงตาราง Orders }}ในสถานการณ์จริง SqlOrderRepository จะถูกส่งเข้าไปให้ OrderProcessor ผ่านกลไก Dependency Injection เช่น DI container ตอน composition root — แต่นั่นเป็นเรื่องของ วิธีจัดหา implementation ไม่ใช่ตัว DIP เอง
แผนภาพด้านล่างแสดงทิศทางการพึ่งพาหลัง apply DIP: OrderProcessor (ระดับสูง) และ SqlOrderRepository (ระดับต่ำ) ต่างพึ่งพา IOrderRepository ซึ่งเป็น abstraction ที่ฝั่งระดับสูงเป็นเจ้าของ ไม่ใช่พึ่งพากันโดยตรงอีกต่อไป
flowchart LR
UI[UI Layer] --> Processor[OrderProcessor High Level]
Processor --> Abstraction[IOrderRepository Abstraction]
SqlRepo[SqlOrderRepository Low Level] --> Abstraction
SqlRepo --> DB[Database]
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- class ตรรกะทางธุรกิจมี keyword
newเพื่อสร้าง collaborator ของตัวเอง (โดยเฉพาะ collaborator ที่คุยกับฐานข้อมูล file เครือข่าย หรือระบบภายนอกอื่น ๆ) หรือเรียก static method ที่ทำสิ่งเดียวกัน — ทั้งสองแบบคือ New is Glue - การพึ่งพาไหลเป็นทอด ๆ ผ่านหลายชั้น (UI → BLL → DAL → DB) จนเปลี่ยนชั้นล่างสุดแล้วกระทบชั้นบนสุดโดยไม่คาดคิด
- interface ถูกกำหนดและอยู่ใน “ชั้น” เดียวกับ implementation ของมัน (เช่นอยู่ใน assembly หรือ namespace ของ data-access) แทนที่จะอยู่ใน namespace ของชั้นตรรกะทางธุรกิจที่เป็นผู้ใช้งาน — เจ้าของ abstraction ผิดฝั่ง
- มี interface แต่กลับสะท้อนรายละเอียดทางเทคนิคของ implementation (เช่น method ชื่อ
ExecuteQuery,GetAutoCommit) แทนที่จะสะท้อนภาษาของ domain — เป็น “abstraction ปลอม” ที่ยังผูกกับรายละเอียดอยู่ - เขียน unit test ของตรรกะทางธุรกิจไม่ได้เลยถ้าไม่มีฐานข้อมูล file หรือ service ภายนอกพร้อมใช้งานจริง
- ใช้ DIP แบบสุดโต่ง คือใส่ interface ให้ทุก class โดยไม่มีเหตุผล ทำให้เกิด boilerplate จำนวนมากโดยไม่ได้ประโยชน์ด้านความยืดหยุ่นจริง (over-engineering)
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Explicit Dependencies Principle
- Hollywood Principle
- Inversion of Control
- Dependency Injection
- Repository
- SOLID