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

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/using type ของ 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) ทั้ง​ที่​มัน​ไม่ใช่​กลิ่น​นี้
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() จะ​ต้อง​ถูก​ไล่​แก้​ทั้งหมด

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) ไป​ไว้​ที่​จุด​เริ่มต้น​ของ​สาย เพื่อ​ให้​ตรรกะ​อยู่​ใกล้​ข้อมูล​ที่​มัน​ใช้​จริง ๆ แทนที่​จะ​ให้​ผู้​เรียก​เป็น​คน​ไล่​ดึง​ข้อมูล​มา​ประมวล​ผล​เอง

การ 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 รู้​รายละเอียด​ภายใน​ของ​กัน​และ​กัน​มาก​เกิน​ไป