Design Pattern 101 — Command Pattern
Command Pattern เป็น pattern ตัวต่อไปของกลุ่ม Behavioral ก็ตรงไปตรงมาว่าเป็น pattern เกี่ยวกับ “คำสั่ง” รูปแบบเดียวกันที่ติดต่อกับระบบอื่นที่มีความหลากหลายในการควบคุม ในบทความนี้จะไปทบทวนและทำความเข้าใจว่า Command Pattern จะมาช่วยแก้ปัญหาอะไรบ้าง
Command Pattern
Problem
ถ้าเราต้องพัฒนาระบบควบคุม Home Automation ด้วย remote control ที่ภายในบ้านประกอบด้วยเครื่องใช้ไฟฟ้าจำนวนหนึ่ง โดยที่ remote control จะประกอบด้วย Slot ที่ควบคุมเครื่องใช้ไฟฟ้าได้ 7 ระบบ แต่ละระบบจะควบคุมด้วยการ “ON” “OFF” และ “UNDO” ได้
ซึ่งแต่ละเครื่องใช้ไฟฟ้าจะมี API ในการควบคุมที่แตกต่างกันออกไป ที่พัฒนาขึ้นโดยผู้ผลิต (Vendor) ของผู้ผลิตเครื่องใช้ไฟฟ้านั้นๆ
โดยที่เราต้องออกแบบซอฟต์แวร์ให้ครอบคลุมเครื่องใช้ไฟฟ้าทั้งหมดที่มี ณ ปัจจุบัน และในอนาคตอีกด้วย
จาก interface ของการควบคุมเครื่องใช้ไฟฟ้าต่างๆ จะเห็นได้ว่ามีความหลากหลายมาก เพียงแต่เราจะจัดกลุ่ม method ของ interface อย่างเช่น on()
หรือ off()
ก็คงไม่เพียงพอเพราะบาง interface ไม่มี method นี้ แต่มี method เฉพาะในการควบคุมต่างหาก อย่างเช่น openValue()
หรือ closeValue()
Solution
จากปัญหานี้ ถึงเวลาแล้วที่เราจะใช้ Design Pattern มาแก้ไขปัญหา และ pattern ที่เหมาะสมกับปัญหานี้ก็คือ Command Pattern โดยสร้าง interface ที่เป็น common ของคำสั่งที่ควบคุมเครื่องใช้ไฟฟ้าทั้งหมด
public interface Command {
void execute();
}
ใน interface Command
ประกอบด้วย method execute()
เพื่อที่จะประมวลผลคำสั่ง ของแต่ละคำสั่ง ดั้งนั้นลองมาสร้าง command แบบง่ายๆ ดูเพื่อเปิดไฟ
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
class LightOnCommand
เป็น command class ที่ implement interface Command
โดยที่ต้อง implement method execute()
และ pass Light object เข้ามาทาง constructor และเรียกใช้ method on()
ใน execute()
เพื่อควบคุมการเปิดไฟ ซึ่ง Light object เป็น Receiver ของ command (เป็นตัวรับคำสั่ง)
และลองเอา LightOnCommand
ไปใช้ใน remote control ง่ายๆ ดูว่าจะใช้อย่างไร
public class SimpleRemoteControl {
Command slot;
public void setCommand(Command command) {
slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
ใน class SimpleRemoteControl
มี method setCommand()
ที่รับค่า Command เข้ามาและเรียกใช้ method execute()
ตอนกดปุ่ม ( buttonWasPressed()
)
ใช้ SimpleRemoteControl
ด้วยการ pass Light object เข้าใน LightOnCommand
และ pass lightOn
เข้าไปใน remote
ด้วย setCommand(lightOn)
เปิดไฟก็ต้องเรียกใช้ method buttonWasPressed()
ซึ่งจะเห็นได้ว่า SimpleRemoteControl
นั้นเป็นตัว Invoker ของ command (เป็นตัวเรียกใช้คำสั่ง)
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonWasPressed();
ที่นี้ถ้าเราเพิ่มคำสั่ง GarageDoorOpenCommand
ก็จะใช้ remote แบบนี้
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
GarageDoor garageDoor = new GarageDoor();
GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor);
remote.setCommand(lightOn);
remote.buttonWasPressed();
remote.setCommand(garageOpen);
remote.buttonWasPressed();
แต่ช้าก่อน requirement ต้องการให้เราทำ remote control ที่รองรับ 7 ระบบ ซึ่ SimpleRemoteControl
ไม่สามารถรองรับได้แน่ ดังนั้นจะได้ class RemoteControl
public class RemoteControl {
public static final int SLOT_SIZE = 7;
Command[] onCommands;
Command[] offCommands;
public RemoteControl() {
onCommands = new Command[SLOT_SIZE];
offCommands = new Command[SLOT_SIZE];
Command noCommand = new NoCommand();
for(int i = 0; i < SLOT_SIZE; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
}
public void offButtonsWasPushed(int slot) {
offCommands[slot].execute();
}
}
และใช้ RemoteControl
ประมาณนี้ จะเห็นได้ว่า setCommand
ด้วย index ของ slot บน Remote Control และคำสั่ง “ON” และ “OFF” และในปุ่มที่กดจะระบุ index ด้วย
// creation of all command
remote.setCommand(0, livingRoomLightOn, livingRoomLightOff);
remote.setCommand(1, kitchenLightOn, kitchenLightOff);
remote.setCommand(2, ceilingFanOn, ceilingFanOff);
remote.setCommand(3, stereoOnWithCD, stereoOffWithCD);
remote.onButtonWasPushed(0);
remote.offButtonsWasPushed(0);
remote.onButtonWasPushed(1);
remote.offButtonsWasPushed(1);
remote.onButtonWasPushed(2);
remote.offButtonsWasPushed(2);
remote.onButtonWasPushed(3);
remote.offButtonsWasPushed(3);
Command Pattern จะซ่อน (encapsulate) request เป็น object จึงปล่อยให้เรากำหนดพารามิเตอร์ของ object อื่น ด้วย request, queue หรือ log request ที่แตกต่างกันได้ และรองรับ undoable operation อีกด้วย
เราจะเห็นได้ว่า command object จะซ่อน request ด้วยการเชื่อมโยงเซตของ action เข้าด้วยกันด้วย receiver โดยเฉพาะ เพื่อบรรลุเป้าหมายนี้ รวม action และ receiver เข้าเป็น object เดียว และ expose เพียง method เดียว นั้นก็คือ execute()
เมื่อเรียก execute()
จะทำให้ action ถูกเรียกใช้งานบน receiver นั้นๆ จากภายนอก ไม่มีทางรู้เลยว่าถายใน execute()
ทำอะไรบ้างกับ receiver รู้เพียงแต่ว่าเรียกใช้งาน execute()
ได้อย่างไร และยังได้เห็นอีกว่าใช้ parameterizing object ด้วย command
สรุป
จากที่ได้ทบทวนและใช้งาน Command Pattern นี้จะเห็นได้ว่า pattern นี้แก้ปัญหาแบบนี้ได้เป็นอย่างดี และทำให้แอพพลิเคชันที่สร้างขึ้นสามารถ ขยายได้ไปอีก โดยที่ไม่ต้องกลับไปแก้ไข code เดิมอีก เพิ่มเติมเฉพาะตรงจุดที่เรียกใช้งาน command เท่านั้น และก็เหมือนเดิมตัวอย่างเอามาจาก Head First Design Pattern 🥂