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 กลุ่มนี้:
aเอง- parameter ของ method นั้น
- object ที่ถูกสร้างขึ้นภายใน method นั้น
- attribute/field ของ
a - 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 มี Balanceif (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 ปัจจุบันให้มากขึ้นไปอีกขั้น