프로그래밍 일반/디자인 패턴

커맨드 패턴(Command Pattern)

지노윈 2020. 3. 4. 21:37
반응형

커맨드 패턴은 어떤 객체를 활용할 때 직접 그 객체의 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);

요약

인자를 전달하여 메서드를 호출하는 직접적인 방법으로 객체에 일을 시키는 대신, 작업 지시 내용을 감싸는 특별한 객체를 두어 객체와 커뮤니케이션하게 합니다.

 

 

 

[독서 리뷰] - 모던 C++ 디자인 패턴

 

모던 C++ 디자인 패턴

[객체 지향 프로그래밍/디자인 패턴] - 빌더 패턴(Builder Pattern) [분류 전체보기] - 디자인 패턴 [객체 지향 프로그래밍/디자인 패턴] - 팩토리 패턴(Factory pattern) [객체 지향 프로그래밍/디자인 패턴] -..

devjino.tistory.com

코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
 

Welcome to C++ Design Patterns’s documentation! — C++ Design Patterns 0.0.1 documentation

© Copyright 2018, Hans-J. Schmid Revision 786c83f8.

cpp-design-patterns.readthedocs.io