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

The Law of Demeter

คุย​กับ​เพื่อน​ใกล้​ตัว​เท่านั้น อย่า​คุย​กับ “เพื่อน​ของ​เพื่อน”

The Law of Demeter (LoD) เป็น​แนวทาง​ออกแบบ​ที่​มุ่ง ลด coupling ระหว่าง class ใน​โปรแกรม​เชิง​วัตถุ พูด​ง่าย ๆ คือ object ควร​มี​ปฏิสัมพันธ์​กับ “เพื่อน​สนิท” ที่​อยู่​ใกล้​ตัว​เท่านั้น ไม่ใช่​กับ “เพื่อน​ของ​เพื่อน”

อย่าง​เป็น​ทางการ method ของ object a ควร​เรียก​ได้​เฉพาะ method ของ: ตัว​มัน​เอง, parameter ที่ method รับ​เข้า​มา, object ที่​มัน​สร้าง​ขึ้น​เอง​ภายใน method, attribute/component ที่​เป็น​ส่วนประกอบ​โดยตรง​ของ​มัน และ global variable ที่​มัน​เข้าถึง​ได้ อย่า​เจาะ​ลึก​เข้าไป​ในไส้​ใน​ของ object อื่น (เช่น​การ​เรียก​ต่อ​กัน​ยาว ๆ แบบ a.getB().getC().doSomething()) เพราะ​ทำให้​ผูก​ติด​กับ​โครงสร้าง​ภายใน​ของ​มัน สรุป​แบบ​ง่าย​ที่สุด​ที่​คน​จำ​กัน​ติดปาก​คือ “ใช้​จุด (.) เดียว” — a.m() โอเค แต่ a.m().n() เริ่ม​ผิด​กฎ

การ​เคารพ LoD ช่วย​ให้ coupling ต่ำ อ่าน​ง่าย และ​แก้ไข​ง่าย​ขึ้น เพราะ​เมื่อ​โครงสร้าง​ภายใน​ของ object เพื่อนบ้าน​เปลี่ยน code ที่​เรียก​มัน​จาก​ภายนอก​ก็​ไม่​ต้อง​เปลี่ยน​ตาม

LoD ถูก​เสนอ​โดย Ian Holland ที่ Northeastern University ช่วง​ปลาย​ปี 1987 ใน​ฐานะ​ส่วน​หนึ่ง​ของ Demeter Project ซึ่ง​เป็น​งาน​วิจัย​ด้าน adaptive และ aspect-oriented programming ชื่อ “Demeter” มา​จาก​เทพี​แห่ง​เกษตรกรรม​ของ​กรีก สื่อ​ถึง​ปรัชญา​แบบ bottom-up คือ “ปลูก​ซอฟต์แวร์” (grow) มากกว่า “สร้าง” (build) มัน​ขึ้น​มา​ทั้งดุ้น ต่อ​มา Karl Lieberherr และ​ทีม​ได้​เผยแพร่​หลักการ​นี้​ใน​รายงาน​วิชาการ​ปี 1988 และ​ตั้ง​ชื่อ​ให้​ติดปากว่า “Law of Demeter” หรือ “only talk to your friends”

รูปแบบ​ทางการ​ของ​กฎ​ระบุ​ว่า method ของ object a เรียก method ได้​เฉพาะ​ของ 5 กลุ่ม​นี้:

  1. a เอง
  2. parameter ของ method นั้น
  3. object ที่​ถูก​สร้าง​ขึ้น​ภายใน method นั้น
  4. attribute/field ของ a
  5. global variable ที่ a เข้าถึง​ได้

หาก​เรียก method ของ object ที่​ได้​มา​จาก​ผลลัพธ์​ของ​การ​เรียก method อื่น​อีก​ที (เช่น a.getB().getC()) ถือว่า​ผิด​กฎ เพราะ a ต้อง​รู้จัก​ทั้ง​โครงสร้าง​ของ B และ C ซึ่ง​ไม่ใช่ “เพื่อน​โดยตรง” ของ​มัน

LoD มี​ความ​เกี่ยวโยง​แนบแน่น​กับ​หลักการ Tell, Don’t Ask: แทนที่​จะ “ถาม” object เพื่อ​ดึง​ข้อมูล​ออก​มา​คำนวณ​เอง ให้ “บอก” object ว่า​ต้อง​ทำ​อะไร แล้ว​ปล่อย​ให้​มัน​จัดการ​ภายใน​ของ​มัน​เอง — นี่​คือ​สาเหตุ​ที่​การ​ทำ​ตาม Tell, Don’t Ask มัก​นำ​ไป​สู่​การ​เคารพ LoD โดย​อัตโนมัติ

