Message Chains
เรียกต่อกันยาว a.getB().getC().doSomething()
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Message Chains คือกลิ่นที่ code ส่งข้อความไปยัง object แล้วส่งต่อไปยังผลลัพธ์ทันที เป็นสาย เช่น a.GetB().GetC().DoSomething() ทุกขั้นดึง object จากภายในตัวก่อนหน้า บังคับให้ผู้เรียกต้องรู้โครงสร้างภายในทั้งหมดของทุก class ที่อยู่บนสาย ไม่ใช่แค่ class ที่ตัวเองคุยด้วยโดยตรง
รูปแบบนี้บางครั้งถูกเรียกว่า “train wreck” — ชื่อเล่นที่สื่อว่าผู้อ่านต้องนั่งไล่ตามการเปลี่ยนผ่านจาก object หนึ่งไปยังอีก object หนึ่งทีละสถานี กว่าจะเข้าใจว่า code บรรทัดนั้นทำอะไรกันแน่ ตัวอย่างคลาสสิกจาก Fowler คือ aPerson.department.manager — ผู้เรียกต้องรู้ว่า Person มี Department และ Department มี Manager ทั้งที่สิ่งที่ผู้เรียกต้องการจริง ๆ มีแค่ manager คนหนึ่งเท่านั้น
ข้อควรระวัง: ไม่ใช่การเรียก method ต่อกันทุกแบบที่เป็น Message Chains ถ้าทุก method ในสายคืน this หรือ object เดิม (เช่น builder pattern อย่าง query.Where(...).OrderBy(...).Take(10)) นั่นคือ Fluent Interface ซึ่งไม่ผิดหลักการใด ๆ เพราะผู้เรียกยังคุยกับ “เพื่อนสนิท” คนเดียวอยู่ตลอด ความแตกต่างสำคัญคือ Fluent Interface เรียก method บน object เดียวกันหรือ component โดยตรงของมันเท่านั้น ในขณะที่ Message Chains กระโดดข้าม object ที่ไม่เกี่ยวข้องกันไปเรื่อย ๆ
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- code หน้าตาแบบ
order.GetCustomer().GetAddress().GetCity()หรือa.GetB().GetC().GetD().DoSomething()— เห็น.ต่อกันหลายทอดในนิพจน์เดียว - ทุกขั้นของสายคืน object คนละชนิดกัน (ไม่ใช่
thisของตัวเอง) — สัญญาณว่ากำลังไต่ผ่าน graph ความสัมพันธ์ของ domain - ผู้เรียกต้อง
import/usingtype ของ object กลาง ๆ ทั้งที่ไม่ได้สนใจ type เหล่านั้นโดยตรง เพียงต้องการ “ผ่านไปให้ถึง” object ปลายทาง - เวลาแก้โครงสร้างภายใน (เช่น ย้าย field จาก
Departmentไปไว้ที่Companyแทน) แล้วพบว่าต้องไล่แก้ทุกจุดที่เรียก.department.managerทั่ว project - code คล้ายกันนี้ปรากฏเป็นกลิ่นย่อยของ Law of Demeter Violations เสมอ — ถ้าเจอ Message Chains แปลว่าละเมิด Law of Demeter อยู่แล้ว
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”- เปราะบางต่อการเปลี่ยนแปลง — การเปลี่ยนโครงสร้างความสัมพันธ์ตรงกลางสาย (เช่น
Departmentไม่มีManagerตรง ๆ อีกต่อไป แต่ต้องผ่านTeamก่อน) จะกระเพื่อมไปกระทบผู้เรียกทุกจุดที่เขียนสายยาวแบบนี้ไว้ ยิ่งสายยาว ยิ่งกระทบวงกว้าง - ละเมิด Law of Demeter — ผู้เรียกรู้เรื่องโครงสร้างภายในของ object ที่ไม่ใช่ “เพื่อนสนิท” โดยตรง ทำลาย encapsulation เพราะ object ที่อยู่กลางสายเปิดเผยรายละเอียดภายในของตัวเองผ่าน getter แทนที่จะห่อหุ้มพฤติกรรมไว้กับข้อมูล
- coupling สูงข้ามหลาย class — code หนึ่งบรรทัดผูกติดกับ class หลาย class พร้อมกัน (ในตัวอย่างข้างต้นคือ
Order,Customer,Addressพร้อมกัน) ทำให้ทดสอบยาก เพราะต้อง mock หรือสร้าง object ทุกชั้นในสายให้ครบก่อนจะทดสอบเงื่อนไขปลายทางได้ - อ่านยาก ตั้งใจอ่านสองรอบ — ผู้อ่านต้องไล่ทีละสถานีว่าแต่ละจุดคืน object อะไร กว่าจะรู้ว่าบรรทัดนี้ทำอะไรจริง ๆ (นี่คือที่มาของชื่อเล่น “train wreck”)
- สับสนกับ Fluent Interface — ถ้าไม่ระวังการ refactor อาจไปทำลาย fluent API ที่ถูกออกแบบมาอย่างตั้งใจ (เช่น query builder) ทั้งที่มันไม่ใช่กลิ่นนี้
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ก่อน — Message Chains
หัวข้อที่มีชื่อว่า “ก่อน — Message Chains”public class OrderController{ public string GetShippingCityLabel(Order order) { // ผู้เรียกต้องรู้ว่า Order มี Customer, Customer มี Address, // Address มี City — ทั้งที่ต้องการแค่ชื่อเมืองปลายทาง return order.GetCustomer().GetAddress().GetCity().ToUpperInvariant(); }}
public class Order{ private readonly Customer _customer; public Customer GetCustomer() => _customer;}
public class Customer{ private readonly Address _address; public Address GetAddress() => _address;}
public class Address{ private readonly string _city; public string GetCity() => _city;}ถ้าวันหนึ่งต้องเปลี่ยนให้ Order รองรับ “ที่อยู่จัดส่ง” ที่ต่างจากที่อยู่ของลูกค้า (เช่นส่งของขวัญให้คนอื่น) ทุกจุดใน project ที่เขียน order.GetCustomer().GetAddress() จะต้องถูกไล่แก้ทั้งหมด
หลัง — ใช้ Hide Delegate
หัวข้อที่มีชื่อว่า “หลัง — ใช้ Hide Delegate”refactor ด้วย Hide Delegate: ให้แต่ละ class ที่อยู่ต้นสายห่อหุ้ม (encapsulate) การเรียกไปยังปลายสายไว้ใน method ของตัวเอง ผู้เรียกจึงคุยกับเพื่อนสนิทคนเดียวเท่านั้น
public class Order{ private readonly Customer _customer;
// ห่อหุ้มการเดินสายไว้ใน method เดียว ผู้เรียกไม่ต้องรู้จัก Customer หรือ Address เลย public string GetShippingCity() => _customer.GetCity();}
public class Customer{ private readonly Address _address;
public string GetCity() => _address.GetCity();}
public class Address{ private readonly string _city; public string GetCity() => _city;}
public class OrderController{ public string GetShippingCityLabel(Order order) { // เรียกแค่ Order ตัวเดียว ไม่ต้องรู้จัก Customer หรือ Address อีกต่อไป return order.GetShippingCity().ToUpperInvariant(); }}ตอนนี้ถ้าโครงสร้างภายในของ Customer หรือ Address เปลี่ยน (เช่นเพิ่ม ShippingAddress แยกจาก HomeAddress) จะแก้แค่ภายใน Order.GetShippingCity() และ Customer.GetCity() เท่านั้น ผู้เรียกภายนอกไม่ได้รับผลกระทบเลย
ทางเลือกอื่น — Extract Method + Move Method
หัวข้อที่มีชื่อว่า “ทางเลือกอื่น — Extract Method + Move Method”ถ้าตรรกะปลายสายซับซ้อนกว่าการดึงค่าตรง ๆ (เช่นมีการคำนวณหรือจัดรูปแบบ) ให้แยกส่วนนั้นด้วย Extract Method ก่อน แล้วย้าย (Move Method) ไปไว้ที่จุดเริ่มต้นของสาย เพื่อให้ตรรกะอยู่ใกล้ข้อมูลที่มันใช้จริง ๆ แทนที่จะให้ผู้เรียกเป็นคนไล่ดึงข้อมูลมาประมวลผลเอง
ข้อควรระวัง — อย่า Hide Delegate จนกลายเป็น Middle Man
หัวข้อที่มีชื่อว่า “ข้อควรระวัง — อย่า Hide Delegate จนกลายเป็น Middle Man”การ Hide Delegate ทุกจุดตลอดสายจะทำให้ class กลางกลายเป็นเพียงตัวส่งต่อ (forwarder) เต็มไปด้วย method wrapper ที่ไม่มีตรรกะของตัวเอง นี่คือกลิ่น Middle Man ควรเลือก hide เฉพาะจุดที่คุ้มค่า ไม่ใช่ทุกขั้นของทุกสาย
flowchart LR Controller -->|เดิม เรียกตรง 3 ทอด| Order Order -.->|เดิม| Customer Customer -.->|เดิม| Address Controller ==>|หลัง Hide Delegate เรียกทอดเดียว| Order2[Order] Order2 -->|ห่อหุ้มไว้ภายใน| Customer2[Customer] Customer2 -->|ห่อหุ้มไว้ภายใน| Address2[Address]
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Law of Demeter Violations — Message Chains คืออาการที่มองเห็นได้ของการละเมิดกฎนี้
- Law of Demeter — หลักการต้นทางที่อธิบายว่าทำไมควร “คุยกับเพื่อนสนิท” เท่านั้น
- Encapsulation — หลักการห่อหุ้มที่ Hide Delegate นำมาใช้แก้กลิ่นนี้
- Middle Man — กลิ่นตรงข้ามที่เกิดจากการ Hide Delegate มากเกินไป
- Feature Envy — กลิ่นใกล้เคียงที่ก็เกี่ยวกับ code ไปสนใจข้อมูลภายในของ object อื่นมากเกินไป
- Inappropriate Intimacy — อีกกลิ่นที่เกิดจาก class รู้รายละเอียดภายในของกันและกันมากเกินไป