- 1. Command Pattern คืออะไร
- 2. ปัญหาที่ต้องการแก้
- 3. องค์ประกอบของ Command Pattern
- 4. ออกแบบ Undo/Redo ก่อนเขียนโค้ด
- 5. ตัวอย่าง Java แบบครบถ้วน
- 6. อ่านโค้ดให้เห็นการแยกหน้าที่
- 7. ทำไมต้องสร้าง Command ใหม่ทุกครั้ง
- 8. ถ้าไม่ต้องใช้ Undo: Command กับ Lambda
- 9. การนำไปใช้จริง
- 10. เปรียบเทียบกับรูปแบบใกล้เคียง
- 11. ข้อดีและข้อจำกัด
- 12. เวิร์กช็อปและแบบฝึกหัด
- 13. ประเด็นสำคัญที่ควรจำ
- แหล่งอ้างอิง
เปลี่ยนคำขอให้เป็นออบเจ็กต์ เพื่อแยกผู้เรียกใช้ออกจากรายละเอียดการทำงาน และต่อยอดสู่การเก็บประวัติ Undo/Redo หรือคิวงาน
ระดับ: พื้นฐานถึงปานกลาง
ความรู้ก่อนเรียน: Java, interface, composition และ collections
สภาพแวดล้อม: JDK 17 ขึ้นไป ไม่ต้องติดตั้งไลบรารีเพิ่มเติม
#1. Command Pattern คืออะไร
Command Pattern เป็นรูปแบบการออกแบบเชิงพฤติกรรม (Behavioral Design Pattern) ที่ห่อหุ้มคำขอให้กลายเป็นออบเจ็กต์ โดยออบเจ็กต์นั้นรู้ว่าจะทำอะไร ใช้ข้อมูลใด และต้องเรียกออบเจ็กต์ใดให้ทำงาน
ตัวอย่างเช่น การเพิ่มข้อความหนึ่งครั้งสามารถแทนด้วยออบเจ็กต์ที่ประกอบด้วยตัวแก้ไขข้อความและข้อความที่ต้องการเพิ่ม ผู้เรียกใช้เพียงเรียก execute() โดยไม่ต้องรู้วิธีจัดการข้อความภายใน
แก่นของรูปแบบนี้คือ ทำให้คำขอเป็นสิ่งที่ส่งต่อ จัดเก็บ และเรียกใช้ภายหลังได้ ความสามารถอย่าง Undo, Redo, retry และการบันทึกคิวถาวรต้องออกแบบเพิ่มตามลักษณะงาน ไม่ได้เกิดขึ้นเองเพียงเพราะใช้ Command Pattern
#2. ปัญหาที่ต้องการแก้
สมมติว่าโปรแกรมมีปุ่ม เมนู และแป้นพิมพ์ลัดสำหรับเพิ่มหรือล้างข้อความ หากแต่ละส่วนเขียนรายละเอียดการทำงานเอง โค้ดจะซ้ำและการเพิ่มประวัติการแก้ไขจะกระจายอยู่หลายแห่ง
// แต่ละส่วนของ UI ผูกกับรายละเอียดการแก้ไขโดยตรง
editor.append("Hello");
editor.clear();
การเรียกเมธอดโดยตรงเหมาะกับงานเรียบง่าย แต่เมื่อจำเป็นต้องเก็บคำขอไว้ในประวัติ เปลี่ยนคำสั่งที่ผูกกับปุ่ม หรือส่งคำขอเข้าคิว การมีออบเจ็กต์คำสั่งจะช่วยจัดโครงสร้างได้ชัดเจนขึ้น
// ผู้เรียกใช้จัดการคำสั่งผ่านสัญญาร่วม
history.run(new AppendTextCommand(editor, "Hello"));
history.run(new ClearTextCommand(editor));
ผู้เรียกใช้ยังต้องมีจุดที่เลือกคำสั่ง แต่รายละเอียดของการแก้ไขจะอยู่ภายในคำสั่งและตัวแก้ไขข้อความ
#3. องค์ประกอบของ Command Pattern
| บทบาท | หน้าที่ | ตัวอย่างในบทความ |
|---|---|---|
| Command | กำหนดสัญญาสำหรับเรียกคำสั่ง | Command |
| Concrete Command | เก็บข้อมูลคำขอและเรียกผู้ทำงานจริง | AppendTextCommand, ClearTextCommand |
| Receiver | ทำงานกับข้อมูลจริง | TextEditor |
| Invoker | เรียกคำสั่งและจัดการประวัติ | CommandHistory |
| Client | สร้างและเชื่อมออบเจ็กต์เข้าด้วยกัน | main() |
ในตัวอย่างนี้เราเพิ่ม undo() เข้าไปในสัญญา Command เพราะทุกคำสั่งของเวิร์กช็อปรองรับการย้อนกลับ หากระบบมีทั้งคำสั่งที่ย้อนกลับได้และไม่ได้ สามารถแยกเป็น Command กับ UndoableCommand แทน
classDiagram
direction TB
class Command {
<<interface>>
+execute() void
+undo() void
}
class CommandHistory {
+run(Command) void
+undo() boolean
+redo() boolean
}
class AppendTextCommand
class ClearTextCommand
class TextEditor {
+append(String) void
+clear() void
+getText() String
+restore(String) void
}
Command <|.. AppendTextCommand
Command <|.. ClearTextCommand
CommandHistory --> Command : invokes
AppendTextCommand --> TextEditor : edits
ClearTextCommand --> TextEditor : edits
main() ซึ่งเป็น Client สร้างทั้ง TextEditor, คำสั่ง และ CommandHistory ส่วน CommandHistory เรียกใช้ผ่าน Command จึงไม่ต้องตรวจสอบว่าคำสั่งนั้นเป็นการเพิ่มหรือล้างข้อความ
#4. ออกแบบ Undo/Redo ก่อนเขียนโค้ด
เวิร์กช็อปนี้ใช้ประวัติสองกองแบบ LIFO ได้แก่ undoStack สำหรับคำสั่งที่ทำสำเร็จแล้ว และ redoStack สำหรับคำสั่งที่ถูกย้อนกลับ
| การกระทำ | สิ่งที่เกิดขึ้นกับคำสั่ง | สิ่งที่เกิดขึ้นกับประวัติ |
|---|---|---|
| Run | เรียก execute() |
เพิ่มเข้า Undo แล้วล้าง Redo |
| Undo | เรียก undo() ของคำสั่งล่าสุด |
ย้ายจาก Undo ไป Redo |
| Redo | เรียก execute() ของคำสั่งที่เพิ่ง Undo |
ย้ายจาก Redo ไป Undo |
| Undo/Redo เมื่อไม่มีประวัติ | ไม่เรียกคำสั่ง | คืนค่า false |
คำสั่งแต่ละตัวเก็บข้อความก่อนแก้ไขไว้เป็น snapshot เมื่อ Undo จึงคืนสถานะเดิมได้ตรงตามประวัติ แทนการเดาว่าต้องทำอะไรตรงข้าม เช่น การ Undo คำสั่งล้างข้อความต้องนำข้อความเดิมกลับมา ไม่ใช่เพียงเพิ่มข้อความว่าง
เราใช้ Deque ร่วมกับ ArrayDeque สำหรับ stack เอกสาร Java SE แนะนำ Deque แทนคลาส Stack รุ่นเก่าสำหรับการใช้งานลักษณะนี้ [1]
ข้อตกลงของตัวอย่าง: ใช้ตัวแก้ไขข้อความหนึ่งตัวต่อประวัติหนึ่งชุด เรียกใช้งานจากเธรดเดียว เปลี่ยนข้อความผ่านประวัติเท่านั้น และสร้าง Command ใหม่สำหรับการกระทำใหม่ทุกครั้ง Command เดิมจะถูกนำกลับมาใช้เฉพาะโดยกระบวนการ Redo
#5. ตัวอย่าง Java แบบครบถ้วน
สร้างไฟล์ชื่อ CommandPatternDemo.java แล้วคัดลอกโค้ดต่อไปนี้ลงในไฟล์ คลาสย่อยถูกวางไว้ภายในคลาสหลักเพื่อให้ทดลองได้ด้วยไฟล์เดียว ในโปรเจกต์จริงสามารถแยกแต่ละคลาสเป็นไฟล์ได้
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
public class CommandPatternDemo {
// Command: undo() is an extension for this example.
interface Command {
void execute();
void undo();
}
// Receiver: owns the text and performs the actual operations.
static final class TextEditor {
private String text = "";
public String getText() {
return text;
}
public void append(String value) {
text += Objects.requireNonNull(value);
}
public void clear() {
text = "";
}
public void restore(String snapshot) {
text = Objects.requireNonNull(snapshot);
}
}
// Concrete Command: one instance per new append action.
static final class AppendTextCommand implements Command {
private final TextEditor editor;
private final String value;
private String before;
AppendTextCommand(TextEditor editor, String value) {
this.editor = Objects.requireNonNull(editor);
this.value = Objects.requireNonNull(value);
}
@Override
public void execute() {
before = editor.getText();
editor.append(value);
}
@Override
public void undo() {
if (before == null) {
throw new IllegalStateException("Command has not been executed");
}
editor.restore(before);
}
}
// Concrete Command: remembers the text that will be cleared.
static final class ClearTextCommand implements Command {
private final TextEditor editor;
private String before;
ClearTextCommand(TextEditor editor) {
this.editor = Objects.requireNonNull(editor);
}
@Override
public void execute() {
before = editor.getText();
editor.clear();
}
@Override
public void undo() {
if (before == null) {
throw new IllegalStateException("Command has not been executed");
}
editor.restore(before);
}
}
// Invoker: depends on Command, not on concrete editor operations.
static final class CommandHistory {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void run(Command command) {
Objects.requireNonNull(command).execute();
undoStack.push(command);
redoStack.clear();
}
public boolean undo() {
if (undoStack.isEmpty()) {
return false;
}
Command command = undoStack.peek();
command.undo();
undoStack.pop();
redoStack.push(command);
return true;
}
public boolean redo() {
if (redoStack.isEmpty()) {
return false;
}
Command command = redoStack.peek();
command.execute();
redoStack.pop();
undoStack.push(command);
return true;
}
}
// Client: wires the receiver, commands, and invoker together.
public static void main(String[] args) {
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();
history.run(new AppendTextCommand(editor, "Hello"));
show("Append 1", editor);
history.run(new AppendTextCommand(editor, ", Java!"));
show("Append 2", editor);
history.undo();
show("Undo", editor);
history.redo();
show("Redo", editor);
history.run(new ClearTextCommand(editor));
show("Clear", editor);
history.undo();
show("Undo clear", editor);
history.undo();
show("Undo append", editor);
history.run(new AppendTextCommand(editor, ", Command!"));
show("New branch", editor);
System.out.println("Redo available: " + history.redo());
}
private static void show(String action, TextEditor editor) {
System.out.printf("%s -> [%s]%n", action, editor.getText());
}
}
#วิธีรัน
เปิด Terminal ในโฟลเดอร์ที่เก็บไฟล์ แล้วใช้คำสั่ง:
javac -encoding UTF-8 CommandPatternDemo.java
java CommandPatternDemo
หรือรันแบบ source-file mode บน JDK 17:
java CommandPatternDemo.java
#ผลลัพธ์ที่คาดหวัง
Append 1 -> [Hello]
Append 2 -> [Hello, Java!]
Undo -> [Hello]
Redo -> [Hello, Java!]
Clear -> []
Undo clear -> [Hello, Java!]
Undo append -> [Hello]
New branch -> [Hello, Command!]
Redo available: false
บรรทัดสุดท้ายเป็น false เพราะหลัง Undo แล้วผู้ใช้เพิ่มคำสั่งใหม่ เส้นทาง Redo เดิมจึงถูกล้างออก ตามพฤติกรรมประวัติแบบเส้นตรงที่เราออกแบบไว้
#6. อ่านโค้ดให้เห็นการแยกหน้าที่
#Command กำหนดสิ่งที่ Invoker เรียกได้
CommandHistory รู้จักเพียง execute() และ undo() จึงรองรับ Concrete Command ใหม่ได้โดยไม่ต้องเพิ่ม if หรือ switch สำหรับชนิดคำสั่งในประวัติ
#Concrete Command เก็บบริบทของคำขอ
AppendTextCommand เก็บ editor, value และ before โดย value คือข้อมูลคำขอ ส่วน before คือข้อมูลที่ช่วยย้อนกลับ การแยกสองส่วนนี้ทำให้เห็นว่าข้อมูลคำขอกับสถานะระหว่างการทำงานมีบทบาทต่างกัน
#Receiver ทำงานกับข้อมูล
TextEditor รู้วิธีเพิ่ม ล้าง และคืนข้อความ แต่ไม่ต้องรู้ว่าผู้ใช้สั่งผ่านปุ่ม เมนู CLI หรือประวัติคำสั่ง
#Invoker คุมลำดับและประวัติ
run() บันทึกคำสั่งหลัง execute() สำเร็จ และล้าง Redo เมื่อเกิดคำสั่งใหม่ ส่วน undo() และ redo() ใช้ peek() เพื่อให้คำสั่งยังอยู่ในกองเดิมจนการทำงานสำเร็จ จากนั้นจึงย้ายกอง
ลำดับนี้ช่วยไม่ให้ประวัติถูกนำออกก่อนการเรียกสำเร็จ แต่ไม่ได้ทำให้ Receiver ย้อนกลับเองเมื่อคำสั่งทำงานไปบางส่วนแล้วเกิดข้อผิดพลาด ระบบที่มีหลายขั้นตอนยังต้องออกแบบความเป็นอะตอมมิกหรือวิธีชดเชยการทำงานเพิ่ม
#7. ทำไมต้องสร้าง Command ใหม่ทุกครั้ง
Command ในตัวอย่างมีสถานะ before ที่เปลี่ยนเมื่อเรียก execute() หากนำออบเจ็กต์เดียวกันส่งเข้า run() หลายครั้ง รายการประวัติจะอ้างถึงออบเจ็กต์เดียวกัน และ snapshot เดิมจะถูกเขียนทับ
// สร้าง instance ใหม่ให้แต่ละรายการประวัติ
history.run(new AppendTextCommand(editor, "A"));
history.run(new AppendTextCommand(editor, "A"));
ตัวอย่างสั้นนี้ไม่ได้บังคับกฎดังกล่าวด้วย state machine หากต้องรับคำสั่งจากหลายส่วนของระบบ อาจเพิ่มการตรวจ lifecycle หรือใช้ factory ที่สร้างออบเจ็กต์ใหม่ทุกครั้ง
การเรียก execute() ซ้ำภายใน redo() ใช้ได้กับงานเพิ่มและล้างข้อความนี้ ภายใต้ข้อตกลงที่ไม่มีการแก้ข้อความนอกประวัติ สำหรับคำสั่งที่สุ่มค่า อ่านเวลาปัจจุบัน เรียก API หรือส่งอีเมล ควรออกแบบ redo() แยกและเก็บผลลัพธ์ที่จำเป็น แทนการเรียกกระบวนการเดิมซ้ำโดยไม่พิจารณาผลข้างเคียง
#8. ถ้าไม่ต้องใช้ Undo: Command กับ Lambda
หากต้องการเพียงห่อหุ้มงานที่เรียกได้ สามารถใช้ functional interface ซึ่งมี abstract method เดียว:
@FunctionalInterface
interface SimpleCommand {
void execute();
}
// ตัวอย่างภายในเมธอด โดยมี editor พร้อมใช้งานแล้ว
SimpleCommand append = () -> editor.append("Hello");
append.execute();
อีกทางเลือกหนึ่งคือใช้ Runnable ซึ่งเป็น functional interface ของ Java และมีเมธอด run() [2]
Runnable task = () -> editor.append("Hello");
task.run();
การเรียก task.run() โดยตรงทำงานบนเธรดที่เรียก ไม่ได้สร้างเธรดใหม่ ต้องใช้กลไกอย่าง Thread หรือ executor หากต้องการจัดการการทำงานต่างเธรด และต้องออกแบบการเข้าถึง Receiver ให้เหมาะสมด้วย
Lambda เหมาะกับคำสั่งสั้นที่ไม่ต้องมีประวัติซับซ้อน ส่วนคลาส Command ชัดเจนกว่าเมื่อจำเป็นต้องมีชื่อคำสั่ง ข้อมูลตรวจสอบย้อนหลัง สถานะ หรือวิธี Undo/Redo ของตนเอง Runnable ไม่ได้มีสัญญา Undo ให้โดยตรง
#9. การนำไปใช้จริง
| งาน | แนวทางประยุกต์ | สิ่งที่ต้องออกแบบเพิ่ม |
|---|---|---|
| ปุ่ม เมนู และแป้นพิมพ์ลัด | ผูกหลายจุดเข้ากับการกระทำเดียวกัน | สถานะเปิดใช้งานและข้อมูลแสดงผล |
| โปรแกรมแก้ไขข้อความหรือภาพ | เก็บการแก้ไขเป็นประวัติ | ขนาดประวัติและข้อมูลสำหรับ Undo |
| คิวงาน | สร้างคำขอก่อน แล้วให้ worker ทำภายหลัง | รูปแบบข้อมูลถาวร การยืนยันงาน และ retry |
| งานตามกำหนดเวลา | แยกนิยามงานออกจากเวลาที่เรียก | ข้อมูลเวลาและการป้องกันงานซ้ำ |
| Macro | รวมคำสั่งย่อยเป็นหนึ่งชุด | ลำดับ Undo ย้อนหลังและกรณีสำเร็จบางส่วน |
| คำสั่งทางธุรกิจ | เก็บผู้สั่ง พารามิเตอร์ และรหัสคำขอ | การตรวจสิทธิ์และผลลัพธ์ที่ตรวจสอบได้ |
ใน Java Swing, Action ช่วยรวมการกระทำที่เข้าถึงได้จากหลาย controls ไว้ในออบเจ็กต์เดียว พร้อมข้อมูลและสถานะของ UI [3] จึงใช้ประกอบการเรียนรู้แนวคิดการแยกคำสั่งออกจากปุ่มได้
ส่วน UndoableEdit และ UndoManager เป็น API สำหรับจัดการการแก้ไขที่ Undo/Redo ได้โดยตรง [4] หากสร้าง editor ด้วย Swing ควรศึกษากลไกเหล่านี้ก่อนสร้างระบบประวัติเอง
สำหรับคิวที่ต้องอยู่ข้ามการรีสตาร์ต ควรเก็บข้อมูลคำขอ เช่น ประเภทคำสั่ง รหัสเอกสาร เนื้อหา และรหัสคำขอ แล้วให้ worker ประกอบการทำงานจากข้อมูลนั้น การมีออบเจ็กต์ Java อยู่ในหน่วยความจำอย่างเดียวไม่ได้ทำให้คิวถาวรหรือส่งข้ามเครื่องได้
#10. เปรียบเทียบกับรูปแบบใกล้เคียง
| รูปแบบ | คำถามหลัก | ตัวอย่าง |
|---|---|---|
| Command | จะห่อหุ้มคำขอเพื่อส่งต่อและเรียกใช้ได้อย่างไร | เพิ่มข้อความ ล้างข้อความ หรือเก็บเป็นประวัติ |
| Strategy | จะสลับวิธีทำงานภายใต้เป้าหมายเดียวกันได้อย่างไร | เลือกวิธีเรียงข้อมูลหรือคำนวณส่วนลด |
| Memento | จะเก็บและคืนสถานะโดยรักษาการห่อหุ้มข้อมูลได้อย่างไร | ให้ editor สร้าง snapshot ภายในของตนเอง |
| Chain of Responsibility | ใครในลำดับผู้จัดการคำขอควรรับหรือส่งต่อ | ตรวจสอบคำขอตามลำดับก่อนดำเนินการ |
Command กับ Strategy อาจมีโครงสร้าง interface คล้ายกัน แต่เจตนาต่างกัน Command เน้นการแทนคำขอ ส่วน Strategy เน้นทางเลือกของอัลกอริทึม
ตัวอย่างนี้ใช้ snapshot แบบ String อย่างเรียบง่าย หาก Receiver มีข้อมูลภายในหลายส่วน สามารถใช้ Memento ร่วมกับ Command เพื่อให้ Receiver เป็นผู้สร้างและกู้คืน snapshot โดยไม่เปิดเผยรายละเอียดภายในให้ Command รู้ทั้งหมด
#11. ข้อดีและข้อจำกัด
ข้อดี
- แยกผู้เรียกใช้จากรายละเอียดของงานที่ทำ
- เพิ่ม Concrete Command ใหม่ได้โดยคง Invoker เดิมไว้ เมื่อสัญญายังเพียงพอ
- นำคำขอไปเก็บในประวัติหรือจัดลำดับการทำงานได้
- ทดสอบคำสั่งและ Receiver แยกจากส่วน UI ได้ง่ายขึ้น
ข้อจำกัด
- จำนวนคลาสและโครงสร้างเพิ่มขึ้น อาจเกินความจำเป็นสำหรับการเรียกเมธอดสั้นเพียงจุดเดียว
- การเก็บข้อความทั้งชุดทุกครั้งทำให้หน่วยความจำของประวัติเพิ่มขึ้น โดยกรณีเลวร้ายที่แต่ละ snapshot ยาวใกล้เคียงกัน จะเติบโตตามจำนวนประวัติคูณขนาดข้อความ
- การเปลี่ยน Receiver นอกประวัติทำให้ snapshot อาจไม่สอดคล้องกับสถานะปัจจุบัน
- การย้อนกลับการกระทำภายนอก เช่น การส่งอีเมล ไม่เหมือนการคืนข้อความในหน่วยความจำ อาจต้องมีคำสั่งชดเชยที่มีความหมายทางธุรกิจ
ตัวอย่างนี้เก็บประวัติไว้ในหน่วยความจำ ใช้เธรดเดียว และยังไม่มีขีดจำกัดจำนวนรายการ การเพิ่มข้อความว่างหรือล้าง editor ที่ว่างยังถือเป็นรายการประวัติหนึ่งรายการตามโค้ด หากไม่ต้องการพฤติกรรมนี้ ควรให้คำสั่งรายงานว่ามีการเปลี่ยนแปลงจริงหรือไม่ก่อนบันทึกประวัติ
#12. เวิร์กช็อปและแบบฝึกหัด
#ขั้นที่ 1: ทดลองลำดับพื้นฐาน
รันโค้ดในหัวข้อ 5 แล้วตรวจว่าผลลัพธ์ตรงกับตัวอย่าง โดยเน้นการ Undo คำสั่งล้างข้อความ และกรณีเพิ่มคำสั่งใหม่หลัง Undo
#ขั้นที่ 2: เพิ่ม ReplaceTextCommand
สร้างคำสั่งเปลี่ยนข้อความทั้งชุด โดยรับ TextEditor และข้อความใหม่ เก็บข้อความเดิมใน execute() และคืนข้อความเดิมใน undo() จากนั้นเรียกผ่าน history.run(...) โดยไม่แก้ไข CommandHistory
#ขั้นที่ 3: ตรวจพฤติกรรมของประวัติ
| กรณีตรวจสอบ | ผลที่ควรได้ |
|---|---|
| Undo เมื่อยังไม่มีคำสั่ง | คืน false และข้อความไม่เปลี่ยน |
| Redo เมื่อยังไม่มีคำสั่ง | คืน false และข้อความไม่เปลี่ยน |
| เพิ่ม A แล้ว B จากนั้น Undo สองครั้ง | กลับเป็นข้อความว่าง |
| หลังกรณีก่อนหน้า Redo สองครั้ง | ได้ข้อความ AB |
| ล้างข้อความแล้ว Undo | ได้ข้อความก่อนล้างครบถ้วน |
| Undo แล้วเพิ่มคำสั่งใหม่ | ประวัติ Redo เดิมใช้ไม่ได้ |
| คำสั่งล้มเหลวก่อนเปลี่ยนข้อมูล | ไม่เพิ่มคำสั่งเข้า Undo และไม่ล้าง Redo เดิม |
#ขั้นที่ 4: ขยายการออกแบบ
- เพิ่ม
name()เพื่อแสดงชื่อคำสั่งล่าสุด เช่น Undo Append Text - จำกัดประวัติไว้ 100 รายการ แล้วกำหนดวิธีทิ้งรายการที่เก่าที่สุด
- สร้าง Macro ที่ Undo คำสั่งย่อยจากท้ายไปต้น พร้อมระบุนโยบายเมื่อทำได้เพียงบางส่วน
- แยก
Commandที่มีเพียงexecute()ออกจากUndoableCommand - ทดลองแทน snapshot ทั้งข้อความด้วยข้อมูลส่วนที่เปลี่ยน และอธิบายผลต่อหน่วยความจำกับความซับซ้อน
#13. ประเด็นสำคัญที่ควรจำ
Command Pattern ทำให้คำขอมีตัวตนเป็นออบเจ็กต์ Invoker เรียกผ่านสัญญาร่วม Concrete Command เก็บบริบทและประสานงาน ส่วน Receiver ทำงานจริง ความสามารถ Undo/Redo ของตัวอย่างเกิดจากการเพิ่ม snapshot และประวัติสองกองลงบนโครงสร้างดังกล่าว
หากความต้องการมีเพียงการเรียกเมธอดครั้งเดียว การเรียกโดยตรงก็เพียงพอ เมื่อคำขอต้องถูกส่งต่อ เก็บประวัติ เปลี่ยนจุดเรียก หรือประมวลผลภายหลัง Command Pattern จะช่วยจัดความรับผิดชอบให้ชัดเจนขึ้น
#แหล่งอ้างอิง
บทความและโค้ดตัวอย่างเขียนขึ้นเพื่ออธิบายการออกแบบ ส่วนพฤติกรรมของ Java API อ้างอิงเอกสาร Java SE 17 ต่อไปนี้:
- Oracle: Deque — ใช้เป็น stack และเปรียบเทียบกับ Stack
- Oracle: Runnable — functional interface และเมธอด run
- Oracle: Action — การใช้การกระทำร่วมกันระหว่าง controls
- Oracle: UndoableEdit — การออกแบบการแก้ไขที่ Undo/Redo ได้
คำสำคัญ: Java, Design Patterns, Command Pattern, Behavioral Pattern, Undo, Redo, Object-Oriented Design