Chain of Responsibility
ส่งต่อคำขอไปตามสายของ handler จนกว่าจะมีตัวจัดการ
เมื่อระบบต้องประมวลผลคำขอ (request) ด้วยขั้นตอนตรวจสอบหรือจัดการหลายแบบ — เช่น validate สิทธิ์, ตรวจ cache, log, แปลงข้อมูล — วิธีตรงไปตรงมาที่สุดคือเขียน if/else หรือ switch ยาวๆ ในที่เดียว ปัญหาคือ code แบบนี้จะบวมขึ้นเรื่อยๆ เมื่อเพิ่มเงื่อนไขใหม่ แก้ยาก ทดสอบยาก และละเมิด Open/Closed Principle เพราะทุกครั้งที่มีกรณีใหม่ต้องเข้าไปแก้ code เดิม
Chain of Responsibility แก้ปัญหานี้ด้วยการตัดแต่ละขั้นตอนออกเป็น handler อิสระ แล้วร้อยต่อกันเป็นสาย (chain) ผู้เรียก (client) ส่งคำขอเข้าที่ปลายสายโดยไม่รู้ว่าใครในสายจะเป็นคนจัดการ handler แต่ละตัวตัดสินใจเองว่า:
- จัดการคำขอนี้แล้วหยุดสาย หรือ
- จัดการบางส่วนแล้วส่งต่อ หรือ
- ไม่จัดการเลยแล้วส่งต่อให้ 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 ซ่อนอยู่ข้างในอีกชั้นหนึ่ง
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”- Client สร้าง handler แต่ละตัว แล้วเรียก
SetNextร้อยเรียงเป็นสายตามลำดับที่ต้องการ (ลำดับนี้กำหนดได้เองและเปลี่ยนได้ตอน runtime) - Client ส่งคำขอให้ handler ตัวแรก เท่านั้น
- handler แต่ละตัวตรวจว่าตัวเองรับผิดชอบคำขอนี้ไหม ถ้าใช่ก็ประมวลผล (อาจหยุดสายหรือส่งต่อก็ได้แล้วแต่ออกแบบ) ถ้าไม่ใช่ก็เรียก
Next?.Handle(request)ส่งต่อทันที - สายจบเมื่อ 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 ทำงานแล้วยังส่งต่อ) ซึ่งเป็นความแตกต่างที่ควรรู้เวลาออกแบบ
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้จำลอง 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 }); // ส่งต่อไป ManagerteamLead.Handle(new LeaveRequest { Employee = "Beam", Days = 10 }); // ส่งต่อไปจนถึง Directorหมายเหตุ: ถ้าอยากได้ version ที่ยืดหยุ่นกว่านี้ อาจแทน abstract class ด้วย delegate หรือใช้ Func<LeaveRequest, bool> แต่รูปแบบ 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 ที่ตั้งใจออกแบบ