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

Middle Man

class ที่​เป็น​เพียง​ชั้น​ส่ง​ผ่าน ไม่​ได้​เพิ่ม​คุณค่า​อะไร

Middle Man คือ class ที่ method ส่วน​ใหญ่​ทำ​หน้าที่​แค่ forward การ​เรียก​ไป​ยัง​อ็อบเจ็กต์อื่น โดย​ไม่​เพิ่ม logic หรือ​คุณค่า​อะไร​ของ​ตัวเอง​เลย มัน​คือ “คนกลาง” ที่​รับ​ข้อความ​จาก​ผู้​เรียก​แล้ว​ส่ง​ต่อ​ให้​อีก class หนึ่ง​จัดการ​ทั้งหมด ถ้า​ลบ class นี้​ออก​แล้ว​ให้​ผู้​เรียก​คุย​กับ​ปลายทาง​ตรง ๆ พฤติกรรม​ของ​ระบบ​ก็​ไม่​เปลี่ยน — นั่น​คือ​สัญญาณ​ว่า class นี้​ไม่มี​เหตุผล​ที่​จะ​ดำรง​อยู่

การ delegate บาง​ส่วน​ไม่ใช่​เรื่อง​ผิด ตรง​กัน​ข้าม การ​ซ่อน​รายละเอียด​ของ​อ็อบเจ็กต์ภายใน​ไว้​หลัง method delegate (refactoring ที่​ชื่อ Hide Delegate) เป็น​วิธี​สนับสนุน encapsulation และ​ลด coupling ที่​ดี​มาก แต่​กลิ่น Middle Man เกิด​ขึ้น​เมื่อ “การ​ซ่อน” นั้น​ถูก​ทำ​จน​สุด​โต่ง — ทั้ง class เหลือ​แค่ methods ที่ forward อย่าง​เดียว ไม่มี state หรือ decision logic ของ​ตัวเอง​หลง​เหลือ​อยู่​เลย

สาเหตุ​ที่​พบ​บ่อย​มี​สอง​แบบ:

  1. แก้ Message Chains มาก​เกิน​ไป — เมื่อ​ทีม​พยายาม​กำจัด Message Chains (เช่น a.getB().getC().getD()) ด้วย​การ​เพิ่ม method delegate ที่​ทุก​ชั้น ผลลัพธ์​ที่​ได้​บางที​คือ class ตรง​กลาง​ที่​ทำ​หน้าที่​แค่​ส่ง​ต่อ กลาย​เป็นกลิ่น​ใหม่​แทนที่​กลิ่น​เดิม
  2. function การ​ทำงาน​ค่อย ๆ ถูก​ย้าย​ออก​ไป — เมื่อ​ทีม refactor และ​ย้าย behavior ของ class หนึ่ง​ไป​ไว้​ที่ class อื่น​ที​ละ​นิด​ตาม​หลัก Feature Envy หรือ Single Responsibility Principle class เดิม​อาจ​เหลือ​แค่​เปลือก​ที่ forward ทุก​อย่าง​ไป​ยัง​ที่​ที่ logic ย้าย​ไป​อยู่ กลาย​เป็น​ทั้ง Middle Man และ​ใกล้​เคียง Lazy Class

