Skip to main content

[Design Pattern] 행동 패턴 8. 커맨드 (Command)




커맨드 패턴 #

커맨드

  • 메서드 호출을 실체화(객체화)한 것이다.
    • 클라이언트의 요구사항을 객체로 만들어서 캡슐화한다.
    • 이것을 실행하는 객체는 구체적인 요구사항을 모르고 실행한다.
  • 특징
    • 작업을 호출하는 클래스와 작업을 수행하는 클래스를 분리했다.
    • 요구사항을 큐에 저장하거나, 로그로 기록하거나, 작업을 취소시킬 수도 있다.
명칭 하는 일 비유
Invoker Command를 갖고서 요청을 시작한다. 웨이트리스
Command Receiver가 적절한 작업을 수행하도록 명령한다. 주문서
Receiver 실제 작업을 수행하는 액터이다. 주방장

게임에서의 예시 #

  • 유저 입력을 받는 코드를 만들고 싶다.
void InputHandler::handleInput()
{
  // 이렇게 하면 BUTTON_X는 무조건 jump()에만 연결된다. 
  // 입력 키 변경을 자유롭게 하려면 어떻게 해야할까?
  if (isPressed(BUTTON_X)) jump();
  
  // ...여러가지 입력 키와 동작들...
}

  • 커맨드 객체를 만든다.
// jump()를 교체 가능한 객체로 만들어버리자. 
class Command
{
public:
  virtual ~Command() {}
  virtual void execute() = 0;
};

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

class InputHandler
{
private:
  Command* buttonX;
  
public: 
  void handleInput()
  {
    // 이렇게 하면 BUTTON_X를 눌렀을 때 
    // buttonX에 연결된 동작이 수행된다. 
    if (isPressed(BUTTON_X)) buttonX->execute();
  }
};

  • 만약에 플레이어뿐만 아니라 다른 액터도 점프하게 하고 싶다면?
class Command
{
public:
  virtual ~Command() {}
  virtual void execute(GameActor& actor) = 0;
};

class JumpCommand : public Command
{
public:
  // 매개변수로 액터를 받는다. 
  // 그리고 그 액터가 점프하도록 한다. 
  virtual void execute(GameActor& actor) { actor.jump(); }
};

class InputHandler
{
private:
  Command* buttonX;
  
public: 
  Command* handleInput()
  {
    // 어떤 액터가 실행되어야 할지 모르므로, 함수 호출 시점을 지연하고 넘긴다. 
    if (isPressed(BUTTON_X)) buttonX;
    return NULL;
  }
};

void executeCommand()
{
  InputHandler inputHandler;
  GameActor actor;
  
  // 액터가 명령을 실행한다. 
  Command* command = inputHandler.handleInput();
  if (command) command->execute(actor);
}

다른 예시들 #

// Command
public interface Command
{
  public string name { get; }
  public void Execute();
  public void Undo();
}

public class NoCommand : Command
{
  public string name => "NoCommand";
  public NoCommand() { }
  public void Execute() { }
  public void Undo() { }
}

public class DoorCloseCommand : Command
{
  public string name => "DoorCloseCommand";

  Door door;

  public DoorCloseCommand(Door door)
  {
    this.door = door;
  }

  public void Execute()
  {
    door.Down();
  }
  
  public void Undo()
  {
    door.Up();
  }
}

public class DoorOpenCommand : Command
{
  public string name => return "DoorOpenCommand";

  Door door;

  public DoorOpenCommand(Door door)
  {
    this.door = door;
  }

  public void Execute()
  {
    door.Up();
  }
  
  public void Undo()
  {
    door.Down();
  }
}

// Receiver
public class Door
{
  private string name;

  public Door(string name)
  {
    this.name = name;   
  }

  public void Up()
  {
    Console.WriteLine(name + " Door up");
  }
  public void Down()
  {
    Console.WriteLine(name + " Door Down");
  }
}

// Invoker
public class RemoteControl
{
  private Command[] onCommands;
  private Command[] offCommands;
  private Command unDoCommand;

