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

Chain of Responsibility

ส่ง​ต่อ​คำขอ​ไป​ตาม​สาย​ของ handler จนกว่า​จะ​มี​ตัว​จัดการ

เมื่อ​ระบบ​ต้อง​ประมวล​ผล​คำขอ (request) ด้วย​ขั้นตอน​ตรวจสอบ​หรือ​จัดการ​หลาย​แบบ — เช่น validate สิทธิ์, ตรวจ cache, log, แปลง​ข้อมูล — วิธี​ตรง​ไป​ตรง​มา​ที่สุด​คือ​เขียน if/else หรือ switch ยาวๆ ใน​ที่​เดียว ปัญหา​คือ code แบบ​นี้​จะ​บวม​ขึ้น​เรื่อยๆ เมื่อ​เพิ่ม​เงื่อนไข​ใหม่ แก้​ยาก ทดสอบ​ยาก และ​ละเมิด Open/Closed Principle เพราะ​ทุก​ครั้ง​ที่​มี​กรณี​ใหม่​ต้อง​เข้าไป​แก้ code เดิม

Chain of Responsibility แก้​ปัญหา​นี้​ด้วย​การ​ตัด​แต่ละ​ขั้นตอน​ออก​เป็น handler อิสระ แล้ว​ร้อย​ต่อ​กัน​เป็น​สาย (chain) ผู้​เรียก (client) ส่ง​คำขอ​เข้า​ที่​ปลาย​สาย​โดย​ไม่รู้​ว่า​ใคร​ใน​สาย​จะ​เป็น​คน​จัดการ handler แต่ละ​ตัว​ตัดสิน​ใจ​เอง​ว่า:

  1. จัดการ​คำขอ​นี้​แล้ว​หยุด​สาย หรือ
  2. จัดการ​บาง​ส่วน​แล้ว​ส่ง​ต่อ หรือ
  3. ไม่​จัดการ​เลย​แล้ว​ส่ง​ต่อ​ให้ handler ถัด​ไป

แนวคิด​หลัก​คือ แยก sender ออก​จาก receiver อย่าง​สมบูรณ์ — ผู้​ส่ง​คำขอ​ผูก​อยู่​กับ handler ตัว​แรก​ใน​สาย​เท่านั้น (หรือ​กับ interface ของ handler) ไม่รู้จัก​และ​ไม่​ผูก​กับ handler ตัว​จริง​ที่​จะ​ประมวล​ผล ทำให้​เพิ่ม/ลบ/สลับ​ลำดับ handler ได้​โดย​ไม่​กระทบ code ฝั่ง client เลย ตัวอย่าง​ที่​วิศวกร​เจอ​ทุก​วัน​คือ middleware pipeline ใน​เว็บ framework (เช่น ASP.NET Core) ที่ request วิ่ง​ผ่าน middleware ที​ละ​ตัว แต่ละ​ตัว​เลือก​จะ​จัดการ​เอง (short-circuit) หรือ​เรียก next() ส่ง​ต่อ

องค์​ประกอบ​หลัก​มี​สาม​ส่วน: Handler (interface หรือ abstract class ที่​นิยาม method ตั้ง​ค่า handler ถัด​ไป​และ method ประมวล​ผล), ConcreteHandler หลาย​ตัว (แต่ละ​ตัว​มี​เงื่อนไข​และ​ตรรกะ​ของ​ตัวเอง) และ Client ที่​ประกอบ​สาย​แล้ว​ยิง​คำขอ​เข้าไป

classDiagram
    class Handler {
      +SetNext(handler) Handler
      +Handle(request) string
    }
    class BaseHandler {
      -Handler next
      +SetNext(handler) Handler
      +Handle(request) string
    }
    class ConcreteHandlerA {
      +Handle(request) string
    }
    class ConcreteHandlerB {
      +Handle(request) string
    }
    class ConcreteHandlerC {
      +Handle(request) string
    }
    class Client {
      +Run()
    }
    Handler <|.. BaseHandler
    BaseHandler <|-- ConcreteHandlerA
    BaseHandler <|-- ConcreteHandlerB
    BaseHandler <|-- ConcreteHandlerC
    BaseHandler o-- Handler
    Client --> Handler

