"한 차원 더 높은 단계의 캡슐화 => 메소드 호출 캡슐화!"
메서드 호출을 캡슐화하면 계산 과정의 각 부분들을 결정화시킬 수 있기에, 계산하는 코드를 호출한 객체에서는 어떤 식으로 일을 처리해야 하는지에 대해 전혀 신경쓰지 않아도 된다. 나아가 그냥 결정화된 메서드를 호출해서 필요한 일만 잘할 수 있는 것 뿐 아니라 캡슐화된 메서드 호출을 로그 기록용으로 저장을 한다거나 취소(undo) 기능을 구현하기 위해 재사용하는 것과 같은 신기한 작업을 할 수 있다.
식당에서 일어나는 일들을 간단하게 단계화 시키면 다음과 같다.
1) 고객(client)가 주문서를 통해 웨이터에게 주문을 한다.
2) 웨이터는 주문을 받아 카운터에 갖다 주고 "주문 받아요!"라고 얘기한다.
3) 주방장은 주문대로 음식을 조리한다.
이 단계화를 객체화 시키면 아래 그림과 같은 클래스 다이어그램을 표현할 수 있다.
그리고 시퀀스 다이어그램과 유사하게 다음과 같이 표현할 수 있다.
주문서는 주문한 메뉴를 요구하는 역할.
이 객체의 인터페이스에는 orderUp()이라는 캡슐화된 메서드가 있다. 이게 유일한 메서드이다. 그리고 식사를 만드는 객체(주방장)에 대한 레퍼런스도 들어있다. 이런 내용은 캡슐화 되어 있기에 웨이트리스는 어떤 내용이 주문되었는지, 누가 식사를 준비할 지 등을 전혀 몰라도 된다. 그냥 주문서를 적당한 곳에 전달하고 알리기만 하면 된다.
웨이트리스는 손님한테 주문을 받고, orderUp() 메서드를 호출하여 식사를 준비시키기만 하면 된다. 주문서에 무슨 내용이 있는지, 누가 식사를 만드는 지에 대해 걱정할 필요가 없다.
웨이트리스의 takeOrder() 메서드에는 여러 고객이 여러 주문서를 매개변수로 전달한다. 모든 주문서 객체에는 orderUp() 메서드가 있고, 그 메서드만 호출하면 식사가 주문된다.
실제로 식사를 준비하는 방법(조리)은 주방장만 알고 있다. 웨이트리스가 orderUp() 메서드를 호출하면 주방장이 그 주문을 받아서 음식을 만들기 위한 메서드를 전부 처리한다. 여기서 주방장과 웨이트리스가 완전히 분리되어 있음을 알 수 있다. 즉 웨이트리스와 주방장 간의 커뮤니케이션이 필요 없다. => 주문서 덕에 분리
정리하자면 객체마을 식당은 어떤 것을 요구하는 객체와 그 요구를 받아들이고 처리하는 객체를 분리시키는 객체지향 디자인 패턴의 한 모델이라고 볼 수 있다. 예들 들어, 다양한 가전기기와 연결되어 제어할 수 있는 다중 버튼 리모콘 API를 생각해보면, 리모컨 버튼이 눌렀을 때 호출되는 코드와 특정 업체에서 제공한, 실제로 일을 처리하는 코드를 분리시킬 수 있는 패턴이다.
(1) 클라이언트에서 커멘드 객체 생성
(2) setCommand()를 호출하여 인보커에 커멘드 객체를 저장
(3) 이 후 클라이언트에서 인보커에 해당 명령을 실행시켜 달라는 요청을 함
1. 커멘드 객체는 모두 같은 인터페이스를 구현해야 한다. 이 인터페이스는 "execute()" (== orderUp())라는 하나의 메서드 밖에 없다.
public interface Command {
public void execute();
}
2. 전등을 켜기 위한 커멘드 클래스 구현
아래 객체는 커멘드 객체로 제어할 특정 전등(== 주방장 객체)에 대한 정보이다. 커멘드 객체에서 execute() 메서드가 호출되면 아래 객체가 그 요청에 대한 리시버가 된다.
// 리시버 역할
public class Light {
public void on(){
System.out.println("Light On");
}
public void off(){
System.out.println("Light Off");
}
}
전자제품 공급 업체에서 제공한 Light 클래스를 예로 들면, on(), off()라는 두 개의 메서드가 있다. 커멘드 객체를 만들 때(createCommandObject() 호출 후)는 다음과 같이 하면 된다.
public class LightOnCommand implements Command{
// execute() 메서드가 호출되면 light 객체가 바로 그 요청에 대한 리시버(reciever)가 된다.
Light light;
// 생성자에서 이 커멘드 객체로 제어할 특정 전등(ex, 거실 전등)에 대한 정보가 전달됨.
public LightOnCommand(Light light){
this.light = light;
}
// execute() 메서드는 리시버 객체에 있는 on() 메서드를 호출
@Override
public void execute() {
// 결국 execute() 메서드에서는 리시버 객체(light 객체)에 있는 on() 메서드를 호출한다.
light.on();
}
}
3. 커멘드 객체 사용하기
인보커에 해당하는 리모컨 객체
// 인보커(Invoker)
public class SimpleRemoteControl {
// 커멘드를 집어넣을 슬롯은 현재 하나밖에 없다.
// 이걸 가지고 장비를 제어할 수 있다.
Command slot;
public SimpleRemoteControl(){}
// 슬롯을 가지고 제어할 명령을 설정하기 위한 메서드.
// 이 코드를 사용하면 클라이언트에서 리모컨 버튼의 기능을 바꾸고 싶다면,
// 이 메서드를 이용해 얼마든지 바꿀 수 있다.
public void setCommand(Command command){
slot = command;
}
// 버튼이 눌려지면 이 메서드가 호출됨.
// 커맨더 객체의 execute()의 wrapper
public void buttonWasPressed(){
slot.execute();
}
}
4. 리모컨을 사용하기 위한 클라이언트 코드
public class Client {
public static void main(String[] args) {
// 인보커
SimpleRemoteControl simpleRemoteControl = new SimpleRemoteControl();
// 리시버
Light light = new Light();
// 조면 on 커멘드 객체
LightOnCommand lightOnCommand = new LightOnCommand(light); // 리시버 전달
// 리시버2
GarageDoor garageDoor = new GarageDoor();
// 차고 문 open 커멘드 객체
GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
// 슬롯에 커멘드 객체 전달
simpleRemoteControl.setCommand(lightOnCommand);
simpleRemoteControl.buttonWasPressed(); // => execute 호출
// 슬롯에 커멘드 객체 전달
simpleRemoteControl.setCommand(garageDoorOpenCommand);
simpleRemoteControl.buttonWasPressed();
}
}
(+a) GarageDoor
// 리시버
public class GarageDoor {
public void up(){
System.out.println("Garage Door Open");
}
public void down(){}
public void stop(){}
public void lightOn(){}
public void lightOff(){}
}
// 커멘드
public class GarageDoorOpenCommand implements Command{
GarageDoor garageDoor;
public GarageDoorOpenCommand(GarageDoor garageDoor){
this.garageDoor = garageDoor;
}
@Override
public void execute() {
garageDoor.up();
}
}