[Design Pattern] 행동 패턴 8. 커맨드 (Command)
Table of Contents
커맨드 패턴 #
- 메서드 호출을 실체화(객체화)한 것이다.
- 클라이언트의 요구사항을 객체로 만들어서 캡슐화한다.
- 이것을 실행하는 객체는 구체적인 요구사항을 모르고 실행한다.
- 특징
- 작업을 호출하는 클래스와 작업을 수행하는 클래스를 분리했다.
- 요구사항을 큐에 저장하거나, 로그로 기록하거나, 작업을 취소시킬 수도 있다.
명칭 | 하는 일 | 비유 |
---|---|---|
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 #
- https://refactoring.guru/ko/design-patterns
- 로버트 나이스트롬, 게임 프로그래밍 패턴
- 에릭 프리먼 & 엘리자베스 롭슨, 헤드 퍼스트 디자인 패턴