커맨드 패턴이란? |
명령 패턴이라고도 불림
이 패턴은 언제 사용하는지를 먼저 알아야 이해가 빠르다.
주로 어떤 함수들이 사용되었는지 이력을 남기고 싶을 때 사용한다고 보면 된다.
즉 스택에 함수들이 사용될때마다 해당 함수가 사용되었다고 push하면 되는데
이렇게 사용하려면 함수포인터를 사용해야한다.
이걸 함수의 개념에서 객체지향의 개념으로 발전시켜서 함수호출을 객체화한 것이고
해당 명령객체를 stack에 저장하는 방식으로 발전시켰다고 이해하면 된다.
언제 사용하는가
1) 데이터의 조작/변경 이력을 남기고 싶을 때
데이터를 조작/변경할때 그냥 객체의 함수를 호출하는 방식이라면 어떠한 이력도 남지 않음
그러므로 디버깅을 하거나 롤백을 하고 싶어서 데이터의 조작/변경 이력을 남기고 싶다면 커맨드 패턴을 사용해서 내려진 명령들을 역으로 추적해나갈 수 있음
2) 함수를 매개변수화하기 위한 콜백의 대안
어려운 설명
명령을 클래스로 만듬
기능을 함수로 사용하는게 아닌 객체로 만들어서 사용해서 요청 자체를 캡슐화하는 패턴
명령을 받을 객체를 매개변수로 만들어 사용해 하나의 함수로 여러 객체에 명령을 내릴 수 있고
요청을 모아서 한번에 실행하거나 로깅하고 되돌릴수 있는 연산을 만들 수 있음
쉬운 설명
리모컨 버튼 하나하나를 클래스로 만드는 것
리모컨으로 조명을 켜고 끌때 Light 클래스가 있고 on()함수와 off()함수가 있다면
light->on()이나 light->off()처럼 직접 객체의 함수를 호출하는 것이 아니라
LightOnCommand클래스와 LightOffCommand 클래스를 만들고
해당 클래스들이 Light 객체를 멤버변수로 갖고 excute()함수와 undo()함수를 작성한다.
LightOnCommand클래스의 excute()함수는 light->on()함수를 작성한다.
만약 되돌리고 싶다면 리모컨에 있는 stack에서 pop하면서 명령 이력을 보고 그 반대행동을 해주면 됨
만약 명령들을 모아뒀다가 한번에 실행시키고 싶다면 vector같은곳에 Command객체들을 쌓아두다가 한번에 for문으로 순회하면서 excute()하는 방법도 가능하다.
커맨드 패턴의 구조 |
1) Command
- CommandInterface
- excute()함수와 undo()함수의 interface를 정의
2) ConcreteCommand
- Command 인터페이스를 실제로 구현한 실제로 실행될 작업
3) Invoker
- 클라이언트로부터 커맨드 객체를 받아서 실행하는 클래스
- 클라이언트와 커맨드를 분리하는 역할
- 아래 예시코드에서의 리모컨에 해당
4) Receiver
- 실제로 커맨드에 의해 작업을 수행하는 객체
5) Client
- Receiver객체를 생성하고 ConcreteCommand를 선택하고 ConcreteCommand에 Receiver객체를 매개변수로 넘김
그 후 Invoker에 명령을 설정하고, 버튼을 누르면 excute()함수가 실행되어 Receiver가 해당 Command를 수행하는 순서로 진행됨
구현 예시 |
#include <bits/stdc++.h>
using namespace std;
/* Light 클래스 */
class Light
{
public:
void on() { cout << "Light is On" << endl; }
void off() { cout << "Light is off" << endl; }
};
/* Television 클래스 */
class Television
{
public:
void channelUp() { cout << "Channel Up" << endl; }
void channelDown() { cout << "Channel Down" << endl; }
};
/* Command 인터페이스 */
class Command
{
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
/* Light Command 클래스 */
class LightOnCommand : public Command
{
private:
Light* light;
public:
LightOnCommand(Light* l) : light(l) { }
void execute() override { light->on(); }
void undo() override { light->off(); }
};
class LightOffCommand : public Command
{
private:
Light* light;
public:
LightOffCommand(Light* l) : light(l) { }
void execute() override { light->off(); }
void undo() override { light->on(); }
};
/* Television Command 클래스 */
class TelevisionChannelUp : public Command
{
private:
Television* television;
public:
TelevisionChannelUp(Television* tv) : television(tv) { }
void execute() override { television->channelUp(); }
void undo() override { television->channelDown(); }
};
class TelevisionChannelDown : public Command
{
public:
TelevisionChannelDown(Television* tv) : television(tv) { }
void execute() override { television->channelDown(); }
void undo() override { television->channelUp(); }
private:
Television* television;
};
/* 리모컨 클래스 */
class RemoteControl
{
private:
Command* button;
/*
이력을 남기고 싶다면
여기에 stack<Command*>를 만들어서 저장하고
되돌리고 싶다면 pressUndoButton()의 내용에 pop해서 해당 내용에 맞는 undo를 실행해주면 됨
*/
public:
RemoteControl() = default;
void setCommand(Command* command) { button = command; }
void pressButton() { button->execute(); }
void pressUndoButton() { button->undo(); }
/*
stack에서 pop()한 값을 undo의 매개변수로 넘기면
각 ConcreteCommand클래스들에 undo를 오버로딩해서
pop()된 명령에 따른 동작을 수행할 수 있음
*/
};
int main()
{
RemoteControl* remote = new RemoteControl(); // Invoker
Light* light = new Light(); // Receiver
LightOnCommand* lightOn = new LightOnCommand(light); // ConcreteCommand
remote->setCommand(lightOn);
remote->pressButton();
Television* television = new Television();
TelevisionChannelUp* tvChannelUp = new TelevisionChannelUp(television);
remote->setCommand(tvChannelUp);
remote->pressButton();
return 0;
}
장단점 |
1) 장점
- 단일책임원칙 : 요청을 호출하는 객체와 실제 작업을 처리하는 객체를 분리
- 개방폐쇄원칙 : 새로운 명령(새로운 리모컨 버튼)이 추가되더라도 ConcreteCommand클래스만 새로 추가하면 된다.
- 작업 큐를 이용해 요청을 모아뒀다가 한번에 실행할 수 있다.
- 작업 이력을 스택에 담아 작업 취소기능을 만들 수 있다.
2) 단점
- 각각의 명령마다 ConcreteCommand 클래스가 필요하므로 클래스 수가 너무 많아진다.
- 발송자와 수신자 사이에 커맨드 객체가 한번 끼어드는것이므로 코드가 더 복잡해질 수 있다.
※ 참고 문헌