Command Pattern
Overview
Invoker - Command - Receiver 관계로 호출자와 실행자의 관계를 디커플링하는 디자인패턴.
Why to use
Invoker (요청자)와 Receiver(실행자)의 타이트한 의존관계를 분리하여 변화에 유연한 구조를 만든다.
서로 의존관계가 타이트할 때 Receiver 코드가 변경되면, Invoker 코드도 변경이 불가피한 문제가 발생한다.
커맨드 패턴 도입 후 Receiver 코드 변경시 오로지 Command 만 영향받는다.
When to use
Invoker - Receiver 디커플링이 필요할 때
Invoker - Receiver Java Example
- Light
- Game
@Getter
public class Light {
private boolean isOn = false;
public void on() {
if (this.isOn) {
System.out.println("이미 불이 켜져있습니다.");
} else {
this.isOn = true;
}
}
public void off() {
if (!this.isOn) {
System.out.println("이미 불이 꺼져있습니다.");
} else {
this.isOn = false;
}
}
}
@Getter
public class Game {
private boolean isStarted = false;
public void start() {
if (isStarted) {
System.out.println("이미 게임이 실행중입니다.");
return;
}
System.out.println("게임 시작");
this.isStarted = true;
}
public void finish() {
if (!isStarted) {
System.out.println("게임이 실행중이지 않습니다.");
}
System.out.println("게임 종료");
this.isStarted = false;
}
}
Button.java
@Getter
@RequiredArgsConstructor
public class Button {
private final Light light;
// toggle 버튼이라 상태전환 무조건 일어남.
public void press() {
if (this.light.isOn()) this.light.off();
else this.light.on();
}
}
나의 앱이 버튼(Invoker)을 통해 Game 혹은 Light 을 키고 끈다고 가정해보저.
여기서 Game, Light 가 Receiver 가 된다.
그럼 Receiver 가 바뀔 때마다 내 앱 클래스가 계속 변경되어야한다.
- LightApp
- GameApp
LightApp 은 Light (Receiver) 에 의존적이다.
@Getter
@RequiredArgsConstructor
public class LightApp {
private final Light light;
public void press() {
this.light.off();
}
}
GameApp 은 Game (Receiver) 에 의존적이다.
@Getter
@RequiredArgsConstructor
public class GameApp {
private final Game game;
public void press() {
this.game.finish();
}
}
GameApp 과 LightApp 은 사실상 거의 같은 코드다.
Invoker(GameApp, LightApp) 와 Receiver(Game, Light) 의 커플링 관계로 인해 거의 같은 코드가 반복되는 Invoker 클래스를 만들거나, 기존 Invoker 코드를 수정해야한다.
📝 이럴 때 커맨드 패턴으로 Invoker, Receiver 간 의존 관계를 분리할 수 있다.
How to use
Command 인터페이스와 구현체를 아래와 같은 구조로 정의한다.
Receiver 코드가 변경되면 Invoker 는 영향을 받지 않고 오로지 Command 만 변경하면 된다.
Diagram
Command.java
public interface Command {
// 오로지 실행만
void execute();
}
- LightCommandOn
- GameCommand
@RequiredArgsConstructor
public class LightOnCommand implements Command {
private final Light light;
@Override
public void execute() {
this.light.on();
}
}
@RequiredArgsConstructor
public class GameStartCommand implements Command {
// Command 객체에서 굳이 game 객체에 접근해서 어떤 상태를 조작할 필요는 없다.
// Command 는 오로지 '동작'이라는 책임만 갖는다.
// 따라서 Getter 는 필요하지 않다.
private final Game game;
@Override
public void execute() {
this.game.start();
}
}
ButtonTest.java
class ButtonTest {
@DisplayName("On Off 버튼 커맨드와 함께 생성")
@Test
void lightButtonOnOffTest() {
Light currentRoomLight = new Light();
// 동일한 불빛을 전달받아 커맨드 생상자 주입
Button onButton = new Button(new LightOnCommand(currentRoomLight));
Button offButton = new Button(new LightOffCommand(currentRoomLight));
onButton.press();
assertThat(currentRoomLight.isOn()).isTrue();
offButton.press();
assertThat(currentRoomLight.isOn()).isFalse();
}
@DisplayName("게임 시작 - 종료 버튼 테스트")
@Test
void gameButtonTest() {
Game game = new Game();
Button gameStartButton = new Button(new GameStartCommand(game));
Button gameFinishButton = new Button(new GameFinishCommand(game));
gameStartButton.press();
assertThat(game.isStarted()).isTrue();
gameFinishButton.press();
assertThat(game.isStarted()).isFalse();
}
}
Pros and Cons
장점
Single Responsibility Principle (SRP) 준수.
- 하나의 커맨드가 오로지 하나의 책임만 갖는다.
- Receiver 코드 변경시 Invoker 코드가 영향받지 않는다.
단점
- 코드 복잡도 증가