Stable Dependencies
จงพึ่งพาไปในทิศทางของความเสถียร
Stable Dependencies Principle (SDP) กล่าวว่า “การพึ่งพาระหว่าง package ซอฟต์แวร์ควรอยู่ในทิศทางของความเสถียร (stability) ของ package นั่นคือ package หนึ่งควรพึ่งพาเฉพาะ package ที่เสถียรกว่าเท่านั้น” หลักการนี้เป็นหนึ่งในสามหลักการจัดวาง package (package coupling principles) ที่ Robert C. Martin เสนอไว้คู่กับ Acyclic Dependencies Principle (ห้ามมีวงจรการพึ่งพา) และ Stable Abstractions Principle (package ที่เสถียรควรเป็นนามธรรมด้วย เพื่อให้ยังขยายได้)
เมื่อใดที่ package หนึ่งเปลี่ยนแปลง package ทั้งหมดที่พึ่งพามันต้องถูกตรวจสอบเพื่อยืนยันว่ายังทำงานตามที่คาดหลังการเปลี่ยนแปลง ดังนั้น ยิ่งมี package จำนวนมากพึ่งพา package ที่ไม่เสถียร (unstable) การรบกวนก็ยิ่งมากขึ้นทุกครั้งที่มันเปลี่ยน การจัดทิศทางการพึ่งพาให้ไหลไปสู่ความเสถียรจึงช่วยลดแรงกระเพื่อมของการเปลี่ยนแปลงลง
Writing software that fully meets its specifications is like walking on water. For each, the former is easy if the latter is frozen and near impossible if fluid.
— Anonymous
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”สิ่งสำคัญที่มักเข้าใจผิดคือ “ความเสถียร” ในที่นี้ ไม่ได้หมายถึงความถี่ที่ code เปลี่ยนแปลง แต่หมายถึง ต้นทุนหรือความยากในการเปลี่ยนแปลงมัน package จะเสถียรก็ต่อเมื่อมีสิ่งอื่นจำนวนมากพึ่งพามันอยู่ — ยิ่งมีผู้พึ่งพามาก การเปลี่ยนมันก็ยิ่งมีความเสี่ยงและต้นทุนสูงขึ้น (แม้ตัวมันเองจะแทบไม่เคยเปลี่ยนก็ตาม) ในทางกลับกัน package ที่แทบไม่มีใครพึ่งพาก็ “เปลี่ยนได้ง่าย” เพราะแรงกระเพื่อมของการเปลี่ยนแปลงมีวงจำกัด
Martin เสนอ metric เชิงปริมาณเรียกว่า Instability (I) เพื่อวัดสิ่งนี้:
I = Ce / (Ca + Ce)โดย
- Ca (Afferent Coupling) = จำนวน class/package ภายนอกที่ พึ่งพาเข้ามาหา package นี้ (incoming dependencies)
- Ce (Efferent Coupling) = จำนวน class/package ภายนอกที่ package นี้ พึ่งพาออกไป (outgoing dependencies)
ค่า I อยู่ระหว่าง 0 ถึง 1 — package ที่มี I เข้าใกล้ 0 (Ca สูง, Ce ต่ำ) คือ package ที่เสถียรมาก มักเป็น package แกนกลาง เช่น Domain layer หรือ library พื้นฐาน ส่วน package ที่มี I เข้าใกล้ 1 (Ce สูง, Ca ต่ำ) คือ package ที่ไม่เสถียร เปลี่ยนได้อย่างอิสระโดยแทบไม่กระทบใคร เช่น project Infrastructure หรือ UI ปลายทาง
Stable Dependencies Principle จึงสรุปสั้น ๆ ว่า: graph การพึ่งพาของระบบควรไหลจาก package ที่มี I สูง (ไม่เสถียร) ไปหา package ที่มี I ต่ำ (เสถียร) เสมอ — ไม่ควรมี package ที่เสถียรพึ่งพา package ที่ไม่เสถียร เพราะนั่นหมายความว่าส่วนที่ “แข็ง” ของระบบถูกดึงให้สั่นไหวตามส่วนที่ “อ่อน” กว่า
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”หัวใจของหลักการนี้คือการควบคุม blast radius ของการเปลี่ยนแปลง เมื่อ package หนึ่งเปลี่ยน ทุก package ที่พึ่งพามันต้อง build ใหม่ ทดสอบใหม่ และตรวจสอบพฤติกรรมใหม่ หาก package ที่มีผู้พึ่งพาจำนวนมาก (Domain, Core, Shared Kernel) ดันไปพึ่งพา package ที่เปลี่ยนบ่อย (SDK ของผู้ให้บริการภายนอก, UI framework version ล่าสุด, การตั้งค่าเฉพาะสภาพแวดล้อม) การเปลี่ยนแปลงเล็กน้อยเพียงจุดเดียวจะไหลย้อนกลับขึ้นไปสั่นสะเทือนทั้งระบบ
เทคนิคเชิงปฏิบัติที่ทำให้ SDP เป็นจริงในระดับ class คือ Dependency Inversion Principle (DIP) — แทนที่ Domain (ซึ่งควรเสถียรที่สุด) จะอ้างอิง class รูปธรรมของ Infrastructure (ซึ่งไม่เสถียร) โดยตรง เราให้ Domain เป็นเจ้าของ interface หรือ abstraction แล้วให้ Infrastructure เป็นฝ่ายพึ่งพา (implement) abstraction นั้นแทน ทิศทางการพึ่งพาระดับ compile จึงถูก “กลับด้าน” ให้สอดคล้องกับทิศทางความเสถียร ในขณะที่ทิศทางการเรียกใช้งานจริงตอน runtime ยังเหมือนเดิม เอกสาร Architectural Principles ของ Microsoft อธิบายภาพนี้ว่าเป็นการเปลี่ยนจาก “direct dependency graph” ไปเป็น “inverted dependency graph”
ผลลัพธ์ของการละเมิด SDP มักปรากฏเป็นอาการเหล่านี้: การทดสอบ unit test ของ Domain ต้อง mock หรือพึ่งพา infrastructure จริง, การอัปเกรด library ภายนอกหนึ่งตัวทำให้ต้องแก้ code หลาย10 file ที่ไม่เกี่ยวข้องกันโดยตรง, และทีมกลัวที่จะแตะ package แกนกลางเพราะไม่รู้ว่าอะไรจะพังบ้าง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”แผนภาพต่อไปนี้แสดงทิศทางการพึ่งพาที่ถูกต้อง — ทั้ง UI และ Infrastructure (ไม่เสถียร, เปลี่ยนบ่อย) ต่างพึ่งพาเข้าหา Domain (เสถียร, มีผู้พึ่งพามาก) ไม่มีลูกศรใดวิ่งออกจาก Domain ไปหาชั้นอื่น:
graph LR
UI[UI Layer] --> App[Application Layer]
App --> Domain[Domain Core]
Infra[Infrastructure Layer] --> Domain
ก่อน (ไม่ดี) — Domain อ้างอิง class รูปธรรมจาก Infrastructure โดยตรง ทำให้ package ที่ควรเสถียรที่สุดกลับผูกติดกับ package ที่เปลี่ยนบ่อยที่สุด (เช่น เปลี่ยนผู้ให้บริการอีเมล เปลี่ยน library SMTP):
namespace Domain{ public class OrderService { // อ้างอิง class รูปธรรมจาก Infrastructure โดยตรง — Domain (เสถียร) พึ่งพา Infrastructure (ไม่เสถียร) private readonly SmtpEmailSender _emailSender;
public OrderService(SmtpEmailSender emailSender) { _emailSender = emailSender; }
public void CompleteOrder(Order order) { // ... ตรรกะทางธุรกิจของการปิดคำสั่งซื้อ _emailSender.Send(order.CustomerEmail, "คำสั่งซื้อสำเร็จแล้ว"); } }}
namespace Infrastructure{ // เปลี่ยนบ่อยตามผู้ให้บริการอีเมล การตั้งค่า SMTP หรือ SDK version ใหม่ public class SmtpEmailSender { public void Send(string to, string message) { /* ... */ } }}หลัง (ดี) — ย้าย abstraction ไปให้ Domain เป็นเจ้าของ แล้วให้ Infrastructure เป็นฝ่าย implement มัน ทิศทางการพึ่งพาจึงไหลจากไม่เสถียรไปสู่เสถียรอย่างถูกต้อง:
namespace Domain{ // Domain เป็นเจ้าของ interface นี้ — เสถียร เพราะมีผู้พึ่งพา (implementers) มาหามัน ไม่ใช่ตรงกันข้าม public interface INotificationSender { void Send(string to, string message); }
public class OrderService { private readonly INotificationSender _notificationSender;
public OrderService(INotificationSender notificationSender) { _notificationSender = notificationSender; }
public void CompleteOrder(Order order) { // ... ตรรกะทางธุรกิจของการปิดคำสั่งซื้อ _notificationSender.Send(order.CustomerEmail, "คำสั่งซื้อสำเร็จแล้ว"); } }}
namespace Infrastructure{ // Infrastructure (ไม่เสถียร) เป็นฝ่ายพึ่งพา Domain (เสถียร) ผ่าน interface — ทิศทางถูกต้องตาม SDP public class SmtpEmailSender : INotificationSender { public void Send(string to, string message) { /* ... */ } }}สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- project Domain หรือ Core มี project reference ไปยัง Infrastructure, UI หรือ library SDK ของผู้ให้บริการภายนอกโดยตรง
- interface ที่ Infrastructure ควรจะ implement กลับถูกนิยามไว้ใน Infrastructure เอง แทนที่จะเป็น Domain ที่เป็นผู้ใช้งาน (เจ้าของ abstraction ผิดชั้น)
- มีวงจรการพึ่งพา (circular dependency) ระหว่าง package — เมื่อ A พึ่งพา B และ B พึ่งพา A กลับ ไม่มีฝ่ายใดสามารถ “เสถียรกว่า” อีกฝ่ายได้เลย
- การอัปเกรดหรือแก้ไข library/package เดียว ทำให้ต้องแก้ file จำนวนมากที่ไม่เกี่ยวข้องกันโดยตรงในหลายชั้นของระบบ
- unit test ของ Domain ต้อง reference หรือ mock รายละเอียดของ Infrastructure (เช่น connection string, HTTP client เฉพาะเจ้า) เพื่อให้ compile ผ่าน
- class utility หรือ configuration ตัวเดียวที่มีผู้พึ่งพาจำนวนมาก (Ca สูง) แต่ยังถูกแก้ไขบ่อยอยู่เรื่อย ๆ — ทุกครั้งที่แก้คือความเสี่ยงต่อทั้งระบบ
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Dependency Inversion Principle
- SOLID
- Inversion of Control
- Explicit Dependencies Principle
- Persistence Ignorance