민돌이2 2023. 3. 5. 23:48

명령 패턴은 메서드 호출을 실체화한 것이다. 실체화는 실제하는 것으로 만든다는 의미이고, 프로그래밍에선 변수에 저장하거나 함수에 전달할 수 있도록 객체로 바꾼다는 것을 의미한다.

즉, 명령 패턴을 '메서드 호출을 실체화한 것'이라고 한 것은 함수 호출을 객체로 감쌌다는 의미다.

 

입력키 변경

게임에서는 유저의 입력을 받고, 게임에서 의미 있는 행동으로 전환된다.

void InputHandler::HandleInput()
{
	if( IsPress( BUTTON_X ) ) Jump();
	else if( IsPress( BUTTON_Y ) ) FireGun();
	else if( IsPress( BUTTON_A ) ) SwapWeapon();
	else if( IsPress( BUTTON_B ) ) LurchIneffectively();
}

이런 경우 HandleInput 함수는 매 프레임 호출될 것이다. 또한 키 변경이 불가능하다.

 

키 변경을 지원하려면 함수를 직접 호출하지 말고 교체 가능한 객체로 바꾸면 된다.

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

class JumpCommand : public Command
{
public:
	virtual void Execute() override { Jump(); }
};

class FireCommand : public Command
{
public:
	virtual void Execute() override { FireGun(); }
};

class InputHandler
{
	Command* buttonX;
	Command* buttonY;
	Command* buttonA;
	Command* buttonB;

public:
	void HandleInput();
};

void InputHandler::HandleInput()
{
	if( IsPress( BUTTON_X ) ) buttonX->Execute();
	else if( IsPress( BUTTON_Y ) ) buttonY->Execute();
	else if( IsPress( BUTTON_A ) ) buttonA->Execute();
	else if( IsPress( BUTTON_B ) ) buttonB->Execute();
}

 

액터에게 지시하기

위의 경우 명확한 한계가 존재한다. 재활용성이 떨어진다는 것이다. JumpCommand의 Execute 함수가 호출될 때 누구를 점프시킬 것인가? 이런 제약을 유연하게 만드릭 위해 제어하려는 객체를 함수에서 찾지 말고 밖에서 전달하게 한다.

class Command
{
public:
	virtual void Execute( Actor& actor ) = 0;
};

class JumpCommand : public Command
{
public:
	virtual void Execute( Actor& actor ) override { actor.Jump(); }
};

Command* InputHandler::HandleInput()
{
	if( IsPress( BUTTON_X ) ) return buttonX;
	else if( IsPress( BUTTON_Y ) ) return buttonY;
	else if( IsPress( BUTTON_A ) ) return buttonA;
	else if( IsPress( BUTTON_B ) ) return buttonB;

	return nullptr;
}

{
	Command* command = inputHandler.HandleInput();
	if( command )
		command->Execute( actor );
}

대부분의 경우 유저가 조종하는 것은 플레이어 캐릭터기 때문에 사실상 기능적으론 이전과 다를게 없다. 하지만, 명령과 액터 사이에 추상 계층을 한 단계 더 둠으로써 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 됬다. 레식에서 사람을 조종하다가 드론을 시점으로 조종하는 상황을 생각하면 이해하기 쉽다.

 

실행취소와 재실행

명령 객체가 어떤 작업을 실행할 수 있다면, 이를 실행취소(undo) 할 수 있게 만들 수 있다.

class Command
{
public:
	virtual void Execute() = 0;
	virtual void Undo() = 0;
};

class MoveUnitCommnad: public Command
{
	Unit* unit;
	int x;
	int y;
	int beforeX;
	int beforeY;

public:
	MoveUnitCommnad( Unit* unit, int x, int y )
		: unit( unit ), x( x ), y( y ), beforeX( 0 ), beforeY( 0 )
	{}

	virtual void Execute() override
	{
		beforeX = unit->GetX();
		beforeY = unit->GetY();
		unit->MoveTo( x, y );
	}

	virtual void Undo() override
	{
		unit->MoveTo( beforeX, beforeY );
	}
};

명령에서 변경하려는 액터와 명령 사이를 추상화한 이전 예제와 다르게, 위 예제는 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인드되어 있다. 

 

클래스만 좋고, 함수형은 별로인가?

함수 포인터에는 상태를 저장할 수 없고, functor는 클래스를 정의해야 한다. C++11에 도입된 람다는 메모리를 직접 관리해야 하기 때문에 쓰기가 까다롭다.

그렇다고 다른 언어에서도 명령 패턴에 함수를 쓰면 안된다는 것은 아니다. 언어에서 클로저(closur)(*주 1)를 지원한다면 안 쓸 이유가 없다. 어떻게 보면 명령 패턴은 클로저를 제대로 지원하지 않는 언어에서 클로저를 흉내 내는 방법중 하나일 뿐이다.

 

정리

커맨드 패턴의 요점은 하나의 객체로 여러 객체에 명령을 한다는 점이다.

일상생활에 예를 들어보면 삼성, LG 등의 TV가 각각 리모컨에 작동하는 것 보다 리모컨 한개로 모든 TV가 작동하도록 하는 것이다.

 

(*주 1) 람다(lamba) vs 클로저(closur)

람다는 람다 표현식의 줄임말이고, 그저 표현식일 뿐이다. 단지 프로그램 소스 코드에서만 존재하고 런타임에서 람다는 존재하지 않는다. 람다의 표현식에 대한 런타임 결과는 오브젝트의 생성이다. 이 오브젝트를 클로저라고 한다.

728x90