ข้อ​วิจารณ์​และ​ข้อ​จำกัด: Martin Fowler เคย​แซว​ติดตลก​ว่า​อยาก​เรียก​กฎ​นี้​ว่า “Occasionally Useful Suggestion of Demeter” มากกว่า เพราะ​มัน​เป็น​แนวทาง​ที่​ควร​ใช้​เป็น​ส่วน​ใหญ่ ไม่ใช่​กฎ​เหล็ก​ที่​ห้าม​ฝ่าฝืน​เด็ดขาด — ใน​ทาง​ปฏิบัติ​มี​ข้อ​ยกเว้น​ที่​ยอมรับ​ได้ เช่น fluent builder ที่ method chain คืน object ประเภท​เดียวกัน, การ​ทำ map/reduce บน collection, หรือ​การ​อ่าน​โครงสร้าง​ข้อมูล​ที่​ซ้อน​ลึก​จาก third-party API ซึ่ง​เป็น​เพียง data structure ไม่ใช่ object ที่​มี behavior นอกจาก​นี้​การ​ทำ​ตาม LoD อย่าง​เคร่งครัด​เกิน​ไป​ก็​มี​ต้นทุน: ต้อง​สร้าง wrapper method จำนวน​มาก​เพื่อ “ส่ง​ต่อ” การ​เรียก (delegation) ซึ่ง​ถ้า​ใช้​พร่ำเพรื่อ​จะ​ทำให้ class มี public interface กว้าง​เกิน​จำเป็น​และ​เพิ่ม​จำนวน method ต่อ class (Weighted Methods per Class) ซึ่ง​งาน​วิจัย​บาง​ชิ้น​ชี้​ว่า​สัมพันธ์​กับ​อัตรา bug ที่​สูง​ขึ้น​ได้​เช่น​กัน​หาก​ทำ​แบบ​ผิด​วิธี

ตัวอย่าง​คลาสสิก​ที่​ใช้​อธิบาย LoD คือ​การ​เปรียบเทียบ “คน​เดิน​สุนัข”: เมื่อ​จะ​สั่ง​ให้​สุนัข​เดิน คุณ​สั่ง​ที่​ตัว​สุนัข​โดยตรง (dog.walk()) ไม่ใช่​ไป​สั่ง​การ​ที่​ขา​แต่ละ​ข้าง​ของ​มัน (dog.getLeg(1).move(), dog.getLeg(2).move(), …) — คุณ​ควร​รู้จัก​แค่ “เพื่อน” ของ​คุณ​คือ​สุนัข ไม่ใช่​ส่วนประกอบ​ภายใน​ของ​มัน

อีก​ตัวอย่าง​ที่​พบ​บ่อย​ใน code จริง​คือ​รูปแบบ Presenter/View: แทนที่ Presenter จะ​เจาะ​เข้าไป​เรียก view.getTextField().setText("Hello") ซึ่ง​ผูก​ติด​กับ​ว่า View มี TextField เป็น​ส่วนประกอบ ควร​ให้ View เปิด method หน้า​บ้าน​ของ​ตัวเอง​เช่น view.setUserMessage("Hello") แล้ว​ให้ View จัดการ​ภายใน​ว่า​จะ set ค่า​ลง​ที่​ควบคุม​ไหน วิธี​นี้​ทำให้​ถ้า​วัน​หนึ่ง​เปลี่ยน​จาก TextField เป็น Label code ฝั่ง Presenter ก็​ไม่​ต้อง​แก้

งาน​วิจัย​เชิง​ประจักษ์​จาก​มหาวิทยาลัย Zurich สรุป​ว่าการ​ทำ​ตาม Law of Demeter เป็น​แนวทาง​ตรง​ไป​ตรง​มา​ที่​ช่วย​ลด​จำนวน bug ใน​ซอฟต์แวร์​ได้​จริง และ​มี​รายงาน​ว่าที่ NASA Jet Propulsion Laboratory ใน​โครงการ Mars Pathfinder การ​ละเมิด LoD ทำให้​ต้นทุน​การ integrate code เข้า​ด้วย​กัน​สูง​กว่า​ปกติ​ราว​หนึ่ง​อันดับ (order of magnitude) เนื่องจาก object ต่าง ๆ ผูก​ติด​กับ​โครงสร้าง​ภายใน​ของ​กัน​และ​กัน​มาก​เกิน​ไป

ตัวอย่าง code C# เปรียบเทียบ​ก่อน-หลัง:

// ละเมิด LoD: Order ต้องรู้ว่า Customer มี Wallet และ Wallet มี Balance
if (order.Customer.Wallet.Balance >= order.Total)
{
order.Customer.Wallet.Withdraw(order.Total);
}
// เคารพ LoD: Order คุยกับ Customer (เพื่อนโดยตรง) เท่านั้น
// ส่วน Customer จัดการรายละเอียดเรื่อง Wallet ภายในของตัวเอง
if (order.Customer.CanAfford(order.Total))
{
order.Customer.Charge(order.Total);
}

