본문 바로가기

3.구현/C or C++

[패턴] Command

아래는 한경두님의 글이니다. 그림이 없어서 머리에 그리기 힘들 것 같네요.- 박재성

#############################################################################

Command 패턴

의도

객체의 request를 캡슐화해서 각각 다른 request를 매개변수화(parameterize)할 수 있도록 한다. 그래서 그것들을 queue에 집어넣거나 기록(log)하거나,undo등을 지원하도록 한다.

동기

또다시 스타크래프트의 유닛 클래스를 만드는 상황을 생각해 봅시다. ( 맨날 스타크래프 트의 예만 드는군요. 저는 스타크를 별로 좋아하지 않지만 대중성있고 잘 만들어진 게임이기 때문에 스타크의 예를 드는 것이예요~ )

지난번의 상태 패턴에 이어서 유닛 클래스를 만들 경우를 생각해 봅시다. 스타에서 지원 하는 웨이포인트를 모르시는 분은 없을 것입니다. 큐에다 이동 명령들을 쌓아놓고 실행하도록 하는 것인데 오직 이동 명령만 넣을 수가 있습니다. 그러나 Earth2140이던가? 그 게임에서는 모든 명령을 큐에다 넣고 실행 할 수 있었던 걸로 기억 되는군요. 작전 명령을 내려놓고, "여의 땅!" 하면 작전 명령에 따라 한 단계씩 수행하는 것입니다. 정말 편리한 기능이죠. 그것을 보고 뚱띵이 빌로퍼가 말했다고 칩시다. "우리도 저 기능을 넣자!! 이게 니 몫이라는건 알고 있겠지?"

당신은 찍소리도 못하고 만들어야만 할 것입니다. 이것을 구현하려면 Queue가 필요할 것 입니다. 그러나 보통의 자료를 저장하는 큐가 아닌 기능을 저장하는 큐가 필요할 것입니다. 그럼 어떻게 기능을 자료처럼 큐에 저장할 것이냐? 아마도 그 방법 중 하나로서 함수포인 터가 있을 것입니다. 그러나 지난 강좌에서 말씀 드렸다시피 함수포인터보다 더 많은 장점을 가진 것이 있습니다. 바로 가상함수와 인터페이스 클래스를 이용한 함수포인터의 구현!!

이것을 이용해 Queue, log Undo, list, Tree 등등을 구현하는 것이 바로 이 Command 패턴인 것입니다. 사실 이 가상함수와 인터페이스 클래스를 이용한 함수포인터는 대부분의 패턴의 기본이 됩니다. 이것을 응용해서 만들어진 것들이 이 패턴이라고도 할 수 있고, 이 패턴들이 모두 이것으로 통한다고도 할 수 있습니다.

소스

그렇다면!! 이제 Command패턴의 구현에 대해서 알아보도록 합시다요~.

class ICommand //각 명령들의 인터페이스 클래스
{
   public:
   virtual void Execute() = 0;
};
//전방 참조
class cCommand_Attack;
class cCommand_Move;
class cCommand_Stop;
class cCommand_HoldPosition;
class cCommand_Patrol;

class IUnit
{
   IState * _pState; //state패턴 강좌에서 cZealot클래스에 있었던
   //변수를 여기로 옮겨옴
   public:
  //Shift가 눌렸을 때 이 함수가 호출되겠죠.
  virtual void ChangeToPlanMode( IUnit *& pPtrUnit )
  {
      //cPlanDecorator를 붙인다.
      new cPlanDecorator( pPtrUnit );
  }
  //Shift가 떼어졌을 때 이 함수가 호출
  virtual void ChangeToNormalMode()
  {}

  //상태에 대한 ID를 리턴하는 함수( state패턴 강좌에서 IState클래스가
  // 수정 되었습니다)
  ID_STATE GetStateID()
  {
       return _pState->GetID();
  }
  //나머지는 지난번 강좌의 IUnit클래스와 같음
   ....
   ....
};

//cUnitDecorator는 지난번 강좌(Decorator)에서 나왔던 그 놈임.
class cPlanDecorator : public cUnitDecorator
{
   IUnit * _pActor;
   queue< ICommand * > * _pCommandQ; //STL의 queue를 사용
   bool _IsInputMode; //명령의 입력 모드인가?(true)
   //아니면 실행 모드인가?(false)
   public:
   cPlanDecorator( IUnit *& pActor )
   : cUnitDecorator( pActor ), _pActor( pActor ), _IsInputMode(true)
   {}