Middle Man จึง​เป็น​ภาพ​สะท้อน​ตรง​ข้าม​กับ Message Chains: Message Chains คือ coupling ที่ “เปิดเผย​เกิน​ไป” (ผู้​เรียก​รู้จัก​โครงสร้าง​ภายใน​ลึก​เกิน​ไป) ส่วน Middle Man คือ encapsulation ที่ “ปิด​จน​ไม่มี​อะไร​เหลือ” (class กลาง​ไม่มี​คุณค่า​ให้​ปกป้อง​อีก​ต่อ​ไป)

  • เปิด​ดู public interface ของ class แล้ว​นับ​ว่า​กี่ method ที่​ทำ​แค่ return _other.SomeMethod(args); โดย​ไม่มี​เงื่อนไข การ​แปลง​ข้อมูล หรือ validation ใด ๆ เพิ่มเติม — ถ้า​เกิน​ครึ่ง​ของ interface เป็น​แบบ​นี้ ให้​สงสัย​ไว้​ก่อน
  • class แทบ​ไม่มี field ของ​ตัวเอง​นอกจาก reference ไป​ยัง​อ็อบเจ็กต์ที่​มัน​คอย forward ให้
  • เวลา​ต้อง​เพิ่ม​ความ​สามารถ​ใหม่​ให้​ระบบ นัก​พัฒนา​ต้อง​แก้​ทั้ง class กลาง​และ class ปลายทาง​พร้อม​กัน​เสมอ (แก้​ที่​เดียว​ไม่​พอ)
  • Unit test ของ class นี้​ทำได้​แค่ mock dependency แล้ว assert ว่า method delegate ถูก​เรียก — แทบ​ไม่มี behavior จริง​ให้​ทดสอบ
  • ชื่อ class มัก​จะ​ดู​กลาง ๆ ทั่วไป (...Manager, ...Handler, ...Wrapper) แต่​เปิด code เข้าไป​แล้ว​ว่างเปล่า
  • ระวัง​ไม่​ให้​สับสน​กับ Facade, Proxy, Decorator หรือ Adapter ที่​ตั้งใจ​สร้าง​ชั้น​ส่ง​ผ่าน​ขึ้น​มา​โดย​มี​เหตุผล​ทาง​สถาปัตยกรรม​ชัดเจน (ดู​หัวข้อ​ถัด​ไป)
  • เพิ่ม​ชั้น​ความ​เข้าใจ​โดย​ไม่​เพิ่ม​มูลค่า — ผู้​อ่าน code ต้อง​กระโดด​ข้าม class กลาง​ไป​ดู class จริง​เสมอ ทำให้​ต้อง​เปิด file เพิ่ม​ขึ้น​โดย​ไม่​ได้​อะไร​กลับ​มา​เลย ขัด​กับ​หลัก Keep It Simple
  • บำรุง​รักษา​ซ้ำซ้อน — ทุก​ครั้ง​ที่ signature ของ class ปลายทาง​เปลี่ยน ต้อง​ตาม​ไป​แก้ method forward ใน class กลาง​ด้วย เพิ่ม​จุด​ที่​ต้อง​แก้​โดย​ไม่มี​ประโยชน์​ชดเชย
  • ทำให้​เข้าใจ ownership ผิด — class กลาง​แอบ​อ้าง​ว่า​ตัว​มัน​รับผิดชอบ​พฤติกรรม​นั้น (เพราะ​มี public method ให้​เรียก) ทั้ง​ที่​จริง ๆ ความ​รับผิดชอบ​อยู่​ที่​อื่น ทำให้​แผนที่​ความคิด​ของ​ทีม​เกี่ยว​กับ​ระบบ​คลาดเคลื่อน
  • ปิดบัง Feature Envy ที่แท้​จริง — บ่อย​ครั้ง Middle Man คือ​สัญญาณ​ว่า​ความ​รับผิดชอบ​ถูกวาง​ผิด​ที่​ตั้งแต่​แรก การ​ไล่​ตาม​แก้​ปัญหา​ที่​ปลาย​เหตุ (แก้​แค่ class กลาง) จะ​ไม่​ทำให้ debt หาย​ไป

ข้อ​ยกเว้น​ที่​สำคัญ: อย่า​รีบ​ลบ Middle Man ทุก​กรณี บาง​ครั้ง​ชั้น​ส่ง​ผ่าน​ถูก​สร้าง​ขึ้น​โดย​ตั้งใจ​เพื่อ:

  • ลด coupling ระหว่าง class สอง​ฝั่ง​ตาม​หลัก Encapsulate — ผู้​เรียก​ไม่​ต้อง​รู้จัก​โครงสร้าง​ภายใน​เลย
  • implement design pattern ที่​ตัว​มัน​เอง​คือ “ชั้น​ส่ง​ผ่าน​ที่​มี​เหตุผล” เช่น Proxy (ควบคุม​การ​เข้าถึง), Decorator (เพิ่ม​พฤติกรรม​ระหว่าง​ทาง) หรือ Facade (ลด​ความ​ซับซ้อน​ของ subsystem ให้​ผู้​เรียก)
  • ปฏิบัติ​ตาม Law of Demeter เพื่อ​ไม่​ให้​ผู้​เรียก​ต้อง “เดิน” ผ่าน​หลาย​อ็อบเจ็กต์ — ตราบ​ใด​ที่​ชั้น​ส่ง​ผ่าน​นั้น​ยัง​มี​เหตุผล​รองรับ ก็​ไม่ใช่​กลิ่น

