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 ของตัวเองหลงเหลืออยู่เลย
สาเหตุที่พบบ่อยมีสองแบบ:
- แก้ Message Chains มากเกินไป — เมื่อทีมพยายามกำจัด Message Chains (เช่น
a.getB().getC().getD()) ด้วยการเพิ่ม method delegate ที่ทุกชั้น ผลลัพธ์ที่ได้บางทีคือ class ตรงกลางที่ทำหน้าที่แค่ส่งต่อ กลายเป็นกลิ่นใหม่แทนที่กลิ่นเดิม - 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 ตัวจริง
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”สมมติ 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 แทนการผ่าน Personvar 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 ออกไป ไม่จำเป็นต้องลบทั้งชั้นเสมอไป