   void ChangeToPlanMode( IUnit *& pPtrUnit )
   {
       _IsInputMode = true; 
   }
   void ChangeToNormalMode()
   {
       _IsInputMode = false;
   } 
   void ClearPlan()
   {
       _pCommandQ->clear();
   }
   void Attack( IUnit * Target ) 
   { 
       //Shift를 누른 상태로 공격 명령을 내리면 명령 추가
       if( _IsInputMode )
       _pCommandQ->push( new cCommand_Attack( Target ) );
       //Shift를 뗀 상태로 공격 명령을 내리면 큐의 모든 명령이 취소되
       //고, 공격 명령을 바로 실행.
       else 
       {
            _pActor->Attack( Target );
            SelfDestroy(); 
        } 
   }
   void Move( int x, int y ) 
   {
       //Attack과 마찬가지..
       if( _IsInputMode )
           _pCommandQ->push( new cCommand_Move( x, y ) );
       else
       {
           _pActor->Move( x, y );
           SelfDestroy();
       }
   }
   void Stop()
   {
        //Stop명령은 큐에 집어넣을 수 없음.
        if( !_IsInputMode )
       {
            _pActor->Stop();
            SelfDestory();
       }
   }
//HoldPosition, Patrol등은 귀찮으니 생략함..
//일꾼 같으면 Build명령이 있을테고, 마법유닛은 Special명령이 있겠지만
//이것도 모두 위의 Attack이나 Move처럼 처리하면 됩니다.

   void Update()
   {
       //Stop, HoldPosition, Patrol등 무한히 지속되는 명령들이 아닌
       //나머지 명령들은 명령수행을 마쳤을 때 Stop상태로 바뀌므로
       //Stop상태가 되었을 때 다음 명령을 실행.
        if( _pActor->GetStateID() == IState::STATE_STOP )
       {
            if( !_pCommandQ->empty() )
           {
                ICommand * pCommand = _pCommandQ->front();
                pCommand->Execute();
                delete pCommand;
          }
           else
          {
                SelfDestroy();
          }
     }

        _pActor->Update();
   }
};

class cCommand_Attack : public ICommand
{
   IUnit * _pActor;
   IUnit * _pTarget;
   public:
   //주의!! 여기서는 편의상 생성자의 선언과 정의를 한꺼번 
   //에 해서 인라인이 되어 버렸지만 생성자를 인라인으로 만 
   //드는 것은 좋지 않습니다.자세한건 Effective C++참조
   cCommand_Attack( IUnit * pActor, IUnit * pTarget )
   : _pActor( pActor ), _pTarget( pTarget )
  {
  }
   void Execute()
   {
   _pActor->Attack( _pTarget );
    }
};

class cCommand_Move : public ICommand
{
   IUnit * _pActor;
   int _DestX, _DestY;
   public:
   cCommand_Move( IUnit * pActor, int x, int y )
   : _pActor( pActor ), _DestX( x ), _DestY( y )
   { }
   void Execute()
   {
        _pActor->Move( _DestX, _DestY );
   }
};

//이런 식으로 사용하게 되겠죠.
void OnKeyDown( int KeyCode )
{
   if( KeyCode == SHIFT )
   {
         IUnit &* pSelectedUnit = GetSelectedUnit();
        pSelectedUnit->ChangeToPlanMode( pSeletecdUnit );
   }
}

void OnKeyUp( int KeyCode )
{
    if( KeyCode == SHIFT )
   {
         IUnit &* pSelectedUnit = GetSelectedUnit();
        pSelectedUnit->ChangeToNormalMode();
    }
}

이 외에도 Command 패턴은 많은 곳에서 쓰입니다. 비동기적인 처리를 위해 어떤 메시지를 받고 나서 수행은 나중에 할 때라던가, 우선순위 큐에 넣어서 이벤트 시스템에 쓰이기도 합니다. undo, redo에서도 사용되구요.Attack과 Move 명령만 구현했는데, 나머지 명령들도

비슷한 방법으로 구현하시면 됩니다.

위에서 만든 명령 예약 기능은 지난 강좌에서 했던 State패턴과 잘 연동되어 돌아갈 것입니다. 유닛이 구체적으로 무엇인지와 관계 없이 그의 인터페이스를 그대로 호출하기 때문

이지요. 각 유닛은 Attack, Move 등등의 명령 함수들만 재정의 해주면 되는 것입니다.

위에서 구현한 것과 다른 방식이 있는데, 각각의 Command클래스에 구현 자체를 전부 넣어버리는 방식입니다. 위에서는 Command클래스의 Execute가 호출되면 유닛의 함수를 호출하기만 하는데, (이걸 '대리'한다고 합니다.) 이 '대리'를 하지 않고 Execute함수에다

구현을 해서 필요한 일을 모두 Execute함수 내에서 수행하는 방식입니다.

위의 것을 이해하셨다면 undo나 redo, log등도 만드실 수 있을 것입니다. undo, redo의 예제는 디자인 패턴 책에 나와있으니 필요하신 분은 책을 참고하세요.

이 패턴을 응용하면 다양한 자료구조에 모두 적용할 수 있습니다. tree, list, 우선순위 큐, 해쉬 등등...

p.s 주의사항!! 제 패턴 강좌의 소스들은 컴파일을 해보지 않은 코드들이기 때문에 실제 사용시에는 문제를 일으킬 수도 있습니다. 때문에 위의 소스 코드는 이해의 목적으로만 사용하시고 복사, 붙여넣기를 해서 사용하진 마세요.

출처: 하이텔 GMA의 한경두님의 글...

반응형

'3.구현 > C or C++' 카테고리의 다른 글

함수호출 규약  (0) 2007.06.18
Metaprogramming  (0) 2007.02.16
Vectors in STL  (0) 2007.02.16
블록 메모리 복사 성능시험  (0) 2007.02.16
메모리 복사 성능시험 (memcpy)  (0) 2007.02.16