Command
ห่อคำขอให้เป็น object เดี่ยว ที่บรรจุข้อมูลของคำขอไว้ครบ
ลองนึกภาพ app แก้ไขข้อความที่มีปุ่ม toolbar, เมนู, และ keyboard shortcut ที่ทำงานเดียวกัน — เช่น “บันทึก file” หากเขียน logic การบันทึกซ้ำไว้ใน event handler ของทั้งสามจุด เมื่อ logic เปลี่ยน ต้องแก้สามที่ ยิ่งไปกว่านั้น ถ้าต้องการ undo, จัดคิวคำสั่งไว้ทำทีหลัง, หรือบันทึก log ว่าใครสั่งอะไรไปบ้าง code ที่ผูก UI เข้ากับ business logic ตรง ๆ แบบนี้จะยิ่งพันกันยุ่งเหยิง
Command เป็น pattern เชิงพฤติกรรมที่ เปลี่ยนคำขอ (request) ให้เป็น object เดี่ยว ที่บรรจุข้อมูลทั้งหมดของคำขอนั้นไว้ครบ — ทั้งว่าจะเรียก method ไหน บน object อะไร ด้วย parameter อะไร แนวคิดหลักคือแยก “ผู้สั่ง” (invoker) ออกจาก “ผู้ทำ” (receiver) โดยมี object คำสั่งเป็นตัวกลาง ผลลัพธ์คือ:
- Parameterize ปุ่ม/เมนู/shortcut ด้วยคำสั่งที่ต่างกันได้ โดยไม่ต้อง subclass ปุ่มแยกตามพฤติกรรม
- จัดคิว หรือหน่วงเวลา การทำงาน เพราะคำสั่งเป็น object ที่เก็บไว้ก่อนแล้วค่อยเรียก
Execute()ทีหลังได้ - Undo/Redo ได้ เพราะคำสั่งรู้วิธีย้อนกลับตัวเอง (
Undo()) และเก็บ history เป็น stack ได้ - Log และ replay คำสั่งที่เกิดขึ้น เพื่อ audit trail หรือ macro recording
invoker (เช่น CommandHistory) ไม่จำเป็นต้องรู้ว่าคำสั่งทำอะไรข้างใน มันแค่เรียก Execute()/Undo() ผ่าน interface กลาง ทำให้เพิ่มคำสั่งชนิดใหม่ได้โดยไม่ต้องแก้ invoker เลย — สอดคล้องกับหลัก Open-Closed โดยตรง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Client
class Invoker {
-ICommand command
+SetCommand(cmd)
+ExecuteCommand()
}
class ICommand {
<<interface>>
+Execute()
+Undo()
}
class ConcreteCommand {
-Receiver receiver
-State backup
+Execute()
+Undo()
}
class Receiver {
+Action()
+ReverseAction()
}
Client --> Invoker
Client --> ConcreteCommand
Invoker o-- ICommand
ICommand <|.. ConcreteCommand
ConcreteCommand --> Receiver
- Client — สร้าง
ConcreteCommandแล้วผูกเข้ากับReceiverที่ถูกต้อง จากนั้นส่งคำสั่งให้Invoker - Invoker — ถือ reference ของ
ICommandและเรียกExecute()/Undo()เมื่อถึงเวลา โดยไม่รู้จัก concrete type เลย (เช่นCommandHistory, ปุ่ม UI, หรือ job scheduler) - ICommand — interface กลางที่ทุกคำสั่งต้องมี อย่างน้อยคือ
Execute()ถ้าต้องการ undo ก็เพิ่มUndo() - ConcreteCommand — เก็บ reference ของ
Receiverและ parameter ที่จำเป็น รวมถึง state ที่ต้องใช้ตอน undo (เช่น ค่าก่อนแก้ไข) - Receiver — class ที่มี business logic จริง ๆ ที่ถูกเรียกใช้งาน (เช่น
Document,Light,BankAccount)
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานทั่วไปคือ client ประกอบคำสั่งขึ้นมาก่อน (bind receiver + parameter) แล้วส่งให้ invoker เก็บหรือเรียกทันที เมื่อ invoker เรียก Execute() คำสั่งจะไปสั่งงาน receiver จริง และถ้าต้องการย้อนกลับ คำสั่งเดิมนั่นเองจะรู้วิธี Undo() เพราะมันเก็บ state ที่จำเป็นไว้ตั้งแต่ก่อน execute
sequenceDiagram
participant C as Client
participant I as Invoker
participant Cmd as ConcreteCommand
participant R as Receiver
C->>Cmd: new ConcreteCommand(receiver, param)
C->>I: SetCommand(cmd)
I->>Cmd: Execute()
Cmd->>R: Action(param)
Note over I,Cmd: เก็บ cmd ไว้ใน history stack
I->>Cmd: Undo()
Cmd->>R: ReverseAction()
จุดสำคัญคือ invoker ผูกกับ ICommand เท่านั้น ไม่ผูกกับ ConcreteCommand หรือ Receiver โดยตรง — เพิ่มคำสั่งใหม่กี่ชนิดก็ได้โดยไม่ต้องแตะ code invoker เลยสักบรรทัด ในบางระบบยังเอาหลายคำสั่งมารวมเป็น MacroCommand เดียว (composite ของคำสั่ง) เพื่อ execute/undo เป็นชุดเดียวกันได้ด้วย
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้จำลอง text editor ง่าย ๆ ที่รองรับ undo/redo ผ่านคำสั่งสองแบบ พร้อม CommandHistory ที่ทำหน้าที่เป็น invoker:
// Receiver — เก็บ business logic จริงของเอกสารpublic class Document{ private readonly StringBuilder _content = new(); public string Content => _content.ToString();
public void Append(string text) => _content.Append(text); public void RemoveLast(int length) => _content.Remove(_content.Length - length, length);}
// ICommand — สัญญากลางที่ invoker รู้จักแค่นี้public interface ICommand{ void Execute(); void Undo();}
// ConcreteCommand #1 — เพิ่มข้อความ พร้อมจำความยาวไว้ใช้ตอน undopublic class AddTextCommand : ICommand{ private readonly Document _doc; private readonly string _text;
public AddTextCommand(Document doc, string text) { _doc = doc; _text = text; }
public void Execute() => _doc.Append(_text); public void Undo() => _doc.RemoveLast(_text.Length);}
// ConcreteCommand #2 — ลบข้อความท้ายสุด พร้อมสำรอง state ไว้ก่อน executepublic class DeleteLastCommand : ICommand{ private readonly Document _doc; private readonly int _length; private string _removedText = string.Empty;
public DeleteLastCommand(Document doc, int length) { _doc = doc; _length = length; }
public void Execute() { // สำรองข้อความที่กำลังจะถูกลบไว้ก่อน เพื่อให้ Undo คืนค่าได้ _removedText = _doc.Content[^_length..]; _doc.RemoveLast(_length); }
public void Undo() => _doc.Append(_removedText);}
// Invoker — ไม่รู้จักคำสั่งชนิดใด ๆ เจาะจง รู้แค่ ICommandpublic class CommandHistory{ private readonly Stack<ICommand> _history = new();
public void Execute(ICommand command) { command.Execute(); _history.Push(command); }
public void UndoLast() { if (_history.Count == 0) return; var last = _history.Pop(); last.Undo(); }}
// การใช้งานฝั่ง Clientvar document = new Document();var history = new CommandHistory();
history.Execute(new AddTextCommand(document, "สวัสดี "));history.Execute(new AddTextCommand(document, "โลก"));Console.WriteLine(document.Content); // "สวัสดี โลก"
history.UndoLast();Console.WriteLine(document.Content); // "สวัสดี "จุดสังเกต: CommandHistory (invoker) ไม่มี if command is AddTextCommand เลยสักบรรทัด — เพิ่มคำสั่งใหม่ เช่น ReplaceTextCommand ก็ทำได้ทันทีโดยไม่แตะ invoker
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการ parameterize object ด้วยการกระทำ (action) แทนที่จะ hardcode logic ไว้ในตัวเรียก เช่น ปุ่ม UI, menu item, keyboard shortcut ที่ใช้ action เดียวกัน
- ต้องการ undo/redo แบบหลายระดับ โดยให้แต่ละคำสั่งรู้วิธีย้อนกลับตัวเอง
- ต้องการ จัดคิวงาน เพื่อทำภายหลัง หรือส่งข้ามกระบวนการ/เครื่อง (เช่น job queue, ส่งคำสั่งผ่าน network)
- ต้องการ log/replay คำสั่งที่เกิดขึ้น เพื่อ audit trail หรือทำ macro recording
- กำลังสร้างระบบแบบ CQRS ที่ต้องแยก “คำสั่งเปลี่ยนสถานะ” ออกจาก query อย่างชัดเจน — Command object คือหน่วยพื้นฐานของฝั่ง write
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- การกระทำมีแค่ไม่กี่แบบ เรียบง่าย ไม่มีแนวโน้มจะ undo/queue/log — เพิ่ม layer ของ command object เข้าไปมีแต่ทำให้ code อ่านยากขึ้นโดยไม่ได้อะไรคืน
- ต้องการแค่เรียก method ตรง ๆ แบบ synchronous ครั้งเดียวจบ ไม่มีความจำเป็นต้อง decouple invoker กับ receiver
- ทีมยังไม่มี pattern การจัดการ state สำหรับ undo (เช่น memento หรือ snapshot) การทำ
Undo()แบบสุ่มเดาอาจทำให้ state เพี้ยนได้ง่ายกว่าประโยชน์ที่ได้
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | แยก invoker ออกจาก receiver อย่างสมบูรณ์ (Single Responsibility) |
| ข้อดี | เพิ่มคำสั่งใหม่ได้โดยไม่แก้ code invoker (Open/Closed) |
| ข้อดี | รองรับ undo/redo, queue, log, macro composition ได้เป็นธรรมชาติ |
| ข้อดี | ทดสอบง่ายขึ้น เพราะแต่ละคำสั่งเป็น object แยกที่ mock receiver ได้ |
| ข้อเสีย | เพิ่มจำนวน class ในระบบ (คำสั่งละ1 class) ทำให้ code verbose ขึ้น |
| ข้อเสีย | เพิ่ม indirection อีกชั้น อาจทำให้ตามรอย flow การทำงานยากขึ้นสำหรับคนอ่านใหม่ |
| ข้อเสีย | ถ้าออกแบบ Undo() ไม่รอบคอบ (ไม่ได้ backup state ให้ครบ) การย้อนกลับอาจทำให้ระบบเข้าสู่ state ที่ไม่ถูกต้อง |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Mediator — มักใช้คู่กัน: mediator ประสานงานระหว่าง object หลายตัว ส่วน command ห่อคำขอแต่ละอันที่ mediator ส่งต่อ
- CQRS — Command object คือหน่วยพื้นฐานของฝั่ง write ใน CQRS
- Memento — ใช้เก็บ snapshot ของ state เพื่อให้
Undo()ของ command คืนค่าได้แม่นยำ - Strategy — โครงสร้างคล้ายกัน (ห่อ behavior ไว้หลัง interface) แต่ Strategy เน้นสลับ “วิธีทำ” ส่วน Command เน้นห่อ “คำขอที่จะทำ” พร้อม undo
- Open-Closed — invoker เปิดรับคำสั่งใหม่ได้โดยไม่ต้องแก้ code เดิม