จุด​สำคัญ​คือ BaseHandler เก็บ reference ไป​ยัง handler ตัว​ถัด​ไป (next) และ​มี​พฤติกรรม​ค่า​เริ่มต้น​คือ​ส่ง​ต่อ ทำให้ ConcreteHandler แต่ละ​ตัว​ต้อง override เฉพาะ​ตรรกะ​ของ​ตัวเอง ไม่​ต้อง​เขียน code ส่ง​ต่อ​ซ้ำ​ทุก​ที่ — นี่​คือ​รูปแบบ Template Method ซ่อน​อยู่​ข้าง​ใน​อีก​ชั้น​หนึ่ง

  1. Client สร้าง handler แต่ละ​ตัว แล้ว​เรียก SetNext ร้อย​เรียง​เป็น​สาย​ตาม​ลำดับ​ที่​ต้องการ (ลำดับ​นี้​กำหนด​ได้​เอง​และ​เปลี่ยน​ได้​ตอน runtime)
  2. Client ส่ง​คำขอ​ให้ handler ตัว​แรก เท่านั้น
  3. handler แต่ละ​ตัว​ตรวจ​ว่า​ตัวเอง​รับผิดชอบ​คำขอ​นี้​ไหม ถ้า​ใช่​ก็​ประมวล​ผล (อาจ​หยุด​สาย​หรือ​ส่ง​ต่อ​ก็ได้​แล้ว​แต่​ออกแบบ) ถ้า​ไม่ใช่​ก็​เรียก Next?.Handle(request) ส่ง​ต่อ​ทันที
  4. สาย​จบ​เมื่อ handler ตัว​ใด​ตัว​หนึ่ง​หยุด​ไว้ หรือ​คำขอ​วิ่ง​จน​สุด​สาย​แล้ว​ไม่มี​ใคร​จัดการ (ต้อง​ออกแบบ​รองรับ​กรณี​นี้ เช่น คืน​ค่า null/false หรือ​โยน exception)
sequenceDiagram
    participant Client
    participant HandlerA
    participant HandlerB
    participant HandlerC
    Client->>HandlerA: Handle(request)
    HandlerA->>HandlerA: ตรวจเงื่อนไข ไม่ตรง
    HandlerA->>HandlerB: Handle(request)
    HandlerB->>HandlerB: ตรวจเงื่อนไข ไม่ตรง
    HandlerB->>HandlerC: Handle(request)
    HandlerC->>HandlerC: ตรวจเงื่อนไข ตรง จัดการเอง
    HandlerC-->>Client: ผลลัพธ์

ข้อสังเกต: GoF ดั้งเดิม​ตั้งใจ​ให้​มี handler เดียว​จัดการ​คำขอ​หนึ่ง​อัน แล้ว​สาย​ก็​หยุด แต่​ใน​ทาง​ปฏิบัติ (เช่น middleware, event handler ใน UI, servlet filter) หลาย implementation อนุญาต​ให้​หลาย handler ใน​สาย​ทำงาน​กับ​คำขอ​เดียวกัน​ได้ (เช่น logging middleware ทำงาน​แล้ว​ยัง​ส่ง​ต่อ) ซึ่ง​เป็น​ความ​แตก​ต่าง​ที่​ควร​รู้​เวลา​ออกแบบ

ตัวอย่าง​นี้​จำลอง pipeline อนุมัติ​คำขอ​ลา​ของ​พนักงาน: หัวหน้า​ทีม​อนุมัติ​ได้​ถึง 2 วัน, ผู้จัดการ​อนุมัติ​ได้​ถึง 5 วัน, ผู้​อำนวย​การ​อนุมัติ​ได้​ทุก​กรณี

// คำขอที่วิ่งผ่านสาย
public class LeaveRequest
{
public string Employee { get; init; } = "";
public int Days { get; init; }
}
// Handler แบบ abstract เก็บ reference ตัวถัดไปและ default behavior คือส่งต่อ
public abstract class Approver
{
protected Approver? Next;
public Approver SetNext(Approver next)
{
Next = next;
return next; // เพื่อให้ chain ต่อกันได้แบบ fluent
}
public abstract void Handle(LeaveRequest request);
// helper ให้ ConcreteHandler เรียกเมื่อจัดการเองไม่ได้
protected void PassToNext(LeaveRequest request)
{
if (Next is null)
{
Console.WriteLine($"ไม่มีใครอนุมัติคำขอของ {request.Employee} ({request.Days} วัน) ได้");
return;
}
Next.Handle(request);
}
}
public class TeamLead : Approver
{
public override void Handle(LeaveRequest request)
{
if (request.Days <= 2)
Console.WriteLine($"TeamLead อนุมัติ {request.Employee} ({request.Days} วัน)");
else
PassToNext(request); // เกินสิทธิ์ ส่งต่อ
}
}
public class Manager : Approver
{
public override void Handle(LeaveRequest request)
{
if (request.Days <= 5)
Console.WriteLine($"Manager อนุมัติ {request.Employee} ({request.Days} วัน)");
else
PassToNext(request);
}
}
public class Director : Approver
{
public override void Handle(LeaveRequest request)
{
// Director อนุมัติได้ทุกกรณี ถือเป็นปลายสาย
Console.WriteLine($"Director อนุมัติ {request.Employee} ({request.Days} วัน)");
}
}
// ---- ประกอบสายฝั่ง client ----
var teamLead = new TeamLead();
var manager = new Manager();
var director = new Director();
teamLead.SetNext(manager).SetNext(director);
// client รู้จักแค่ handler ตัวแรก ไม่รู้ว่าใครจะเป็นคนอนุมัติจริง
teamLead.Handle(new LeaveRequest { Employee = "Nook", Days = 1 }); // TeamLead อนุมัติ
teamLead.Handle(new LeaveRequest { Employee = "Aom", Days = 4 }); // ส่งต่อไป Manager
teamLead.Handle(new LeaveRequest { Employee = "Beam", Days = 10 }); // ส่งต่อไปจนถึง Director

