메소드 호출을 캡슐화 하여 계산의 각 과정부분들을 결정화 시킬 수 있다. 캡슐화된 method 호출을 로그 기록용으로 저장하거나 취소 기능을 구현하고 재사용 할 수 있다. 이 기능을 구현하면서 각 도메인의 핵심적인 로직을 몰라도 되는 변경에 닫혀있고, 확장에 열려있는 코드를 작성해보자.
이 컨텐츠는 Head First의 Design Pattern 책의 6번 챕터인 커맨트 패턴을 정리한 내용입니다
코드를 모두 입력해두면 컨텐츠 길이가 의미없이 길어질것 같아 Github 에 코드를 올려두었습니다
요구사항
프로그래밍이 가능한 리모콘에 7개의 소켓이 있고, 각 소켓에 필요한 프로그램을 연결한다
각 프로그램을 통해 특정 기능을 on/off 할 수 있다.
이렇게 다양한 클래스가 있는데 어떻게 하나의 리모콘에 연결할 수 있을까?
구현 방법
1.
리모콘은 제작 업체가 제공한 클래스를 몰라야한다
2.
작업을 요청한쪽과 그 작업을 처리한 쪽을 분리시킨다.
•
특정 작업 요청을 캡슐화 하자
3.
사용자가 버튼을 눌렀을 때 작업을 처리하여 리모콘은 자세한 내용을 모르게 한다.
식당에서 발생하는 event 로 이해하는 command pattern
고객 → 웨이트리스에게 주문 → 주문서 작성 → 주방장에게 주문 전달
•
웨이트리스 입장
◦
어떤 주문인지 조차도 몰라도 된다.
◦
누가 식사를 준비할지 몰라도 된다.
•
주방장 입장
◦
주문서를 읽고 필요한 메뉴만 준비하면 된다.
◦
누가 주문했는지 알 필요가 없다
Search
Command 객체
command 객체는 오로지 execute() 메소드만 제공한다.
public interface Command {
void execute();
}
Java
복사
public class LightCommand implements Command {
private Light light;
public LightCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
Java
복사
public class Light {
public void on() {
System.out.println("turn on light");
}
public void off() {
System.out.println("turn off light");
}
}
Java
복사
Command 객체 사용하기
// Invoker 역할을 수행한다.
public class SimpleRemoteControl {
private Command slot;
public void setSlot(Command command) {
this.slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
Java
복사
Command 패턴의 정의
•
커맨드 객체는 일련의 행동을 특정 리시버와 연결시켜 요구사항을 캡슐화 한다
•
행동과 리시버를 한 객체에 집어넣고, execute() 메소드 하나만 외부에 공해한다.
•
리시버에서 execute() 만 가지고 일련의 작업을 수행한다.
•
외부에선 누가 리시버이며, 그 내부에서 무엇을 하는지 알 수 없다.
•
client 는 ConcreteCommand 를 생성하며 Receiver 를 설정한다
•
Receiver 는 요구사항 수행을 위해 어떤 일을 처리해야 하는지 알고있으며, 요청을 처리한다
•
ConcretCommand 는 action과 receiver 를 연결하며 Invoker 에서 execute() 호출을 통해 요청을 하면 리시버에 있는 메소드를 호출하면서 필요한 업무를 수행한다.
•
Invoker 는 execute() 를 호출함으로써 커맨드 객체가 어떤 작업을 수행해야할지 요구한다
•
Command 는 모든 커맨드 객체가 구현해야 한다.
◦
execute() 를 사용해 receiver 에게 특정 작업을 수행할것을 요구한다
다시 리모콘으로 돌아가자 7개의 소켓이 각각 무엇인지 어떤 명령을 수행해야하는지 어떻게 알아야 할까?
슬롯에 명령 할당하기
각 슬롯에 알맞는 데이터를 추가해보자
RemoteLoader
public class NoCommand implements Command {
@Override
public void execute() {
System.out.println("no command!");
}
}
Java
복사
이와 같은 NoCommand 를 사용하지 않는다면 onButtonWasPushed 는 매번 null 체크를 해야한다.
작업 취소기능 (Undo)
public interface Command {
public void execute();
public void undo();
}
Java
복사
undo() 를 Command Interface 에 추가한다 (하나하나 수정해야 함 ㅠㅠ)
@RequiredArgsConstructor
public class StereoOffWithCdCommand implements Command {
private final Stereo stereo;
@Override
public void execute() {
stereo.off();
}
@Override
public void undo() {
stereo.on();
stereo.setCd();
stereo.setVolume(11);
}
}
Java
복사
이와같이 execute 에 딱 반대되는 행동을 수행한다. 그렇다면 Invoker 는 다음과 같다.
/**
* Invoker with Undo
*/
public class RemoteControlWithUndo extends RemoteControl {
private Command undoCommand;
public RemoteControlWithUndo() {
super();
undoCommand = new NoCommand();
}
@Override
public void onButtonWasPushed(int slot) {
super.onButtonWasPushed(slot);
undoCommand = onCommands[slot];
}
@Override
public void offButtonWasPushed(int slot) {
super.offButtonWasPushed(slot);
undoCommand = offCommands[slot];
}
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
Java
복사
Macro Command 사용 방법
매크로 커맨드란? Command 객체에서 execute() 를 실행할 때 n 개의 command 를 한꺼번에 실행한다.
@RequiredArgsConstructor
public class MacroCommand implements Command {
// final 로 사용한 이유는 생성자 사용을 강제하기 위함이다.
private final Command[] commands;
@Override
public void execute() {
Stream.of(commands)
.forEach(Command::execute);
}
@Override
public void undo() {
Stream.of(commands)
.forEach(Command::undo);
}
}
Java
복사
•
MacroCommand 를 사용하면 기존에 사용했던 RemoteControl 은 그대로 재사용이 가능하다
•
하지만, n 개의 command 를 주입받아 사용할 수 있다.
public static void main(String[] args) {
MacroControl macroControl = new MacroControl();
Light livingRoom = new Light("livingRoom");
Light kitchenLight = new Light("kitchen");
CeilingFan ceilingFan = new CeilingFan("Living Room");
Stereo stereo = new Stereo("Living Room");
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoom);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoom);
LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
CeilingFanOn ceilingFanOn = new CeilingFanOn(ceilingFan);
CeilingFanOff ceilingFanOff = new CeilingFanOff(ceilingFan);
StereoOnWithCdCommand stereoOnWithCdCommand = new StereoOnWithCdCommand(stereo);
StereoOffWithCdCommand stereoOffWithCdCommand = new StereoOffWithCdCommand(stereo);
Command[] partyOn = { livingRoomLightOn, kitchenLightOn, ceilingFanOn, stereoOnWithCdCommand};
Command[] partyOff = { livingRoomLightOff, kitchenLightOff, ceilingFanOff, stereoOffWithCdCommand};
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
macroControl.setCommand(0, partyOnMacro, partyOffMacro);
}
Java
복사
이와 같이 command[] 배열로 받아 MacroCommand 를 생성하고 이것을 Invoker 에 제공하면 여러 command 를 마치 매크로 처럼 사용할 수 있다.
커맨드 패턴의 활용: 요청을 큐에 저장하자
커맨드 패턴을 구현한 객체들을 큐에 저장한다
큐 한쪽 끝은 command 를 추가하고, 반대쪽 끝에서는 execute() 를 실행하기 위한 Thread 들이 대기하고 있는 것이다.
큐를 이와같이 준비하면 command 객체와 그 객체 안에 있는 execute 만 고민하면 되기 때문에 큐에 어떠한 작업이 와도 상관 없다.
금융 작업을 하다가 네트워크 작업을 해도 되고, 다운로드 작업을 수행해도 된다
Summary
•
command pattern 을 이용하면 요청하는 객체와 그 요청을 수행하는 객체를 분리할 수 있다.
•
분리 시키는 과정의 중심에 Command 객체가 존재한다 (식당 예제의 주문서)
◦
이 Command 는 Receiver 를 캡슐화 한다.
•
Invoker 는 Command 를 통해서 execute() 함수를 호출한다.
•
execute() 커맨드는 Command 를 확장하여 작업 취소 기능을 구현할 수 있다.