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

책임 사슬(Chain of Responsibility Pattern)

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

회사에서 어떠한 큰 문제가 발생한다면 해당 임직원 뿐만 아니라 그 조직의 조직장 더 나아가 CEO까지 책임을 져야 한다. 이것이  책임 사슬의 예입니다.

포인터 사슬

 

크리처가 이름, 공격력, 방어력을 가지고 있습니다. 게임 플레이 도중 공격력과 방어력이 어떤한 이벤트에 의해 변경이 됩니다.

 

이를 위해 CreatureModifier를 호출합니다. 이벤트들이 여러번 발생하여 CreatureModifier가 여러번 호출 될 수 있도록 변경 작업을 크리처 별로 쌓아 순서대로 변경 될 수 있도록 합니다.

즉, 연결된 고리의 어느 한 객체가 실제 상황에 적합하다고 판단되면 자신에게 정의된 서비스를 제공합니다.

 

게임에 크리처들이 있고 이름, 공격력, 방어력 속성을 가지고 있습니다.

struct Creature
{
  string name;
  int attack, defense;
 
  Creature(const string& name, const int attack, const int defense)
    : name(name),
      attack(attack),
      defense(defense)
  {
  }
 
  friend ostream& operator<<(ostream& os, const Creature& obj)
  {
    return os
      << "name: " << obj.name
      << " attack: " << obj.attack
      << " defense: " << obj.defense;
  }
};

게임이 진행 되는 동안 공격력과 방어력은 변경될 수 있습니다. 이를 위해 CreatureModifier를 호출합니다.

class CreatureModifier
{
  CreatureModifier* next{ nullptr };    // 포인터 사슬, 현재 변경 작업 이후에 뒤따르는 다른 CreatureModifier를 가르킵니다.
protected:
  Creature& creature;
public:
  explicit CreatureModifier(Creature& creature)
    : creature(creature) {}
 
  void add(CreatureModifier* cm)        // 책임 사슬을 연결합니다.
  {
    if (next) next->add(cm);
    else next = cm;
  }
 
  virtual void handle()             // 다음 항목을 처리합니다.
  {
    if (next) next->handle();            // 핵심적인 부분!
  }
};

이러한 구현은 연결 리스트에 항목을 추가하는 것이며 특별할 것이 없습니다.

그렇지만 이 클래스를 상속받아 실질적인 작업들이 추가 되면 이 구현의 의미가 더욱 명확해집니다.

 

크리처의 공격력을 두 배로 키우는 변경 작업의 예는 다음과 같습니다.

class DoubleAttackModifier : public CreatureModifier
{
public:
  explicit DoubleAttackModifier(Creature& creature)
    : CreatureModifier(creature) {}
 
  void handle() override        // override 하였으며 2배 공격력과 부모의 handle을 호출 하였습니다.
  {
    creature.attack *= 2;
    CreatureModifier::handle();     // 꼭 부모의 handle을 호출해야합니다.
  }
};

다음은 공격력이 2이하인 크리처의 방어력을 1 증가 시키는 변경 작업을 수행합니다.

class IncreaseDefenseModifier : public CreatureModifier
{
public:
  explicit IncreaseDefenseModifier(Creature& creature)
    : CreatureModifier(creature) {}
 
  void handle() override
  {
    if (creature.attack <= 2)
      creature.defense  = 1;
    CreatureModifier::handle();
  }
};

지금까지의 구현을 사용하는 코드는 다음과 같습니다.

Creature goblin{ "Goblin", 1, 1 };
CreatureModifier root{ goblin };
DoubleAttackModifier r1{ goblin };
DoubleAttackModifier r1_2{ goblin };
IncreaseDefenseModifier r2{ goblin };
 
root.add(&r1);
root.add(&r1_2);
root.add(&r2);
 
root.handle();
 
cout << goblin << endl;
// 출력 결과: name: Goblin atack: 4 defense: 1

격력이 4배가 되었고 방어력은 조건에 부합하지 않아 그대로입니다.

CreatureModifier들이 책임 사슬을 이루어서 Creature의 공격력 또는 방어력 변경을 수행합니다.

 

지금까지의 구현은 실제 게임 구현에서 사용하기에는 인위적이고 부족합이 있습니다.

연결 리스트로는 능력을 받는 것 이외에 잃을 수도 있는데 이것을 지원하기에는 부족합니다.

영구적으로 크리쳐의 능력을 수정하기 보다는 원본을 남겨 두고 변경을 해야 겠습니다.

브로커 사슬

중앙 집중화된 컴포넌트를 두며 게임에서 발생할 수 있는 모든 변경 작업의 목록을 관리합니다.

특정 크리처의 공격력 또는 방어력의 상태 변경에 대한 이력이 있으며 이 것들이 반영된 상태를 구할 수 있습니다.

