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

Command

Command เปลี่ยนคำขอให้เป็น object ระดับชั้นหนึ่ง (first-class object) ที่พกพาทุกสิ่งที่จำเป็นในการทำแอ็กชัน และที่สำคัญที่สุดคือวิธีย้อนกลับแอ็กชันนั้น เพราะคำขอเป็น object จึงเข้าคิว บันทึก log ส่งให้ผู้ที่จะรันในภายหลัง และผลักเข้าไปใน history stack เพื่อรองรับ undo ได้

โปรแกรมแก้ไขข้อความต้องการการพิมพ์ การลบ และการจัดรูปแบบ ซึ่งทั้งหมดถูกกระตุ้นจากเมนู ปุ่ม และแป้นพิมพ์ลัด หากตัวควบคุมแต่ละตัวเรียก method ของ editor โดยตรง ตัวควบคุมและ editor ก็เชื่อมติดกัน และฟีเจอร์อย่าง undo ก็กลายเป็นฝันร้าย เพราะไม่มีบันทึกว่า อะไร เกิดขึ้น มีแต่สถานะผลลัพธ์ การจะ undo คุณต้องสร้างส่วนกลับของทุกแอ็กชันที่เป็นไปได้ขึ้นมาแบบ inline

แนวคิดที่ขาดไปคือ ตัวคำขอเองในฐานะค่าที่คุณเก็บไว้ได้ Command ทำให้แต่ละแอ็กชันเป็น object ที่มี method execute และ method undo ตัวควบคุมเพียงแค่ส่ง command ให้ invoker invoker รัน command แล้วจดจำไว้บน stack จากนั้น undo ก็เป็นแบบเดียวกันหมด คือ pop command ตัวล่าสุดออกมาแล้วเรียก undo ของตัวเอง editor ไม่ต้องสนใจอีกต่อไปว่าใครกระตุ้นแอ็กชัน

classDiagram
  class Command {
    <<interface>>
    +execute()
    +undo()
  }
  class InsertText {
    +execute()
    +undo()
  }
  class Editor {
    -text: string
    +insert(s)
    +deleteLast(n)
  }
  class History {
    -stack: List~Command~
    +run(cmd)
    +undo()
  }
  Command <|.. InsertText
  InsertText --> Editor : acts on
  History o--> Command : remembers
invoker History รัน object Command ที่กระทำกับ receiver Editor และสามารถย้อนกลับได้
  • Command — interface ที่ประกาศ execute และ undo
  • Concrete Command — ผูก receiver เข้ากับแอ็กชันและเก็บสิ่งที่จำเป็นในการย้อนกลับแอ็กชันนั้น
  • Receiver — object ที่ทำงานจริง ในที่นี้คือ editor ที่ถือข้อความ
  • Invoker — กระตุ้น command และเก็บ history ในที่นี้คือ history stack ที่ขับเคลื่อน undo ด้วย
  • Client — สร้าง concrete command และส่งให้ invoker

editor ที่มีคำสั่ง insert ถูกบันทึกบน history stack เพื่อให้แอ็กชันล่าสุดถูก undo ได้ แต่ละ command เก็บข้อมูลมากพอที่จะย้อนกลับตัวเอง

class Editor {
text = '';
insert(s: string): void {
this.text += s;
}
deleteLast(n: number): void {
this.text = this.text.slice(0, -n);
}
}
interface Command {
execute(): void;
undo(): void;
}
class InsertText implements Command {
constructor(private editor: Editor, private value: string) {}
execute(): void {
this.editor.insert(this.value);
}
undo(): void {
this.editor.deleteLast(this.value.length);
}
}
class History {
private stack: Command[] = [];
run(cmd: Command): void {
cmd.execute();
this.stack.push(cmd);
}
undo(): void {
const cmd = this.stack.pop();
if (cmd) cmd.undo();
}
}
const editor = new Editor();
const history = new History();
history.run(new InsertText(editor, 'hello '));
history.run(new InsertText(editor, 'world'));
console.log(editor.text); // "hello world"
history.undo();
console.log(editor.text); // "hello "
  • ข้อดี: แยก object ที่กระตุ้นแอ็กชันออกจาก object ที่ทำแอ็กชันนั้น
  • ข้อดี: เพราะแต่ละคำขอเป็น object คุณจึงได้ undo, redo, คิว, การ logging และ macro มาแทบจะฟรี
  • ข้อดี: command ใหม่ถูกเพิ่มได้โดยไม่ต้องเปลี่ยน invoker หรือ receiver
  • ข้อเสีย: ทุกแอ็กชันกลายเป็น class ที่เป็นพิธีรีตองมากสำหรับการเรียกครั้งเดียวจบ
  • ข้อเสีย: undo ที่เชื่อถือได้ต้องให้แต่ละ command เก็บสถานะมากพอที่จะย้อนกลับตัวเอง ซึ่งอาจยุ่งยากสำหรับการดำเนินการที่ซับซ้อน
  • Memento เป็นคู่หูที่พบบ่อย แทนที่จะคำนวณส่วนกลับ command สามารถ snapshot สถานะด้วย memento แล้วคืนค่าสถานะนั้นเมื่อ undo
  • Strategy ก็ห่อหุ้มพฤติกรรมไว้ใน object เช่นกัน แต่ strategy ถูกเลือกเพื่อทำให้อัลกอริทึมผันแปร ขณะที่ command แทนคำขอที่เจาะจงและถูกเลื่อนเวลาออกไป
CommandStrategyChain of Responsibility
ห่อaction (พร้อม state)algorithmrequest ที่ผ่าน handler หลายตัว
undoได้ (เก็บ state ไว้)ไม่ได้ไม่ได้
queueได้ไม่ได้ไม่ได้
ตัวอย่างeditor action, HTTP requestpricing rule, sortmiddleware, event handler chain
Command pattern ห่อหุ้มสิ่งใด?
invoker มีบทบาทอะไร?
ทำไม undo จึงเป็นเรื่องธรรมชาติกับ Command?
สิ่งใดเป็นข้อเสียที่แท้จริงของ Command?