명령 패턴
메소드 호출을 실체화, 즉 일급 객체로 만드는 것을 의미
바꿔 말하면 함수 호출을 객체로 감쌌다는 의미이다.
void InputHandler::handleInput() {
if (isPressed(Button_X)) jump();
else if (isPressed(Button_X)) fireGun();
else if (isPressed(Button_A)) swapWeapon();
else if (isPressed(Button_B)) lurchIneffectively();
}
handleInput
함수가 매 프레임마다 호출되어 해당 키 입력에 따른 함수를 호출한다.
이때 문제점은 키 변경이 불가능하다는 것!
class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
};
Command
라는 추상 클래스 선언하고
class JumpCommand : public Command {
public:
virtual void execute() { jump(); }
};
class FireCommand : public Command {
public:
virtual void execute() { fireGun(); }
};
Command
를 상속받는 각 행동의 하위 클래스를 만든다.
class InputHandler {
public:
void handleInput();
private:
Command* buttonX_;
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
void InputHandler::handleInput() {
if (isPressed(Button_X)) buttonX_->execute();
else if (isPressed(Button_X)) buttonY_->execute();
else if (isPressed(Button_A)) buttonA_->execute();
else if (isPressed(Button_B)) buttonB_->execute();
}
InputHandler
는 명령 패턴을 사용하지 않을 때와는 달리 직접 함수를 호출하지 않는다.
따라서 각 버튼의 Command
객체만 바꿔주면 키 변경도 지원할 수 있게 되었다.
그러나 jump()
, fireGun()
과 같이 (아마도 플레이어를 조작하는) 전역함수만을 실행할 수 있다는 문제가 있다.
플레이어 캐릭터만이 아니라 그 어떤 액터에게도 지시할 수 있도록,
제어하려는 객체를 함수가 직접 찾지 않고 밖에서 전달해주자!
class Command {
public:
virtual ~Command() {}
virtual void execute(GameActor& actor) = 0;
};
class JumpCommand : public Command {
public:
virtual void execute(GameActor& actor) {
actor.jump();
}
};
Command
의 자식 클래스는 execute()가 호출될 때 인자로 GameActor
객체를 전달받음으로써 어떤 객체든 조작할 수 있게 되었다.
Command* InputHandler::handleInput() {
if (isPressed(Button_X)) return buttonX_;
else if (isPressed(Button_X)) return buttonY_;
else if (isPressed(Button_A)) return buttonA_;
else if (isPressed(Button_B)) return buttonB_;
return NULL:
}
어떤 액터를 조작할지는 handleInput
에서 모르기 때문에, 입력 처리에서는 단순히 해당 입력에 대한 Command
객체만 리턴한다. 즉, 함수 호출 시점을 지연시킨다.
Command* command = inputHandler.handleInput();
if (command) {
command->execute(actor);
}
actor
만 바꾸어도 어떤 액터든 제어할 수 있게 되었다!
이렇게 입력을 처리할 때 직접 함수를 호출하는 방식에서 명령 패턴을 사용하면서 입력 처리와 액터 사이의 커플링을 제거했다.
명령 패턴을 이용한다면 실행취소 기능을 쉽게 구현할 수 있다.
class MoveUnitCommand : public Command {
public:
MoveUnitCommand(Unit* unit, int x, int y) : unit_(unit), x_(x), y_(y) {}
virtual void execute() {
unit_->moveTo(x_, y_);
}
private:
Unit* unit_;
int x_;
int y_;
};
아까 만든 JumpCommand
와 같이 MoveUnitCommand
은 어떠한 객체를 이동시키는 명령이다.
그치만 액터와 명령 사이를 추상화로 격리한 JumpCommand
와 달리 MoveUnitCommand
는 이동시킬 객체와 위치값을 생성자에서 받아서 명시적으로 바인드했다.
JumpCommand
JumpCommand
객체는 매번 재사용된다.MoveUnitCommand
MoveUnitCommand
객체를 생성한다.class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0; // 실행 취소
};
class MoveUnitCommand : public Command {
public:
MoveUnitCommand(Unit* unit, int x, int y) : unit_(unit), xBefore_(0), yBefore_(0), x_(x), y_(y) {}
virtual void execute() {
// 실행취소를 대비해 기존 위치 저장
xBefore_ = unit_->x();
yBefore_ = unit_->y();
unit_->moveTo(x_, y_);
}
virtual void undo() {
unit_->moveTo(xBefore_, yBefore_);
}
private:
Unit* unit_;
int x_, y_;
int xBefore_, yBefore_;
};
실행취소를 지원하는 undo
함수를 만들고 handleInput
에서는 매번 새로운 MoveUnitCommand
와 같은 명령 객체들을 리턴하고 이를 리스트에 저장해둔다.
실행취소를 할 때마다 현재 명령을 가리키는 포인터를 뒤로 이동하면서 undo
를 호출하면 실행취소 기능 완성!
반대로 재실행을 하려면 포인터를 다음으로 이동하여 execute
를 실행하면 된다.
명령을 함수로 구현하지 않은 이유는 C++이 클로저를 제대로 지원하지 않기 때문이다.
vector<function<int(int)>> vec;
void func()
{
auto num = 3;
vec.emplace_back(
[&](int value) { return value + num; }
);
}
int main()
{
func();
auto funcc = vec.at(0);
cout<<funcc(3); // 쓰레기값
}
C++은 별도의 처리가 없다면 클로저로 캡쳐된 변수는 선언된 스코프와 수명이 동일하다.
따라서 실제 람다 함수가 실행되는 순간에 해당 변수가 유효하지 않을 수 있다.
참고: [C++] 람다식(functional) 사용법과 클로저(closure)
C++에서 클로저 지원하기
1.shared_ptr
변수를 캡쳐auto obj = std::make_shared<SomeClass>(); auto lambda = [obj]() { /* 코드 */ };
bind
로 함수와 변수 바인딩int x = 5; std::function<int()> func = std::bind([](int val) { return val; }, x);
const a = () => {
let i = 0;
return () => {
i += 1;
return i;
}
}
const b = a();
console.log(b()); // 1
console.log(b()); // 2
console.log(b()); // 3
반면 자바스크립트는 클로저를 통해 함수의 상태를 저장할 수 있어서 굳이 클래스를 이용한 명령 패턴을 사용할 필요가 없다.