  public RemoteControl()
  {
    onCommands = new Command[7];
    offCommands = new Command[7];

    for (int i = 0; i < onCommands.Length; i++)
    {
      onCommands[i] = new NoCommand();
      offCommands[i] = new NoCommand();
    }

    unDoCommand = new NoCommand();
  }

  public void SetCommand(int slot, Command onCommand, Command offCommand)
  {
    onCommands[slot] = onCommand;
    offCommands[slot] = offCommand;
  }

  public void OnButtonPressed(int slot)
  {
    onCommands[slot].Execute();
    unDoCommand = offCommands[slot];
  }

  public void OffButtonPressed(int slot)
  {
    offCommands[slot].Execute(); 
    unDoCommand = onCommands[slot];
  }

  public void UndoButtonPressed()
  {
    unDoCommand.Undo();
  }
}

class Program
{
  static void Main(string[] args)
  {
    RemoteControl remoteControl = new RemoteControl();
    Door door = new Door("");
    
    // Command가 Receiver를 설정한다. 
    DoorOpenCommand doorOpen = new DoorOpenCommand(door);
    DoorCloseCommand doorClose = new DoorCloseCommand(door);

    // Invoker가 Command를 설정한다.  
    remoteControl.SetCommand(0, doorOpen, doorClose);
    
    // Command 실행 
    remoteControl.OnButtonPressed(0); // Door up
    remoteControl.UndoButtonPressed(); // Door down
  }
}

  • 일반화 코드
// Command
public interface ICommand
{
  void Execute();
}

// 간단한 Command
class SimpleCommand : ICommand
{
  private string _payload = string.Empty;

  public SimpleCommand(string payload)
  {
    this._payload = payload;
  }

  public void Execute()
  {
    Console.WriteLine($"SimpleCommand: ({this._payload})");
  }
}

// Receiver를 담은 Command
class ComplexCommand : ICommand
{
  private Receiver _receiver;

  // Receiver의 메서드를 호출하기 위해 필요한 필드들
  private string _a;
  private string _b;

  // Receiver를 받는다. 
  public ComplexCommand(Receiver receiver, string a, string b)
  {
    this._receiver = receiver;
    this._a = a;
    this._b = b;
  }

  // Receiver의 메서드를 호출할 수 있다.
  public void Execute()
  {
    Console.WriteLine("ComplexCommand: 리시버 호출로 동작한다.");
    this._receiver.DoSomething(this._a);
    this._receiver.DoSomethingElse(this._b);
  }
}

// Receiver
// 중요한 비즈니스 로직을 갖고 있어서, 모든 요청사항들을 다룰 수 있다. 
class Receiver
{
  public void DoSomething(string a)
  {
    Console.WriteLine($"Receiver: Working on ({a}.)");
  }

  public void DoSomethingElse(string b)
  {
    Console.WriteLine($"Receiver: Also working on ({b}.)");
  }
}

// Invoker 
class Invoker
{
  private ICommand _onStart;
  private ICommand _onFinish;

  // Command를 받는다. 
  public void SetOnStart(ICommand command)
  {
    this._onStart = command;
  }

  public void SetOnFinish(ICommand command)
  {
    this._onFinish = command;
  }

  // Invoker는 구체화된 Command나 구체화된 Receiver를 모른다. 
  // 그냥 Command를 실행시킬 뿐이다. 
  // 그러면 Command에 따라서 그 안에 있는 Receiver가 적절히 동작한다. 
  public void DoSomethingImportant()
  {
    if (this._onStart is ICommand)
    {
      this._onStart.Execute();
    }
    
    if (this._onFinish is ICommand)
    {
      this._onFinish.Execute();
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    Invoker invoker = new Invoker();
    Receiver receiver = new Receiver();
    
    // Invoker가 Command를 설정하고, Command가 Receiver를 설정한다. 
    invoker.SetOnStart(new SimpleCommand("Say Hi!"));
    invoker.SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));

    invoker.DoSomethingImportant();
    
    // 출력: 
    // SimpleCommand: (Say Hi!)
    // Receiver: Working on (Send email.)
    // Receiver: Also working on (Save report.)
  }
}



References #