คำถาม​ที่​ต้อง​ถาม​ตัวเอง​เสมอ​คือ “ถ้า​ลบ class นี้​ทิ้ง​แล้ว​ให้​ผู้​เรียก​คุย​กับ​ปลายทาง​ตรง ๆ จะ​เสีย​อะไร​ไป​บ้าง” ถ้า​คำ​ตอบ​คือ “ไม่​เสีย​อะไร​เลย” นั่น​คือ Middle Man ตัว​จริง

สมมติ Person ถือ Department ไว้ และ​มี method ชุด​หนึ่ง​ที่​แค่ forward ทุก​การ​เรียก​ไป​ยัง Department โดย​ไม่​เพิ่ม​อะไร​เลย:

public class Department
{
public string Name { get; set; }
public Employee Manager { get; set; }
}
public class Person
{
private readonly Department _department;
public Person(Department department)
{
_department = department;
}
// Middle Man: ส่งต่อทุกอย่างไปที่ Department โดยไม่เพิ่มคุณค่าของตัวเอง
public Employee GetManager() => _department.Manager;
public string GetDepartmentName() => _department.Name;
public void SetDepartmentName(string name) => _department.Name = name;
public void ChangeManager(Employee manager) => _department.Manager = manager;
}
// การใช้งาน
var manager = person.GetManager();
var deptName = person.GetDepartmentName();
person.ChangeManager(newManager);

Person ใน​ที่​นี้​ไม่มี logic ของ​ตัวเอง​เลย ทุก method คือ one-liner ที่​แค่​เรียก _department ต่อ — ถ้า​ลบ​ทิ้ง​แล้ว​ให้​ผู้​เรียก​คุย​กับ Department โดยตรง พฤติกรรม​ของ​ระบบ​จะ​ไม่​เปลี่ยนแปลง นี่​คือ​สัญญาณ​คลาสสิก​ของ Middle Man

refactor ด้วย Remove Middle Man (จาก catalog ของ Martin Fowler) โดย​เปิด​ทาง access ไป​ยัง Department ตรง ๆ แทน​การ wrap ทุก method:

public class Person
{
public Department Department { get; }
public Person(Department department)
{
Department = department;
}
}
// การใช้งาน - เรียกตรงไปที่ Department แทนการผ่าน Person
var manager = person.Department.Manager;
var deptName = person.Department.Name;
person.Department.Name = "Engineering";
person.Department.Manager = newManager;

จำนวน method หาย​ไป​ทั้งหมด เหลือ​แค่​จุด access เดียว (Department) ที่​ผู้​เรียก​ใช้​คุย​กับ​ปลายทาง​ได้​ตรง ๆ code สั้น​ลง จุด​ที่​ต้อง​อัปเดต​เมื่อ Department เปลี่ยน API ก็​ลด​ลง​เหลือ​ที่​เดียว

ข้อ​ควร​ระวัง: การ Remove Middle Man แบบ​นี้​แลก encapsulation กับ​ความ​เรียบ​ง่าย — ถ้า Person มี​เหตุผล​ทาง​ธุรกิจ​ที่​ต้อง​ปกป้อง​ไม่​ให้​ผู้​เรียก​แตะ Department ตรง ๆ (เช่น ต้อง validate ก่อน​เปลี่ยน manager) ให้​เก็บ method ที่​มี logic นั้น​ไว้ และ​ใช้ Inline Function ลบ​เฉพาะ method ที่​เป็น pure delegate ออก​ไป ไม่​จำเป็น​ต้อง​ลบ​ทั้ง​ชั้น​เสมอ​ไป