ใน version แรก Order ต้อง​รู้จัก​ทั้ง​โครงสร้าง​ของ Customer และ Wallet — ถ้า​วัน​หนึ่ง​เปลี่ยน​วิธี​เก็บ​เงิน (เช่น​เปลี่ยน​จาก Wallet เป็น PaymentMethod หลาย​ใบ) code ของ Order ต้อง​แก้​ตาม ใน version หลัง Order คุย​กับ Customer เพียง​จุด​เดียว ส่วน Customer ห่อ​หุ้ม (encapsulate) รายละเอียด​เรื่อง wallet ไว้​ภายใน

แผนภาพ​ต่อ​ไป​นี้​แสดง​ให้​เห็น​ว่า​เส้น​ทางการ​เรียก​แบบ​ไหน​ที่ Order ควร​ใช้ (เส้น​ทึบ ผ่าน​เพื่อน​โดยตรง​คือ Customer) เทียบ​กับ​เส้นทาง​ที่​ละเมิด LoD (เส้น​ประ เจาะ​ทะลุ​เข้าไป​ถึง Wallet โดยตรง):

graph LR
    Order -->|CanAfford Charge| Customer
    Customer --- Wallet
    Order -.->|"Balance Withdraw ผิดกฎ"| Wallet
  • นับ​จุด (dot) ใน​โซ่​การ​เรียก เป็น heuristic ง่าย ๆ ใน​การ​ตรวจ​จับ​การ​ละเมิด: ถ้า​พบ a.b().c().d() ให้​สงสัย​ไว้​ก่อน​ว่า​อาจ​เป็น “train wreck” หรือ message chain ที่​ควร refactor
  • ใช้ Tell, Don’t Ask เป็น​แนวทาง​หลัก​ใน​การ​ออกแบบ method — ส่ง​คำ​สั่ง​ให้ object ทำงาน​แทน​การ​ดึง​ข้อมูล​ออก​มา​คำนวณ​เอง
  • อย่า “ห่อ” LoD ไป​ทุก​จุด​แบบ​สุด​โต่ง โดย​เฉพาะ​กับ fluent API, builder pattern, LINQ/map-reduce หรือ third-party DTO ที่​เป็น​แค่​โครงสร้าง​ข้อมูล​ไม่มี behavior — ใน​กรณี​เหล่า​นี้ method chaining เป็น​เรื่อง​ปกติ​และ​มี​ประโยชน์
  • ระวัง​ต้นทุน​ของ wrapper method ที่​มาก​เกิน​จำเป็น — การ​สร้าง delegation method เพื่อ “บังหน้า” ทุก field ของ​ทุก object จะ​ทำให้ interface บวม​และ​อ่าน​ยาก​ขึ้น ให้​เลือก​ห่อ​เฉพาะ​จุด​ที่​มี​ความ​เสี่ยง​จะ​เปลี่ยน​โครงสร้าง​บ่อย​หรือ​มี​ผล​ต่อ coupling จริง ๆ
  • ใช้ LoD เป็น สัญญาณ​เตือน (smell detector) ไม่ใช่​กฎ​เหล็ก — เมื่อ​เจอ message chain ยาว ให้​ถาม​ว่า “มี​ใคร​ควร​รับผิดชอบ logic นี้​แทน” มัก​จะ​นำ​ไป​สู่​การ​ค้น​พบ responsibility ที่​หลง​ทาง​อยู่​ผิด​ที่ (feature envy) หรือ​ควร​ย้าย behavior เข้าไป​อยู่​ใกล้​ข้อมูล​มาก​ขึ้น
  • ประโยชน์​ต่อ​การ​เขียน test: code ที่​เคารพ LoD มัก​จะ mock ได้​ง่าย​กว่า เพราะ​แต่ละ object ต้อง​รู้จัก​แค่​เพื่อน​โดยตรง​ของ​มัน จำนวน collaborator ที่​ต้อง stub ใน test หนึ่ง​เคส​จึง​น้อย​ลง และ test เปราะบาง (fragile test) น้อย​ลง​ตาม​ไป​ด้วย
  • โปรด​จำ​ไว้​ว่า​กฎ​นี้​ถูก​เสนอ​ครั้ง​แรก​โดย Ian Holland และ​เผยแพร่​ใน​นาม​ทีม Demeter Project ของ Karl Lieberherr ก่อน​จะ​ถูก​ขยาย​ใน​ปี 2003/2004 เป็น “Law of Demeter for Concerns” (only talk to your friends who share your concerns) ซึ่ง​เน้น​การ​ใช้​เทคนิค aspect-oriented เพื่อ​ซ่อน​รายละเอียด​ที่​ไม่​เกี่ยวข้อง​กับ concern ปัจจุบัน​ให้​มาก​ขึ้น​ไป​อีก​ขั้น