이러한 기능을 하는 컴포넌트를 이벤트 브러커라 부릅니다. 중재자(Mediator) 패턴이기도 합니다.

상태 변경에 관여한 모든 컴포넌트들을 연결하는 역할을 하기 때문입니다. 

 

  • Boost.Signal2
    어떤 신호를 발생시키고 그 신호를 기다리고 있는 모든 수신처가 신호를 처리할 수 있게 합니다.

queries가 모든 상태 변경 컴포넌트를 모두 가집니다.

Query클래스는 상태 변경 조회와 결과 처리를 합니다.

struct Query
{
    string creature_name;
    enum Argument { attack, defense } argument;
    int result;
 
    Query(const string& creature_name, const Argument argument, const int result)
        : creature_name(creature_name), argument(argument), result(result) {}
};
 
struct Game
{
    signal<void(Query&)> queries;
};

Creature는 이전 구현과 거의 같으며 GetAttack과 GetDefense  Query조회를 통해 상태 변경 값을 얻습니다.

이 과정에서 원래 값은 그대로 보존 되면서 상태(attack, defense)의 보너스 값을 얻거나 잃을 수 있습니다.

class Creature
{
private:
    Game& game;
    int attack, defense;
public:
    string name;
    Creature(Game& game, const string& name, const int attack, const int defense)
        : game(game),attack(attack),defense(defense),name(name) {}
 
    int GetAttack() const
    {
        Query q{ name, Query::Argument::attack, attack };   // Query::Argument::attack 조회이며 result 초기 값은 attack 값입니다.
        game.queries(q);
        return q.result;
    }
    int GetDefense() const
    {
        Query q{ name, Query::Argument::defense, defense }; // Query::Argument::defense 조회이며 result 초기 값은 defense 값입니다.
        game.queries(q);
        return q.result;
    }
 
    friend ostream& operator<<(ostream& os, const Creature& obj)
    {
        return os
            << "name: " << obj.name
            << " attack: " << obj.GetAttack() // GetAttack으로 수정 되었습니다!
            << " defense: " << obj.GetDefense();
    }
};
 
class CreatureModifier
{
    Game& game;
    Creature& creature;
public:
    CreatureModifier(Game& game, Creature& creature)
        : game(game), creature(creature) {}
};
 
class DoubleAttackModifier : public CreatureModifier
{
    connection conn;
public:
    DoubleAttackModifier(Game& game, Creature& creature)
        : CreatureModifier(game, creature)
    {
        conn = game.queries.connect([&](Query& q)       // queries의 수신처를 double attack lambda query를 등록합니다.
            {
                if (q.creature_name == creature.name &&             // [1] double attack lambda query
                    q.argument == Query::Argument::attack)
                    q.result *= 2;
            });
    }
 
    ~DoubleAttackModifier()
    {
        conn.disconnect();
    }
};
 
class DoubleDefenseModifier : public CreatureModifier
{
    connection conn;
public:
    DoubleDefenseModifier(Game& game, Creature& creature)
        : CreatureModifier(game, creature)
    {
        conn = game.queries.connect([&](Query& q)           // queries의 수신처를 double defense lambda query를 등록합니다.
            {
                if (q.creature_name == creature.name &&     // [2] double defense lambda query
                    q.argument == Query::Argument::defense)
                    q.result *= 2;
            });
    }
 
    ~DoubleDefenseModifier()
    {
        conn.disconnect();
    }
};

DoubleAttackModifier , DoubleDefenseModifier는 각각 더블 공격 상태 변경 수신처와 더블 방어 상태 변경 수신처 구현입니다.

이 클래스들의 생성자에서 수신처 연결을 파괴자에서 수신처 연결 종료를 하고 있습니다.

GetAttack과 GetDefense는 원하는 상태에 대한 Query를 던지는 구현입니다. 현재 상태에서 등록되어 있는 수신처들에게 조회를 하여 각각 공격, 방어 값을 얻어 옵니다.

Game game;
Creature goblin{ game, "Strong Goblin", 2, 2 };
 
cout << goblin << endl; // attack 2, defense 2
{
    DoubleAttackModifier dam{ game, goblin };
    cout << goblin << endl; // attack 4, defense 2
 
    DoubleDefenseModifier ddm{ game, goblin };
    cout << goblin << endl; // attack 4, defense 4
}
 
cout << goblin << endl; // attack 2, defense 2

요약

책임 사슬은 컴포넌트들이 어떤명령이나 조회 작업을 차례로 처리할 수 있게하는 단순한 디자인 패턴입니다.

포인터 사슬은 보통 vector나 list로 대체 될 수 있습니다.

브로커 사슬 구현에는 중재자 패턴과 옵져버 패턴이 활용되어 구현되었습니다.

 

 

[독서 리뷰] - 모던 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