커맨드 패턴은 어떤 객체를 활용할 때 직접 그 객체의 API를 호출하여 조작하는 대신, 작업을 어떻게 하라고 명령을 보내는 방식입니다.
데이트를 조작할때 그 데이터를 직접 조작하면 어떠한 변경 이력도 남지 않습니다.
때문에 데이터를 관리/감시 하거나 이력 기반으로 디버깅을 하거나 데이터를 되돌릴 필요가 있을 경우 커맨드 패턴이 유용하게 사용 될 수 있습니다.
객체로 명령을 만듭니다. 이를통해 명령을 대기, 로깅, 되돌릴 수 있는 기능을 지원 할 수 있습니다.
시나리오
입금, 출금 기능을 가진 은행의 마이너스 통장이 있습니다.
금융 감사를 위해 모든 입출금 내역을 기록해야 합니다.
그런데 이 입출금 기능은 이미 만들어졌으며 동작중이므로 수정할 수 없는 상황입니다.
struct BankAccount
{
int balance = 0;
int overdraft_limit = -500;
void deposit(int amount)
{
balance = amount;
cout << "deposited " << amount << ", balance now " <<
balance << "\n";
}
void withdraw(int amount)
{
if (balance - amount >= overdraft_limit)
{
balance -= amount;
cout << "withdrew " << amount << ", balance now " <<
balance << "\n";
}
}
};
커맨드 패턴의 구현
Command 인터페이스를 정희가고 BankAccountCommand에 의한 Command에 의해 입금 또는 출금 작업을 실행합니다.
struct Command
{
virtual void call() const = 0;
};
struct BankAccountCommand : Command
{
BankAccount& account;
enum Action { deposit, withdraw } action;
int amount;
BankAccountCommand(BankAccount& account,
const Action action, const int amount)
: account(account), action(action), amount(amount) {}
void call() const override
{
switch (action)
{
case deposit:
account.deposit(amount);
break;
case withdraw:
account.withdraw(amount);
break;
default: break;
}
}
};
커맨드를 만들고 커맨드가 지정하고 있는 계좌에 명령을 수행합니다.
BankAccount ba;
BankAccountCommand cmd{ ba, BankAccountCommand::deposit, 100 };
cmd.call();
되돌리기(Undo) 작업
call과 undo를 함께 구현하지 않고 ISP(인터페이스 부분리 원칙)에 따르는 것이 바람직하지만 여기서는 편의상 이렇게 구현합니다.
struct Command
{
virtual void call() const = 0;
virtual void undo() const = 0;
};
BankAccountCommand::undo() 구현합니다.
struct BankAccountCommand : Command
{
...
void undo() const override
{
switch (action)
{
case withdraw:
account.deposit(amount);
break;
case deposit:
account.withdraw(amount);
break;
default: break;
}
}
};
명령을 call()후 undo()를 수행합니다.
BankAccount ba;
BankAccountCommand cmd{ ba, BankAccountCommand::deposit, 100 };
cmd.call();
cmd.undo();
컴포지트 커맨드
컴포지트 커맨드는 "컴포지트 패턴"에서 지향하는 바와 동일합니다.
명령을 그룹으로 관리하여 반복적으로 명령을 수행하게 합니다.
struct CompositeBankAccountCommand
: vector<BankAccountCommand>, Command
{
CompositeBankAccountCommand(const initializer_list<value_type>& items)
: vector<BankAccountCommand>(items) {}
void call() const override
{
for (auto& cmd : *this)
cmd.call();
}
void undo() const override
{
//for (auto& cmd : *this)
//cmd.undo();
for(auto it = rbegin(); it != rend(); it) // 역순으로 undo
it->undo();
}
};
BankAccount ba;
CompositeBankAccountCommand commands{
BankAccountCommand{ba, BankAccountCommand::deposit, 100},
BankAccountCommand{ba, BankAccountCommand::withdraw, 200}
};
commands.call();
commands.undo();
명령과 조회의 분리
- 명령 : 시스템의 상태 변화가 일어나는 작업 지시, 결과값 생성 없음
- 조회 : 결과값을 생성하는 정보 요청, 그 요청을 처리하는 시스템의 상태 변화를 일으키지 않는 것
크리처의 strength, agility 속성있을때 직접적으로 get/set 메서드 호출하는 대신 아래처럼 단일한 커맨드 인터페이스를 제공할 수 있습니다.
class Creature
{
private:
int strength, agility;
public:
Creature(int strength, int agility)
: strength{ strength }, agility{ agility } {}
void process_command(const CreatureCommand& cc);
int process_query(const Creature& q) const;
};
Creature가 제공해야 하는 속성과 기능이 아무리 늘어나더라도 이 API 두 개만으로 처리됩니다.
커맨드만으로 Creature와의 상호작용을 수행할 수 있습니다.
enum class CreatureAbility { strength, agility };
struct CreatureCommand
{
enum Action { set, increaseBy, decreaseby } action;
CreatureAbility ability;
int amount;
};
struct CreatureQuery
{
CreatureAbility ability;
};
Creature를 완전히 구현해 보았습니다.
class Creature
{
private:
int strength, agility;
public:
Creature(int strength, int agility)
: strength{ strength }, agility{ agility } {}
void process_command(const CreatureCommand& cc)
{
int* ability = nullptr;
switch (cc.ability)
{
case CreatureAbility::strength:
ability = &strength;
break;
case CreatureAbility::agility:
ability = &agility;
break;
}
switch (cc.action)
{
case CreatureCommand::set:
*ability = cc.amount;
break;
case CreatureCommand::increaseBy:
*ability = cc.amount;
break;
case CreatureCommand::decreaseby:
*ability -= cc.amount;
break;
}
}
int process_query(const CreatureQuery& q) const
{
switch (q.ability)
{
case CreatureAbility::strength: return strength;
case CreatureAbility::agility: return agility;
}
return 0;
}
};
process_command로 명령을 수행하여 상태를 변경하고 process_query로 변경된 상태의 결과를 조회합니다.
Creature creature{ 100, 50 };
CreatureCommand command{ CreatureCommand::set, CreatureAbility::strength, 80 };
CreatureQuery query{ CreatureAbility::strength };
creature.process_command(command);
int strength = creature.process_query(query);
요약
인자를 전달하여 메서드를 호출하는 직접적인 방법으로 객체에 일을 시키는 대신, 작업 지시 내용을 감싸는 특별한 객체를 두어 객체와 커뮤니케이션하게 합니다.
코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글
Null 객체 (0) | 2020.03.04 |
---|---|
메멘토 패턴(Memento Pattern) (0) | 2020.03.04 |
중재자 패턴(Mediator Pattern) (0) | 2020.03.04 |
반복자 패턴(Iterator Pattern) (0) | 2020.03.04 |
책임 사슬(Chain of Responsibility Pattern) (0) | 2020.03.04 |