/ OOD

[365 วันแห่งโปรแกรม #day61] Command pattern

วันที่หกสิบเอ็ดของ ‪#‎365วันแห่งโปรแกรม นี่เป็นตอนที่สองของ Behavioral pattern ครับ วันนี้ผมขอนำเสนอเรื่อง Command pattern


Command pattern

หลายๆ คนคงจะคุ้นเคยกับคำว่า Command ในเชิงโปรแกรมมิ่งกันมาบ้างแล้ว เช่น ในการเขียนโปรแกรมแบบ MVVM เราก็มีการ bind ส่วนที่เป็น View เข้ากับ Command หรือว่าเมื่อมี Event หนึ่งเกิดขึ้นแล้วสั่งให้ทำอะไรต่อ พวกนั้นก็คือ Command ที่เราจะคุยกันในวันนี้ครับ

ใน Command pattern นั้น เราจะมีการสร้าง object ชิ้นนึงที่ encapsulate ข้อมูล และการทำงานทุกอย่างไว้ และเจ้าตัว object นี้จะถูกเรียกขึ้นมาทำงานเมื่อเกิดเหตุการณ์อะไรบางอย่างขึ้น

Command Pattern จะประกอบไปด้วย 4 ส่วนสำคัญได้แก่ command, receiver, invoker และ client

  • command เป็น object ห่อหุ้มการทำงานของ receiver เอาไว้ กล่าวคือ command จะเป็นผู้เรียก receiver ขึ้นมาทำงานนั่นเอง

  • receiver คือส่วนที่ทำงานจริงๆ ถูกเรียกโดย command

  • invoker คือผู้สั่งการ ผู้นี้จะรู้ว่าต้องเรียก command อย่างไร เรียกเมื่อไหร่ แต่ไม่รู้หรอกว่า command นั้นทำอะไรบ้าง

  • client คือผู้สร้างและเลือกว่าจะให้ invoker ใช้ command ไหนในการทำอะไร (โดยการ inject เข้าไป)

The "check" at a diner

การสั่งอาหารเป็นตัวอย่างนึงของ Command pattern จากแผนภาพจะเห็นว่า customer สั่งอาหารกับ waiter ไป waiter ก็จะจดว่ามีอะไรบ้าง พอสั่งเสร็จปุ๊บ waiter ก็จะนำรายการที่สั่งไปส่งที่ครัว ซึ่งก็จะมีการจัดทำอาหารตามที่สั่งในลำดับต่อไป

command มันอยู่ตรงที่ waiter จด order ครับ พอจดเสร็จปุ๊บก็ทำการ execute โดยการส่งให้คนที่รับผิดชอบ (receiver) ไปทำงานกันต่อนั่นเอง

Command pattern กับ Callback คือสิ่งเดียวกันหรือไม่?

Command pattern เป็นวิธีหนึ่งที่ใช้ในการ implement callback ครับ เนื่องจากภาษาโปรแกรมเชิงวัตถุ (แบบ pure) มีจุดด้อยที่ไม่สามารถสร้าง Higher-Order function แบบที่ทำกันใน Functional Programming ได้ ดังนั้นการส่ง command เข้าไปให้ invoker จึงเป็นวิธีที่ดีในการทดแทนจุดด้อยนั้น

ประโยชน์ของ Command pattern

Command pattern ทำให้เราสามารถการผูกติดระหว่าง invoker กับเนื้องานที่จะทำ หรือ receiver ลง ซึ่งทำให้เราสามารถกำหนดและปรับเปลี่ยน behavior ได้ในตอนรันไทม์

ตัวอย่างการนำไปใช้

โดยทั่วไปแล้วเรื่อง GUI จะเป็นตัวอย่างที่ดีของ Command pattern แต่พอดีผมไปเจอตัวอย่างเรื่องการ handle ปุ่มบน joy stick แล้วก็เห็นว่าน่าสนใจดีเลยเอามาใช้

สิ่งแรกที่เราต้องรู้ก่อนที่จะเริ่มตัวอย่างนี้คือ โดยธรรมชาติของเกมแล้วจะเขียนขึ้นโดยใช้ infinity loop ครอบไว้ทั้งหมด ในแต่ละครั้งของการวนก็จะมีการตรวจสอบต่างๆ รวมไปถึงตรวจสอบ interaction ของผู้เล่น และวาดแสดงผล ประเด็นของเราวันนี้คือส่วนตรวจสอบการกดปุ่มบน joy stick ครับ

void InputHandler::handleInput()
{
    if (isPressed(BUTTON_X)) jump();
    else if (isPressed(BUTTON_Y)) fireGun();
    else if (isPressed(BUTTON_A)) swapWeapon();
    else if (isPressed(BUTTON_B)) lurchIneffectively();
}

จากโค้ดข้างต้นพบว่าถ้าผู้เล่นกด BUTTON_X ก็ให้ทำอย่างนึง กด BUTTON_Y ก็ให้ทำอย่างนึง อะไรแบบนี้ โดยการทำงานเป็นการเรียก method ภายในเกมโดยตรงเลย ซึ่งถ้าวันดีคืนดีเราอยากให้ผู้เล่นกำหนดเองได้ล่ะ ว่ากดปุ่มนี้ให้เป็นอะไร กดปุ่มนู้นให้ทำอะไร เป็นไงล่ะครับ พังสิ!!

สิ่งแรกที่เราต้องทำคือแยก logic ของ action เกมเป็น command ออกจากส่วนตรวจสอบ output ให้หมด แล้วใช้วิธี inject เข้ามาตอนรันไทม์แทน

เริ่มจากสร้าง base command ก่อนเลยครับ

class Command
{
    public:
    virtual ~Command() {}
    virtual void execute() = 0;
};

interface มี method execute() อย่างเดียวสำหรับให้ตัวเกมเรียกใช้เพื่อ handle input

หลังจากสร้างเสร็จเราก็ต้องไปสร้าง concrete command ครับ

class JumpCommand : public Command
{
    public:
    virtual void execute() { jump(); }
};

class FireCommand : public Command
{
    public:
    virtual void execute() { fireGun(); }
};

สมมติว่าเสร็จแล้ว ตอนนี้เรามี Command สำหรับกระโดกับยิงละ ต่อไปเราก็ต้องแก้ให้ตัวเกมเรียกใช้ Command พวกนี้แทน

class InputHandler
{
    public:
    void handleInput();

    // Methods to bind commands...

    private:
    Command* buttonX_;
    Command* buttonY_;
    Command* buttonA_;
    Command* buttonB_;
};

ประกาศตัวแปร Command สำหรับแต่ละปุ่มเอาไว้ แล้วก็สร้าง method สำหรับ inject command ด้วยก็ดีครับ หลังจากนั้นกลับไปแก้ที่ handleInput ต่อ

void InputHandler::handleInput()
{
    if (isPressed(BUTTON_X)) buttonX_->execute();
    else if (isPressed(BUTTON_Y)) buttonY_->execute();
    else if (isPressed(BUTTON_A)) buttonA_->execute();
    else if (isPressed(BUTTON_B)) buttonB_->execute();
}

คราวนี้แค่สั่ง execute บน command ของปุ่มนั้นๆ เพียงเท่านี้ code ของ game และ command ก็จะเป็นอิสระต่อกันมากขึ้นแล้ว

ปัญหาต่อมาถ้ามีบาง command ที่ต้องการรับค่าจาก invoker ล่ะ ทำได้ไหม? ได้ครับก็ไปแก้ interface command ใหม่ให้รับพารามิเตอร์ได้ โดยทั่วไปแล้วการ input ของ user จะกระทำต่อตัวละครที่บังคับเท่านั้น เราก็อาจจะส่งพารามิเตอร์เป็นตัวละครเข้าไป

class Command
{
    public:
    virtual ~Command() {}
    virtual void execute(GameActor& actor) = 0;
};

class JumpCommand : public Command
{
    public:
    virtual void execute(GameActor& actor)
    {
        actor.jump();
    }
};

แล้วก็มาแก้ handleInput

Command* InputHandler::handleInput()
{
    if (isPressed(BUTTON_X)) return buttonX_;
    if (isPressed(BUTTON_Y)) return buttonY_;
    if (isPressed(BUTTON_A)) return buttonA_;
    if (isPressed(BUTTON_B)) return buttonB_;

    // Nothing pressed, so do nothing.
    return NULL;
}

//game loop start
    Command* command = inputHandler.handleInput();
    if (command)
    {
        command->execute(actor);
    }
//game loop end

เพียงเท่านี้ command ของเราก็จะมี object ของตัวละครไปใช้ละ

*จริงๆ จะใช้วิธี injection ตอนสร้าง command ก็ได้ครับเผื่อ command ต้องการพารามิเตอร์ที่แตกต่างกัน

References

Command - GameProgrammingPatterns

Command pattern - Wikipedia

Command Design Pattern - SourceMaking

Command Pattern - OODesign

#‎day61 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...