커맨드 패턴을 좀 더 이해하기 위해 식당에서 주문하는 과정을 코드에 빗대어 살펴봅니다.
- 고객이 주문을 합니다: CreateOrder() - Order객체 생성
- 홀직원이 주문을 받습니다: TakeOrder()
- 주방에 주문을 전달합니다: OrderUp()
- 음식을 만듭니다: MakeBurger()
Order(주문서)를 메뉴 정보가 들어있는 객체로 생각해볼때, 이 객체에는 식사 준비를 위해 OrderUp()이라는 단 하나의 메소드만 필요합니다. 주문을 받는 홀직원은 사실상 Order에 무엇이 들어있는지, 누가 요리할지 몰라도 상관없습니다. 단지 TakeOrder(), OrderUp()을 호출하는 역할만 할 뿐입니다.
주문서 내에 들어있는 것을 가지고 요리하는 방법은 주방장이 알고 있습니다. 주방장은 주방에서 음식을 만들기 위한 메소드를 처리할 뿐입니다. 그리고 사실 홀직원과 주방장은 Order를 주고받을 뿐 다른 얘기를 할 필요가 없습니다.
이 과정을 잘 살펴보면 Order라는 객체 내에 수행할 목록이 들어 있기 때문에 홀직원과 주방장이 분리되어 있습니다. 커맨드 객체가 존재하여 다른 객체들을 분리시키는 것입니다.
대강은 이해가 되는 것 같습니다. 이제 좀 더 복잡한 예로 넘어가 봅니다.
홍 오토메이션 장비에 대한 개발을 의뢰받았습니다. 그 장치는 하나의 리모콘인데 조명, 팬, 욕조, 오디오 등을 제어하는 장치입니다. 리모콘에는 7개의 슬롯과 각 슬롯에 해당하는 on, off 스위치가 각각 붙어 있습니다. 슬롯은 각기 전자제품에 연결되어 제어할 수 있고, 가장 아래쪽에 마지막으로 누른 버튼에 대한 명령을 취소할 수 있는 "UNDO" 버튼이 있습니다.
장치를 개발한 쪽에서는 각 가전제품에 대한 제어용 클래스 코드도 동봉해왔습니다.
그 클래스들을 살펴보면 다음과 같습니다.
보시다시피 숫자도 많고, 공통의 인터페이스도 보이지 않는데다가 향후 제어할 목록이 더 늘어날 수도 있습니다. 이것을 리모콘과 연결시키기 위해 "1번 슬롯에 Light가 연결되어 있으면 light.On(), 욕조가 연결되어 있으면 hottub.JetsOn()..." 이런 식으로 만들었다간 일이 끝이 없습니다.
이런 경우에 앞서 식당의 예에서 살펴본 커맨드 패턴을 적용합니다. 리모콘의 버튼에 대해 커맨드 객체를 지정해두면 리모콘은 구체적으로 어떤 기계를 어떻게 조작할지에 대해서는 몰라도 됩니다. 마치 리모콘 버튼에게서 주문을 받아 기계 클래스에 전달해 주는 것과 마찬가지죠.
갑자기 Client, Invoker...이 등장했습니다. 영어라 와닿지 않는듯 하지만 뜻을 살펴보면 좀 더 명확해집니다.
Client : 고객(식당-고객)
Invoker : 호출자(식당-웨이트리스)
Command : 명령(식당-주문서), Excute()는 식당의 OrderUp()
Receiver : 수신자(식당-주방장), Action()은 식당의 MakeBurger()
Client는 CreateCommandObject() 메소드로 Command 객체를 만듭니다. 이 커맨드 객체에는 Execute()라는 메소드 하나 뿐이며, 행동과 리시버에 대한 정보가 들어갑니다. 좀 더 구체적으로는
public void Execute
{
receiver.Action();
receiver.Action2();
}
와 같은 형태입니다.
이렇게 생성된 커맨드 객체는 인보커 객체의 SetCommand() 메소드로서 인보커에 넘겨지고, 인보커가 커맨드 객체의 Execute()를 호출하면 리시버에 있는 특정 메소드가 호출되는 방식입니다.
Client : 고객(식당-고객)
Invoker : 호출자(식당-웨이트리스)
Command : 명령(식당-주문서), Excute()는 식당의 OrderUp()
Receiver : 수신자(식당-주방장), Action()은 식당의 MakeBurger()
Client는 CreateCommandObject() 메소드로 Command 객체를 만듭니다. 이 커맨드 객체에는 Execute()라는 메소드 하나 뿐이며, 행동과 리시버에 대한 정보가 들어갑니다. 좀 더 구체적으로는
public void Execute
{
receiver.Action();
receiver.Action2();
}
와 같은 형태입니다.
이렇게 생성된 커맨드 객체는 인보커 객체의 SetCommand() 메소드로서 인보커에 넘겨지고, 인보커가 커맨드 객체의 Execute()를 호출하면 리시버에 있는 특정 메소드가 호출되는 방식입니다.
첫 번째 커맨드 객체
아직 구체적으로 어떻게 해야할지 잘 모르지만 가장 기본이 되는 Command 인터페이스부터 구현하도록 합니다.
public interface Command
{
void Execute();
}
| cs |
다음으로 우선 전등을 켜기 위한 커맨드 클래스를 작성하려 합니다. 먼저 전자제품 업체에서 제공된 Light class를 살펴봅시다.
public class Light
{
public Light()
{
}
public void On()
{
Console.WriteLine("Light is on");
}
public void Off()
{
Console.WriteLine("Light is off");
}
}
| cs |
On(), Off()의 메소드가 있습니다. 이 중 On()에 대한 클래스를 작성해 봅니다.
public class LightOnCommand : Command
{
Light light;
public LightOnCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.On();
}
}
| cs |
테스트를 위해 슬롯이 하나뿐인 리모콘을 가정하고 코드를 만들어 봅니다.
public class SimpleRemoteControl
{
Command slot;
public SimpleRemoteControl()
{
}
public void SetCommand(Command command)
{
slot = command;
}
public void ButtonPressed()
{
slot.Execute();
}
}
| cs |
그리고 테스트를 작성합니다.
static void Main(string[] args)
{
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
remote.SetCommand(lightOn);
remote.ButtonPressed();
}
| cs |
결과)
Light is on
계속하려면 아무 키나 누르십시오 . . .
그렇다면 GarageDoorOpenCommand 클래스는 어떻게 만들면 되겠습니까? GarageDoor 클래스는 오른쪽과 같으므로,
public class GarageDoorOpenCommand : Command
{
GarageDoor garage;
public GarageDoorOpenCommand(GarageDoor garage)
{
this.garage = garage;
}
public void Execute()
{
garage.Up();
}
}
| cs |
테스트를 약간 수정해서 출력해 보면...
static void Main(string[] args)
{
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
GarageDoor garageDoor = new GarageDoor();
LightOnCommand lightOn = new LightOnCommand(light);
GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor);
remote.SetCommand(lightOn);
remote.ButtonPressed();
remote.SetCommand(garageOpen);
remote.ButtonPressed();
}
| cs |
결과)
Light is on
Garage Door is Open
계속하려면 아무 키나 누르십시오 . . .
커맨드 패턴의 정의
- 커맨드 패턴 - 요구사항을 객체로 캡슐화 가능하고, 매개변수를 사용하여 다른 요구 사항을 집어넣을 수 있는 패턴. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있고, 작업취소 기능도 지원 가능함
커맨드 객체는 일련의 행동을 리시버와 연결하여 요구 사항을 캡슐화한 것입니다. 그것을 위해 행동과 리시버를 한 객체에 집어 넣고, 외부에는 Execute()라는 메소드 하나만 공개합니다. 외부에서 볼 때는 객체가 리시버 역할인지, 또 실제로 어떤 역할을 하는지 알 수 없습니다. 다만 Execute() 메소드를 호출하면 요구사항이 처리된다는 점만 알 수 있죠.
리모콘
리모콘은 커맨드 패턴에서 인보커 역할입니다. 식당의 서빙직원처럼 주문을 받아 리시버인 각 전자장치에 전달해주는 역할을 합니다. 앞에서 슬롯이 하나인 리모콘을 이미 구현해 봤습니다. 이제 슬롯이 일곱 개에 버튼이 슬롯당 각 두 개씩 14개와, 추가로 UNDO 버튼이 달린 문제의 리모콘으로 가 보도록 합니다.
그림이 없으면 이해가 어려워서 너무나 허접하여 부끄러움에도 불구하고 그림판으로 그려봤습니다.
각 슬롯에 연결할 리시버들이 할당되고, 그 옆에 그것을 끄고 켤 버튼이 있습니다. 버튼이 눌려서 Execute()메소드가 실행되면 연결된 작업이 수행될 겁니다.
이것을 바탕으로 코드를 작성해 보겠습니다.
public class RemoteControl
{
Command[] onCommands;
Command[] offCommands;
public RemoteControl()
{
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++)
{
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void SetCommand(int slot, Command onCommand, Command offCommand)
{
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void OnButtonWasPushed(int slot)
{
onCommands[slot].Execute();
}
public void OffButtonPushed(int slot)
{
offCommands[slot].Execute();
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("\n------ Remote Control -----\n");
for (int i = 0; i < onCommands.Length; i++)
{
stringBuilder.Append("[slot " + i + "] " +
onCommands[i].GetType().Name + " " +
offCommands[i].GetType().Name + "\n");
}
return stringBuilder.ToString();
}
}
| cs |
생성자에서 커맨드의 배열을 만들고 초기화 했습니다. 그리고 NoCommand라는 클래스를 일단 할당했습니다. 특정 슬롯이 null인지 확인하고 실행하는 번거로움을 피하기 위해 아무것도 하지 않는 커맨드 클래스를 구현하여 일단 모든 슬롯에 집어넣습니다. 그러면 굳이 null인지 확인할 필요가 없습니다. 일종의 널 객체(null object)로서 유용하게 쓰이는 방법입니다.
public class NoCommand : Command
{
public void Execute() { }
}
| cs |
다시 리모콘 코드로 돌아가서 SetCommand로 슬롯에 커맨드를 설정하고, 버튼이 눌렸을 때 실행하가 위한 메소드를 작성해 주었습니다.
그리고 ToString()을 오버라이드 해서 슬롯별 명령출력에 쓰도록 하였습니다.
커맨드 클래스
Light 클래스를 다시 한번 살펴봅시다.
public class Light
{
string Location { get; set; }
public Light(string location)
{
this.Location = location;
}
public void On()
{
Console.WriteLine(Location + " Light is on");
}
public void Off()
{
Console.WriteLine(Location + " Light is off");
}
}
이 클래스를 참고하여 이미 앞에서 작성한 LightOnCommand 클래스처럼 LightOffCommand 클래스를 작성해봅니다.
public class LightOffCommand : Command
{
Light light;
public LightOffCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.Off();
}
}
| cs |
LightOnCommand와 거의 동일합니다.
그 중 좀 복잡한 편인 오디오 커맨드를 만들어 보겠습니다. 끄는 건 그냥 Off()와 연결해 주면 되니까 간단하고, 오디오를 켤 때 자동으로 CD가 재생되도록 StereoOnWithCDCommand 클래스를 작성해 보겠습니다.
public class StereoOnWithCDCOmmand : Command
{
Stereo stereo;
public StereoOnWithCDCOmmand(Stereo stereo)
{
this.stereo = stereo;
}
public void Execute()
{
stereo.On();
stereo.SetCD();
stereo.SetVolume(11);
}
}
| cs |
오디오를 켜고, CD를 장착하고, 볼륨을 임의로 11로 설정합니다.
나머지 장치에 대한 클래스도 다 작성했다는 가정하에 테스트를 해 보겠습니다.
리모컨 테스트
static void Main(string[] args)
{
RemoteControl remoteControl = new RemoteControl();
Light livingRoomLight = new Light("Living Room");
Light kitchenLight = new Light("Kitchen");
CeilingFan ceilingFan = new CeilingFan("Living Room");
GarageDoor garageDoor = new GarageDoor("");
Stereo stereo = new Stereo("Living Room");
// 전등용 커맨드
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
// 선풍기 커맨드
CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);
// 차고 커맨드
GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);
// 오디오 커맨드
StereoOnWithCDCOmmand stereoOnWithCD = new StereoOnWithCDCOmmand(stereo);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
// 리모콘 슬롯에 커맨드 로드
remoteControl.SetCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteControl.SetCommand(1, kitchenLightOn, kitchenLightOff);
remoteControl.SetCommand(2, ceilingFanOn, ceilingFanOff);
remoteControl.SetCommand(3, stereoOnWithCD, stereoOff);
// ToString()메소드로 리모콘 슬롯 정보 출력
Console.WriteLine(remoteControl);
// 슬롯 작동
remoteControl.OnButtonWasPushed(0);
remoteControl.OffButtonPushed(0);
remoteControl.OnButtonWasPushed(1);
remoteControl.OffButtonPushed(1);
remoteControl.OnButtonWasPushed(2);
remoteControl.OffButtonPushed(2);
remoteControl.OnButtonWasPushed(3);
remoteControl.OffButtonPushed(3);
}
| cs |
결과)
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] LightOnCommand LightOffCommand
[slot 2] CeilingFanOnCommand CeilingFanOffCommand
[slot 3] StereoOnWithCDCOmmand StereoOffCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room Light is on
Living Room Light is off
Kitchen Light is on
Kitchen Light is off
Living Room ceiling fan is on high
Living Room ceiling fan is off
Living Room stereo is on
Living Room stereo is set for CD input
Living Room Stereo volume set to 11
Living Room stereo is off
계속하려면 아무 키나 누르십시오 . . .
Undo
아직 Undo 기능을 구현하지 않았습니다. 버튼을 누를 시 마지막 수행한 작업이 취소되도록 하는 버튼입니다. 커맨드에서 지원하기 위해 인터페이스에 Undo()를 추가합니다.
public interface Command
{
void Execute();
void Undo();
}
그리고 커맨드 중 일단 LightOnCommand()를 수정해 봅니다.
그렇습니다. 커맨드에 반대되는 메소드를 하는 행위만 추가해주면 되는 겁니다.
같은 이치로 LightOffCommand()에서는
public void Undo()
{
light.On();
}
만 추가해 주면 되겠지요.
바람의 세기를 High(), Medium(), Low()와 같은 메소드를 사용해서 조절하고 있습니다.
GetSpeed()라는 코드로 현재 속도를 알도록 되어있죠. 지금까지 우리가 선풍기 커맨드에서 사용한 코드는 High, Medium, Low 중 무엇이던 상관없이 On 버튼으로 취급한 셈입니다.
그렇다면 이제는 CeilingFanOnCommand를 없애고, 선풍기에 있는 High, Medium, Low, Off 메소드 각각을 커맨드로 만들어야 합니다.
그 중 우선 CeilingFanHighCommand부터 구현합니다.
이런 식으로 Low, Medium, Off에 대해서도 커맨드를 만들면 되고, 다른 커맨드에 대해서도 유사한 방법으로 수정합니다.
다 수정했다는 가정 하에, RemoteControl 클래스를 손봅니다. 마지막으로 실행되었던 커맨드가 무엇이었는지 기록해두는 변수가 필요합니다.
내부에 undoCommand라는 변수를 두어 변동시 기록해두도록 했습니다.
이제 테스트를 해 보겠습니다.
결과)
Living Room Light is on
Living Room Light is off
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room Light is on
Living Room Light is off
Living Room Light is on
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room Light is off
Living Room ceiling fan is on high
Living Room ceiling fan is off
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room ceiling fan is off
Living Room ceiling fan is on high
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room ceiling fan is off
계속하려면 아무 키나 누르십시오 . . .
{
void Execute();
void Undo();
}
그리고 커맨드 중 일단 LightOnCommand()를 수정해 봅니다.
public class LightOnCommand : Command
{
Light light;
public LightOnCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.On();
}
public void Undo()
{
light.Off();
}
}
| cs |
그렇습니다. 커맨드에 반대되는 메소드를 하는 행위만 추가해주면 되는 겁니다.
같은 이치로 LightOffCommand()에서는
public void Undo()
{
light.On();
}
만 추가해 주면 되겠지요.
선풍기 Undo
Light를 끄고 켜는건 간단했는데, 선풍기 같은 경우는 좀 복잡합니다. On/Off 두 상태가 아니라 바람의 세기 조절과 같은 것들이 있기 때문입니다.
선풍기 제조업체에서 제공한 클래스 코드를 좀 보겠습니다.
public class CeilingFan
{
string location = "";
int level;
public static readonly int HIGH = 3;
public static readonly int MEDIUM = 2;
public static readonly int LOW = 1;
public static readonly int OFF = 0;
public CeilingFan(string location)
{
this.location = location;
level = OFF;
}
public void High()
{
// turns the ceiling fan on to high
level = HIGH;
Console.WriteLine(location + " ceiling fan is on high");
}
public void Medium()
{
// turns the ceiling fan on to medium
level = MEDIUM;
Console.WriteLine(location + " ceiling fan is on medium");
}
public void Low()
{
// turns the ceiling fan on to low
level = LOW;
Console.WriteLine(location + " ceiling fan is on low");
}
public void Off()
{
// turns the ceiling fan off
level = OFF;
Console.WriteLine(location + " ceiling fan is off");
}
public int GetSpeed()
{
return level;
}
}
| cs |
GetSpeed()라는 코드로 현재 속도를 알도록 되어있죠. 지금까지 우리가 선풍기 커맨드에서 사용한 코드는 High, Medium, Low 중 무엇이던 상관없이 On 버튼으로 취급한 셈입니다.
그렇다면 이제는 CeilingFanOnCommand를 없애고, 선풍기에 있는 High, Medium, Low, Off 메소드 각각을 커맨드로 만들어야 합니다.
그 중 우선 CeilingFanHighCommand부터 구현합니다.
public class CeilingFanHighCommand : Command
{
CeilingFan ceilingFan;
int prevSpeed; //선풍기 기존 상태를 알기위한 지역변수
public CeilingFanHighCommand(CeilingFan ceilingFan)
{
this.ceilingFan = ceilingFan;
}
public void Execute()
{
prevSpeed = ceilingFan.GetSpeed(); //기존 상태 저장
ceilingFan.High();
}
public void Undo()
{
if (prevSpeed == CeilingFan.HIGH)
{
ceilingFan.High();
}
else if (prevSpeed == CeilingFan.MEDIUM)
{
ceilingFan.Medium();
}
else if (prevSpeed == CeilingFan.LOW)
{
ceilingFan.Low();
}
else if (prevSpeed == CeilingFan.OFF)
{
ceilingFan.Off();
}
}
}
| cs |
이런 식으로 Low, Medium, Off에 대해서도 커맨드를 만들면 되고, 다른 커맨드에 대해서도 유사한 방법으로 수정합니다.
다 수정했다는 가정 하에, RemoteControl 클래스를 손봅니다. 마지막으로 실행되었던 커맨드가 무엇이었는지 기록해두는 변수가 필요합니다.
public class RemoteControlWithUndo
{
Command[] onCommands;
Command[] offCommands;
Command undoCommand; //최종 커맨드 저장용
public RemoteControlWithUndo()
{
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++)
{
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand; //일단 noCommand로 초기화
}
public void SetCommand(int slot, Command onCommand, Command offCommand)
{
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void OnButtonWasPushed(int slot)
{
onCommands[slot].Execute();
undoCommand = onCommands[slot];
}
public void OffButtonPushed(int slot)
{
offCommands[slot].Execute();
undoCommand = offCommands[slot];
}
public void UndoButtonWasPushed()
{
undoCommand.Undo();
}
public override string ToString()
{
...
}
}
| cs |
내부에 undoCommand라는 변수를 두어 변동시 기록해두도록 했습니다.
이제 테스트를 해 보겠습니다.
static void Main(string[] args)
{
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
Light livingRoomLight = new Light("Living Room");
CeilingFan ceilingFan = new CeilingFan("Living Room");
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan);
CeilingFanLowCommand ceilingFanLow = new CeilingFanLowCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);
remoteControl.SetCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteControl.SetCommand(1, ceilingFanHigh, ceilingFanOff);
remoteControl.SetCommand(2, ceilingFanLow, ceilingFanOff);
remoteControl.OnButtonWasPushed(0);
remoteControl.OffButtonPushed(0);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OffButtonPushed(0);
remoteControl.OnButtonWasPushed(0);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OnButtonWasPushed(1);
remoteControl.OffButtonPushed(1);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OnButtonWasPushed(2);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
}
| cs |
결과)
Living Room Light is on
Living Room Light is off
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room Light is on
Living Room Light is off
Living Room Light is on
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room Light is off
Living Room ceiling fan is on high
Living Room ceiling fan is off
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room ceiling fan is off
Living Room ceiling fan is on high
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] NoCommand NoCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
Living Room ceiling fan is off
계속하려면 아무 키나 누르십시오 . . .
매크로
Undo까지 무사히 따라왔습니다. 이제 매크로 기능을 구현할 차례입니다.
리모콘 버튼에 파티모드를 추가하려고 합니다. 하나의 버튼을 누르면 전등이 어두워지고, 오디오와 TV가 켜지고, DVD모드로 변경되고, 욕조에 물이 채워지는 것까지 한번에 처리하려는 생각입니다. 파티에 왜 욕조가 필요한지는 모르겠지만....
MacroCommand라는 클래스를 먼저 작성합니다.
public class MacroCommand : Command
{
Command[] commands;
//커맨드 배열을 받아서 내부에 저장
public MacroCommand(Command[] commands)
{
this.commands = commands;
}
// 각 커맨드를 순서대로 실행
public void Execute()
{
for (int i = 0; i < commands.Length; i++)
{
commands[i].Execute();
}
}
// Undo실행시는 나중에 한것부터 Undo
public void Undo()
{
for (int i = commands.Length-1; i >= 0; i--)
{
commands[i].Undo();
}
}
}
| cs |
생성자에서 커맨드 배열을 받아 내부에서 순차적으로 처리해줍니다.
이 커맨드를 사용할 때는 이렇게 합니다.
먼저, 커맨드에 들어갈 커맨드들을 생성합니다.
// 매크로 테스트용 전기제품 생성
Light light = new Light("Living Roim");
TV tv = new TV("Living Romm");
Stereo stereo = new Stereo("Living Room");
Hottub hottub = new Hottub();
// 매크로 On 커맨드 생성
LightOnCommand lightOn = new LightOnCommand(light);
StereoOnCommand stereoOn = new StereoOnCommand(stereo);
TVOnCommand tvOn = new TVOnCommand(tv);
HottubOnCommand hottubOn = new HottubOnCommand(hottub);
//매크로 Off 커맨드 생성
LightOffCommand lightOff = new LightOffCommand(light);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
TVOffCommand tvOff = new TVOffCommand(tv);
HottubOffCommand hottubOff = new HottubOffCommand(hottub);
다음으로, On,Off 커맨드를 넣을 베열을 만듭니다.
Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn };
Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff };
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
그리고, 매크로 객체를 리모콘 버튼에 할당합니다.
remoteControl.SetCommand(3, partyOnMacro, partyOffMacro);
잘 작동하는지 테스트 해봅시다.
Console.WriteLine(remoteControl);
Console.WriteLine("------Pushing Macro On------");
remoteControl.OnButtonWasPushed(3);
Console.WriteLine("------Pushing Macro Off------");
remoteControl.OffButtonPushed(3);
결과)
------ Remote Control -----
[slot 0] LightOnCommand LightOffCommand
[slot 1] CeilingFanHighCommand CeilingFanOffCommand
[slot 2] CeilingFanLowCommand CeilingFanOffCommand
[slot 3] MacroCommand MacroCommand
[slot 4] NoCommand NoCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
------Pushing Macro On------
Living Roim Light is on
Living Room stereo is on
Living Romm TV is on
Living Romm TV channel is set for DVD
Hottub is heating to a steaming 104 degrees
Hottub is bubbling!
------Pushing Macro Off------
Living Roim Light is off
Living Room stereo is off
Living Romm TV is off
Hottub is cooling to 98 degrees
계속하려면 아무 키나 누르십시오 . . .
저는 책과 달리 그냥 앞 테스트에 이어 3번 버튼에 할당하여 작성하였기 때문에 결과가 조금 다릅니다.
마지막으로 지금까지 쓰인 코드를 전부 올리면서 커맨드 패턴을 바칠까 합니다.
클래스별로 별도 파일에 들어갈 것을 편의상 전부 한 파일에 넣었기 때문에 코드가 쓸데없이 길어진 감이 있습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
class Program
{
// 제공된 코드
// Light 클래스
public class Light
{
string Location { get; set; }
//int Level { get; set; }
public Light(string location)
{
this.Location = location;
}
public void On()
{
//Level = 100;
Console.WriteLine(Location + " Light is on");
}
public void Off()
{
//Level = 0;
Console.WriteLine(Location + " Light is off");
}
/*
public void Dim(int level)
{
this.Level = level;
if (level == 0)
{
Off();
}
else
{
Console.WriteLine("Light is dimmed to " + level + "%");
}
} */
}
// 차고 클래스
public class GarageDoor
{
string location;
public GarageDoor(string location)
{
this.location = location;
}
public void Up()
{
Console.WriteLine("Garage Door is Open");
}
public void Down()
{
Console.WriteLine("Garage Door is Closed");
}
public void Stop()
{
Console.WriteLine("Garage Door is Stopped");
}
public void LightOn()
{
Console.WriteLine("Garage light is on");
}
public void LightOff()
{
Console.WriteLine("Garage light is off");
}
}
// 오디오 클래스
public class Stereo
{
string location;
public Stereo(string location)
{
this.location = location;
}
public void On()
{
Console.WriteLine(location + " stereo is on");
}
public void Off()
{
Console.WriteLine(location + " stereo is off");
}
public void SetCD()
{
Console.WriteLine(location + " stereo is set for CD input");
}
public void SetDVD()
{
Console.WriteLine(location + " stereo is set for DVD input");
}
public void SetRadio()
{
Console.WriteLine(location + " stereo is set for Radio");
}
public void SetVolume(int volume)
{
// code to set the volume
// valid range: 1-11 (after all 11 is better than 10, right?)
Console.WriteLine(location + " Stereo volume set to " + volume);
}
}
// 천장 선풍기
public class CeilingFan
{
string location = "";
int level;
public static readonly int HIGH = 3;
public static readonly int MEDIUM = 2;
public static readonly int LOW = 1;
public static readonly int OFF = 0;
public CeilingFan(string location)
{
this.location = location;
level = OFF;
}
public void High()
{
// turns the ceiling fan on to high
level = HIGH;
Console.WriteLine(location + " ceiling fan is on high");
}
public void Medium()
{
// turns the ceiling fan on to medium
level = MEDIUM;
Console.WriteLine(location + " ceiling fan is on medium");
}
public void Low()
{
// turns the ceiling fan on to low
level = LOW;
Console.WriteLine(location + " ceiling fan is on low");
}
public void Off()
{
// turns the ceiling fan off
level = OFF;
Console.WriteLine(location + " ceiling fan is off");
}
public int GetSpeed()
{
return level;
}
}
// TV 클래스
public class TV
{
string location;
int channel;
public TV(string location)
{
this.location = location;
}
public void On()
{
Console.WriteLine(location + " TV is on");
}
public void Off()
{
Console.WriteLine(location + " TV is off");
}
public void SetInputChannel()
{
this.channel = 3;
Console.WriteLine(location + " TV channel is set for DVD");
}
}
// 욕조 클래스
public class Hottub
{
bool on;
int temperature;
public Hottub()
{
}
public void On()
{
on = true;
}
public void Off()
{
on = false;
}
public void Circulate()
{
if (on)
{
Console.WriteLine("Hottub is bubbling!");
}
}
public void JetsOn()
{
if (on)
{
Console.WriteLine("Hottub jets are on");
}
}
public void JetsOff()
{
if (on)
{
Console.WriteLine("Hottub jets are off");
}
}
public void SetTemperature(int temperature)
{
if (temperature > this.temperature)
{
Console.WriteLine("Hottub is heating to a steaming " + temperature + " degrees");
}
else
{
Console.WriteLine("Hottub is cooling to " + temperature + " degrees");
}
this.temperature = temperature;
}
}
// Command 인터페이스
public interface Command
{
void Execute();
void Undo();
}
// NoCommand용 클래스
public class NoCommand : Command
{
public void Execute() { }
public void Undo() { }
}
//전등용 Command
public class LightOnCommand : Command
{
Light light;
public LightOnCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.On();
}
public void Undo()
{
light.Off();
}
}
public class LightOffCommand : Command
{
Light light;
public LightOffCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.Off();
}
public void Undo()
{
light.On();
}
}
// 차고 Command
public class GarageDoorUpCommand : Command
{
GarageDoor garage;
public GarageDoorUpCommand(GarageDoor garage)
{
this.garage = garage;
}
public void Execute()
{
garage.Up();
}
public void Undo()
{
garage.Down();
}
}
public class GarageDoorDownCommand :Command
{
GarageDoor garage;
public GarageDoorDownCommand(GarageDoor garage)
{
this.garage = garage;
}
public void Execute()
{
garage.Down();
}
public void Undo()
{
garage.Up();
}
}
// 오디오 Command
public class StereoOnCommand : Command
{
Stereo stereo;
public StereoOnCommand(Stereo stereo)
{
this.stereo = stereo;
}
public void Execute()
{
stereo.On();
}
public void Undo()
{
stereo.Off();
}
}
public class StereoOnWithCDCOmmand : Command
{
Stereo stereo;
public StereoOnWithCDCOmmand(Stereo stereo)
{
this.stereo = stereo;
}
public void Execute()
{
stereo.On();
stereo.SetCD();
stereo.SetVolume(11);
}
public void Undo()
{
stereo.Off();
}
}
public class StereoOffCommand : Command
{
Stereo stereo;
public StereoOffCommand(Stereo stereo)
{
this.stereo = stereo;
}
public void Execute()
{
stereo.Off();
}
public void Undo()
{
stereo.On();
}
}
// 천장선풍기 커맨드
public class CeilingFanHighCommand : Command
{
CeilingFan ceilingFan;
int prevSpeed; //선풍기 기존 상태를 알기위한 지역변수
public CeilingFanHighCommand(CeilingFan ceilingFan)
{
this.ceilingFan = ceilingFan;
}
public void Execute()
{
prevSpeed = ceilingFan.GetSpeed(); //기존 상태 저장
ceilingFan.High();
}
public void Undo()
{
if (prevSpeed == CeilingFan.HIGH)
{
ceilingFan.High();
}
else if (prevSpeed == CeilingFan.MEDIUM)
{
ceilingFan.Medium();
}
else if (prevSpeed == CeilingFan.LOW)
{
ceilingFan.Low();
}
else if (prevSpeed == CeilingFan.OFF)
{
ceilingFan.Off();
}
}
}
public class CeilingFanMediumCommand : Command
{
CeilingFan ceilingFan;
int prevSpeed; //선풍기 기존 상태를 알기위한 지역변수
public CeilingFanMediumCommand(CeilingFan ceilingFan)
{
this.ceilingFan = ceilingFan;
}
public void Execute()
{
prevSpeed = ceilingFan.GetSpeed(); //기존 상태 저장
ceilingFan.High();
}
public void Undo()
{
if (prevSpeed == CeilingFan.HIGH)
{
ceilingFan.High();
}
else if (prevSpeed == CeilingFan.MEDIUM)
{
ceilingFan.Medium();
}
else if (prevSpeed == CeilingFan.LOW)
{
ceilingFan.Low();
}
else if (prevSpeed == CeilingFan.OFF)
{
ceilingFan.Off();
}
}
}
public class CeilingFanLowCommand : Command
{
CeilingFan ceilingFan;
int prevSpeed; //선풍기 기존 상태를 알기위한 지역변수
public CeilingFanLowCommand(CeilingFan ceilingFan)
{
this.ceilingFan = ceilingFan;
}
public void Execute()
{
prevSpeed = ceilingFan.GetSpeed(); //기존 상태 저장
ceilingFan.High();
}
public void Undo()
{
if (prevSpeed == CeilingFan.HIGH)
{
ceilingFan.High();
}
else if (prevSpeed == CeilingFan.MEDIUM)
{
ceilingFan.Medium();
}
else if (prevSpeed == CeilingFan.LOW)
{
ceilingFan.Low();
}
else if (prevSpeed == CeilingFan.OFF)
{
ceilingFan.Off();
}
}
}
public class CeilingFanOffCommand : Command
{
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanOffCommand(CeilingFan ceilingFan)
{
this.ceilingFan = ceilingFan;
}
public void Execute()
{
ceilingFan.Off();
}
public void Undo()
{
if (prevSpeed == CeilingFan.HIGH)
{
ceilingFan.High();
}
else if (prevSpeed == CeilingFan.MEDIUM)
{
ceilingFan.Medium();
}
else if (prevSpeed == CeilingFan.LOW)
{
ceilingFan.Low();
}
else if (prevSpeed == CeilingFan.OFF)
{
ceilingFan.Off();
}
}
}
// TV 커맨드
public class TVOnCommand : Command
{
TV tv;
public TVOnCommand(TV tv)
{
this.tv = tv;
}
public void Execute()
{
tv.On();
tv.SetInputChannel();
}
public void Undo()
{
tv.Off();
}
}
public class TVOffCommand : Command
{
TV tv;
public TVOffCommand(TV tv)
{
this.tv = tv;
}
public void Execute()
{
tv.Off();
}
public void Undo()
{
tv.On();
}
}
// 욕조 커맨드
public class HottubOnCommand : Command
{
Hottub hottub;
public HottubOnCommand(Hottub hottub)
{
this.hottub = hottub;
}
public void Execute()
{
hottub.On();
hottub.SetTemperature(104);
hottub.Circulate();
}
public void Undo()
{
hottub.Off();
}
}
public class HottubOffCommand : Command
{
Hottub hottub;
public HottubOffCommand(Hottub hottub)
{
this.hottub = hottub;
}
public void Execute()
{
hottub.SetTemperature(98);
hottub.Off();
}
public void Undo()
{
hottub.On();
}
}
// 파티용 매크로 커맨드
public class MacroCommand : Command
{
Command[] commands;
//커맨드 배열을 받아서 내부에 저장
public MacroCommand(Command[] commands)
{
this.commands = commands;
}
// 각 커맨드를 순서대로 실행
public void Execute()
{
for (int i = 0; i < commands.Length; i++)
{
commands[i].Execute();
}
}
// Undo실행시는 나중에 한것부터 Undo
public void Undo()
{
for (int i = commands.Length-1; i >= 0; i--)
{
commands[i].Undo();
}
}
}
// 버튼 하나 뿐인 리모콘
public class SimpleRemoteControl
{
Command slot;
public SimpleRemoteControl()
{
}
public void SetCommand(Command command)
{
slot = command;
}
public void ButtonPressed()
{
slot.Execute();
}
}
// 정식 리모콘
public class RemoteControlWithUndo
{
Command[] onCommands;
Command[] offCommands;
Command undoCommand; //최종 커맨드 저장용
public RemoteControlWithUndo()
{
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++)
{
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand; //일단 noCommand로 초기화
}
public void SetCommand(int slot, Command onCommand, Command offCommand)
{
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void OnButtonWasPushed(int slot)
{
onCommands[slot].Execute();
undoCommand = onCommands[slot];
}
public void OffButtonPushed(int slot)
{
offCommands[slot].Execute();
undoCommand = offCommands[slot];
}
public void UndoButtonWasPushed()
{
undoCommand.Undo();
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("\n------ Remote Control -----\n");
for (int i = 0; i < onCommands.Length; i++)
{
stringBuilder.Append("[slot " + i + "] " +
onCommands[i].GetType().Name + " " +
offCommands[i].GetType().Name + "\n");
}
return stringBuilder.ToString();
}
}
static void Main(string[] args)
{
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
Light livingRoomLight = new Light("Living Room");
CeilingFan ceilingFan = new CeilingFan("Living Room");
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan);
CeilingFanLowCommand ceilingFanLow = new CeilingFanLowCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);
remoteControl.SetCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteControl.SetCommand(1, ceilingFanHigh, ceilingFanOff);
remoteControl.SetCommand(2, ceilingFanLow, ceilingFanOff);
remoteControl.OnButtonWasPushed(0);
remoteControl.OffButtonPushed(0);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OffButtonPushed(0);
remoteControl.OnButtonWasPushed(0);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OnButtonWasPushed(1);
remoteControl.OffButtonPushed(1);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
remoteControl.OnButtonWasPushed(2);
Console.WriteLine(remoteControl);
remoteControl.UndoButtonWasPushed();
//매크로
Console.WriteLine("매크로 테스트");
// 매크로 테스트용 전기제품 생성
Light light = new Light("Living Roim");
TV tv = new TV("Living Romm");
Stereo stereo = new Stereo("Living Room");
Hottub hottub = new Hottub();
// 매크로 On 커맨드 생성
LightOnCommand lightOn = new LightOnCommand(light);
StereoOnCommand stereoOn = new StereoOnCommand(stereo);
TVOnCommand tvOn = new TVOnCommand(tv);
HottubOnCommand hottubOn = new HottubOnCommand(hottub);
//매크로 Off 커맨드 생성
LightOffCommand lightOff = new LightOffCommand(light);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
TVOffCommand tvOff = new TVOffCommand(tv);
HottubOffCommand hottubOff = new HottubOffCommand(hottub);
// 커맨드 배열 생성
Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn };
Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff };
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
// 버튼에 할당
remoteControl.SetCommand(3, partyOnMacro, partyOffMacro);
Console.WriteLine(remoteControl);
Console.WriteLine("------Pushing Macro On------");
remoteControl.OnButtonWasPushed(3);
Console.WriteLine("------Pushing Macro Off------");
remoteControl.OffButtonPushed(3);
}
}
}
| cs |
댓글 없음:
댓글 쓰기