หมายเหตุ: ถ้า​อยาก​ได้ version ที่​ยืดหยุ่น​กว่า​นี้ อาจ​แทน abstract class ด้วย delegate หรือ​ใช้ Func&lt;LeaveRequest, bool&gt; แต่​รูปแบบ object-oriented ด้าน​บน​คือ​แบบ GoF ดั้งเดิม​และ​เข้าใจ​ง่าย​ที่สุด

  • เมื่อ​มี handler ที่​เป็น​ไป​ได้​หลาย​ตัว แต่​ไม่รู้​ล่วงหน้า​ว่า​ตัว​ไหน​จะ​จัดการ​คำขอ และ​ลำดับ​อาจ​เปลี่ยน​ได้​ตอน runtime
  • เมื่อ​ต้องการ decouple ผู้​ส่ง​คำขอ​ออก​จาก​ผู้รับ เพื่อ​ให้​แก้ไข/เพิ่ม handler ได้​โดย​ไม่​กระทบ code ฝั่ง​เรียก (สอดคล้อง​กับ Open/Closed Principle)
  • เมื่อ​สร้าง pipeline ประมวล​ผล​แบบ​เป็น​ขั้นตอน เช่น middleware, validation pipeline, event bubbling ใน UI
  • เมื่อ​ต้องการ​ให้​แต่ละ handler มี​ความ​รับผิดชอบ​เดียว (Single Responsibility) แยก​จาก​กัน​ชัดเจน ทดสอบ​แยก​หน่วย​ได้​ง่าย
  • เมื่อ​รู้​แน่ชัด​อยู่​แล้ว​ว่า​ใคร​จะ​จัดการ​คำขอ และ​ลำดับ​คงที่​ตายตัว — if/else ธรรมดา​ที่​อ่าน​ง่าย​อาจ​ดี​กว่า​การ​เพิ่ม abstraction
  • เมื่อ​ทุก handler ต้อง​เห็น​คำขอ​ครบ​ทุก​ตัว​เสมอ​โดย​ไม่มี​การ short-circuit และ​ไม่​สนใจ​ว่า​ใคร​ประมวล​ผล​อะไร​ก่อน​หลัง — กรณี​นี้ observer หรือ pipeline แบบ fan-out อาจ​เหมาะ​กว่า
  • เมื่อ​สาย​ยาว​มาก​จน​ตาม debug ไม่​ออกว่า​ใคร​กัน​แน่​ที่​ประมวล​ผล (หรือ​ไม่​ประมวล​ผล) คำขอ ควร​มี logging หรือ​เครื่องมือ trace กำกับ
  • เมื่อ performance เป็น​เรื่อง​วิกฤต เพราะ​คำขอ​ต้อง​ไล่​ผ่าน handler ที​ละ​ตัว​จนกว่า​จะ​เจอ​ตัว​ที่​จัดการ ทำให้​มี overhead เพิ่ม​ตาม​ความ​ยาว​สาย
ด้านรายละเอียด
ข้อดีควบคุม​ลำดับ​การ​จัดการ​คำขอ​ได้​เอง (Single Responsibility ต่อ handler)
ข้อดีเพิ่ม/ลบ/สลับ handler ได้​โดย​ไม่​แก้ code เดิม (Open/Closed Principle)
ข้อดีClient ไม่​ผูก​กับ handler ตัว​จริง ทดสอบ​และ​แทนที่​ง่าย
ข้อ​เสียคำขอ​อาจ​ไม่​ถูก​จัดการ​เลย​ถ้า​ไม่มี handler ใด​รับผิดชอบ — ต้อง​ออกแบบ fallback
ข้อ​เสียตาม​รอย (trace) ยาก​ขึ้น​เมื่อ​สาย​ยาว เพราะ logic กระจาย​อยู่​หลาย class
ข้อ​เสียมี overhead จาก​การ​ไล่​ตรวจ​ที​ละ handler โดย​เฉพาะ​สาย​ยาวๆ
  • Single Responsibility — หลักการ​ที่​ทำให้​แต่ละ handler ทำ​หน้าที่​เดียว
  • Command — มัก​ใช้​คู่​กัน โดย request ที่​ส่ง​ผ่าน​สาย​อาจ​ห่อ​เป็น Command object
  • Decorator — โครงสร้าง​คล้าย​กัน​มาก (wrap object ต่อ​กัน​เป็น​สาย) แต่ decorator ทุก​ชั้น​ทำงาน​เสมอ ขณะ​ที่ chain of responsibility อาจ​หยุด​กลาง​สาย
  • Composite — ใช้ recursive structure คล้าย​กัน แต่​จุด​ประสงค์​คือ​รวม object เป็น​โครงสร้าง​ต้นไม้ ไม่ใช่​ส่ง​ต่อ​คำขอ
  • Mediator — อีก pattern ที่​ลด coupling ระหว่าง object แต่​รวม​ศูนย์​ตรรกะ​ไว้​ที่ mediator แทนที่​จะ​กระจาย​เป็น​สาย
  • Message Chains (code smell) — กลิ่น code ที่​ควร​ระวัง​ไม่​ให้​สับสน​กับ Chain of Responsibility ที่​ตั้งใจ​ออกแบบ