커맨드 패턴은 "명령", "실행" 이라는 관점에서 DIP 원칙과 전략 패턴을 적극 활용한 패턴이다.
예시
선언부
public class Lamp {
public void turnOn(){ System.out.println("Lamp On"); }
}
public class Button {
private Lamp theLamp;
public Button(Lamp theLamp) { this.theLamp = theLamp; }
public void pressed() { theLamp.turnOn(); }
}
사용부
public class Main {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Button lampButton = new Button(lamp);
lampButton.pressed();
}
}
Button 이 명령을 내리는 발행자로 명령을 받는 수행자 Lamp 와 강하게 결합이 되어 있다. 새로운 요구사항이 생겨 해당 Button 으로 다른 수행자의 명령을 수행하게 만든다면 어떻게 될까?
선언부
enum Mode { LAMP, ALARM };
public class Button {
private Lamp theLamp;
private Alarm theAlarm;
private Mode theMode;
public Button(Lamp theLamp, Alarm theAlarm) {
this.theLamp = theLamp;
this.theAlarm = theAlarm;
}
public void setMode(Mode mode) { this.theMode = mode; }
public void pressed() {
switch(theMode) {
case LAMP: theLamp.turnOn(); break;
case ALARM: theAlarm.start(); break;
}
}
}
Button 에 명령을 수행하는 수행자 Lamp 와 Alarm 이 생성자 결합으로 강하게 묶이게 되고 계속 요구사항이 늘어날 때마다 Button 클래스를 수정해야 한다. 이는 OCP 원칙에 위배된다.
DIP 원칙을 이용하여 발행자와 수행자 사이에 객체를 하나 두어 한 단계 추상화해주면 된다. (인터페이스로 연결하고 각각의 구현체로 실질적인 로직들을 구현)
선언부
// 인터페이스
public interface Command { public abstract void execute(); }
public class Button {
private Command theCommand;
public Button(Command theCommand) { setCommand(theCommand); }
public void setCommand(Command newCommand) { this.theCommand = newCommand; }
public void pressed() { theCommand.execute(); }
}
public class Lamp {
public void turnOn(){ System.out.println("Lamp On"); }
}
public class LampOnCommand implements Command {
private Lamp theLamp;
public LampOnCommand(Lamp theLamp) { this.theLamp = theLamp; }
public void execute() { theLamp.turnOn(); }
}
public class Alarm {
public void start(){ System.out.println("Alarming"); }
}
public class AlarmStartCommand implements Command {
private Alarm theAlarm;
public AlarmStartCommand(Alarm theAlarm) { this.theAlarm = theAlarm; }
public void execute() { theAlarm.start(); }
}
사용부
public class Main {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Command lampOnCommand = new LampOnCommand(lamp);
Alarm alarm = new Alarm();
Command alarmStartCommand = new AlarmStartCommand(alarm);
Button button1 = new Button(lampOnCommand);
button1.pressed();
Button button2 = new Button(alarmStartCommand);
button2.pressed();
button2.setCommand(lampOnCommand);
button2.pressed();
}
}
Button 에 한 단계 추상화 한 Command 를 부착할 수 있게 만들어 두고, 그 Command 의 메서드로 각각의 구현체가 동작하는 방식이다.
리팩토링 관점에서 보면...
사실 처음부터 설계하는 것보다는 기존의 코드를 수정할 일이 더 많다.
"명령" 과 "수행" 이라는 관점에서 커맨드 패턴을 떠올려 리팩토링할 수도 있지만, 조금 더 범용적인 관점에서 문제코드의 문제점을 해결할 수 있어야 커맨드 패턴을 처음 만든 프로그래머들의 취지를 이해할 수 있다고 생각한다.
커맨드 패턴을 알지 못 한 시점에서 해당 코드를 보고 무슨 문제점이 있는지 다시 살펴보자.
조건문(if 문이나 switch 문)에 각각 객체들이 포진되어 있고, 요구사항이 늘어날 때마다 결합되는 객체의 갯수와 조건문이 점점 커진다. 이 말은 고차원에서 저차원으로 가는데 저차원의 모듈 갯수가 현저히 많아 생긴 문제이다. 앞서 소개한 커맨드 패턴으로 예를 들면, 명령을 할 때 버튼을 눌러서 명령할 수도 있고, 손가락을 지시하여 명령할 수도 있다. 명령을 받는 주체는 그 명령을 받아 명령을 수행한다.
명령한다 => 버튼을 눌러서 명령한다 or 손가락을 지시하여 명령한다. => 램프를 킨다. or 알람을 설정한다. or 자동차 시동을 건다 등등
명령을 내리는 주체보다 명령을 받는 주체가 점점 많아지므로 DIP 원칙에서도 설명하였듯이 항상 저차원의 갯수가 많다. 따라서 고차원과 저차원의 모듈이 강하게 결합이 되는 것을 막으려면 한 단계를 추상화해 변수를 만들고 그 변수를 중간 매개체로 삼아 연결해야 한다. 그 매개체가 인터페이스이다. 이 인터페이스를 합성으로 고차원에 연결하면 앞서 소개한 커맨드 패턴이 되는 것이다.
결국 커맨드 패턴은 DIP 와 합성을 "명령" + "실행" 관점으로 바라본 디자인 패턴이다.
조건문이 계속 커지고 그 조건문 내에 객체의 갯수가 많아지면 인터페이스로 분리하여 합성하자.
'디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 전략 패턴 (Strategy Pattern) (0) | 2022.04.23 |
---|