해당 예시는 헤드 퍼스트 디자인 패턴을 참고했습니다.
위와 같은 생김새의 만능 리모콘을 개발해야 한다.
좌측의 0, 1, 2, 3 인덱스에 등록하여 어떠한 기기의 특정 기능을 수행하거나(ON
) 꺼버릴 수(OFF
) 있다.
예를 들어 0번째 인덱스의
ON
에 불을 켜는 기능,OFF
에 불을 끄는 기능 등록
각 인덱스에는 새로운 기능을 수행하도록 기능을 교체할 수 있어야 한다.
UNDO
버튼은 마지막으로 실행한 명령을 되돌리는 기능을 수행한다.
마지막으로 불을 키는 동작을 수행했다면,
UNDO
버튼은 불을 끔
자, 이제 커맨드 패턴을 사용하여 만능 리모콘을 구현해보자!
커맨드 패턴은 다음과 같이 구성된다.
execute()
의 구현을 강제하며 위의 예제에서는 되돌리는 기능도 존재하기 때문에 undo()
도 존재execute()
를 실제로 구현하는 클래스만능 리모콘 예제를 구현하면서 커맨드 패턴의 구성 요소들을 위에서부터 차례대로 살펴보자.
public interface Command {
void execute();
void undo();
}
기능이라는 단위를 추상화 시키면, Invoker
에서 어떠한 기능인지 몰라도 단순히 execute()
와 undo()
를 호출하여 느슨한 결합으로 이루어질 수 있다.
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
Concrete Command에서 Command
에서 정의된 execute()
와 undo()
를 구현했다.
LightOnCommand
의 경우 실행되면 불을 키므로 취소되면 불을 끄면 될 것이다.
어떠한 기능(작업)의 단위에선 실제로 작업을 수행하는 클래스(Receiver)가 필요한데, 여기서 Light
에 해당된다.
public class RemoteControl {
private static final int COMMAND_NUM = 3;
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControl() {
onCommands = new Command[COMMAND_NUM];
offCommands = new Command[COMMAND_NUM];
for (int i = 0; i < COMMAND_NUM; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
undoCommand = new NoCommand();
}
public void setCommand(int idx, Command onCommand, Command offCommand) {
onCommands[idx] = onCommand;
offCommands[idx] = offCommand;
}
public void onBtnPressed(int idx) {
Command onCommand = onCommands[idx];
onCommand.execute();
undoCommand = onCommand;
}
public void offBtnPressed(int idx) {
Command offCommand = offCommands[idx];
offCommand.execute();
undoCommand = offCommand;
}
public void undoBtnPressed() {
undoCommand.undo();
}
}
위의 예제의 리모콘(Invoker)를 구현한 클래스이다.
각 인덱스에 맞게 ON
커맨드, OFF
커맨드를 등록할 수 있고 각 버튼에 맞게 Command
의 execute()
를 호출하여 기능을 수행한다.
UNDO
버튼을 클릭할 경우를 대비하여 마지막 명령을 undoCommand
에 저장한다.
public class Light {
boolean isTurnedOn;
public Light() {
isTurnedOn = false;
}
public void on() {
isTurnedOn = true;
System.out.println("불을 켭니다.");
}
public void off() {
isTurnedOn = false;
System.out.println("불을 끕니다.");
}
public boolean isTurnedOn() {
return isTurnedOn;
}
}
실제 작업을 처리하는 Receiver다.
리모컨에 ON
, OFF
버튼밖에 없다고 해당 작업만 수행할 수 있는게 아니라, 만약 불을 반만 켤 수 있는 기능이 필요하다면 LightHalfOnCommand
라는 기능을 추가하여 Invoker에 등록 후 사용하면 된다.
만약 반만 켜는 기능을 추가하면, 현재는 isTurnedOn
으로 true or false이기 때문에 Enum으로 변경하여 관리하거나 int를 사용하면 될 것이다.
이제 커맨드 패턴의 구성에 대해 파악했으니, psvm을 살펴보자.
필자는 선풍기도 추가하여 OFF, LOW, HIGH로 바람의 세기를 조절할 수 있게 리모콘에 등록하였다.
public class Main {
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
Light light = new Light();
Fan fan = new Fan();
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
Command fanLowCommand = new FanLowCommand(fan);
Command fanHighCommand = new FanHighCommand(fan);
Command fanOffCommand = new FanOffCommand(fan);
remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
remoteControl.setCommand(1, fanLowCommand, fanOffCommand);
remoteControl.setCommand(2, fanHighCommand, fanOffCommand);
remoteControl.onBtnPressed(0);
remoteControl.undoBtnPressed();
remoteControl.onBtnPressed(1);
remoteControl.onBtnPressed(2);
remoteControl.undoBtnPressed();
remoteControl.offBtnPressed(1);
}
}
위에서 부터 설명하면,
remoteControl
이라는 Invoker를 생성했다.
실제 작업을 처리하는 Receiver에 해당되는 light
와 fan
을 생성했다.
Concrete Command들을 생성했다.
Invoker가 추상적으로 사용하여 결국 Receiver가 작업을 처리하므로, Concrete Command는 Invoker와 Receiver 사이를 연결하는 매개체라고 생각할 수 있다.
Invoker에 커맨드를 등록한다.
필자는 다음과 같이 커맨드를 등록했다.
0 : 불 켜기(
ON
), 불 끄기(OFF
)
1 : 선풍기 약한 바람(ON
), 선풍기 끄기(OFF
)
2 : 선풍기 강한 바람(ON
), 선풍기 끄기(OFF
)
이제 출력을 살펴보자.
지금까지 살펴본 것처럼 커맨드 패턴은 요청(커맨드)을 캡슐화하여 사용한다.
따라서 요청하는 객체와 요청을 수행하는 객체를 분리할 수 있다.
필요에 따라 요청을 큐에 저장하거나 로그로 기록하거나 구현했던 것 처럼 취소하는 기능을 사용할 수 있으므로 참고하자.
모든 소스코드는 여기에서 확인할 수 있다.