(최신 C++ 디자인 패턴) 책을 바탕으로 연구한 내용을 요약한 것입니다.
명상 패턴
우리가 작성하는 대부분의 코드는 포인터 또는 직접 참조를 사용하여 서로 다른 구성 요소(클래스) 간에 통신합니다. 경우에 따라 구성 요소 간에 다른 개체의 존재를 명시적으로 알아야 하는 것이 불편할 수 있습니다. 또는 상대 객체를 알고 있더라도 객체 생성/소멸 시간이 관리되기 때문에 포인터나 참조를 통해 액세스하고 싶지 않을 수 있습니다.
중개자는 구성 요소 간의 통신을 지원하는 메커니즘입니다. 중개자 자체는 통신과 관련된 모든 구성 요소에서 액세스할 수 있어야 합니다. 즉, 모든 구성 요소에 노출되는 전역 정적 변수 또는 참조여야 합니다.
대화방
대화방은 중간 디자인 패턴을 적용할 수 있는 가장 대표적인 예입니다. 중재자 패턴에 대한 설명에 들어가기 전에 간단한 대화방이 필요합니다.
// 참여자
struct Person {
Person(const string &name);
// 메시지 수신
void receive(const string &origin, const string &message);
// 채팅 룸의 모든 참여자에게 메시지 송신
void say(const string &message) const;
// 개인 메시지(Private Message) 기능으로, 특정 참여자만 지정해 메시지 송신
void pm(const string &who, const string &message) const;
string name;
ChatRoom *room = nullptr;
vector<string> chat_log;
};
// 채팅룸
struct ChatRoom {
// 채팅 룸에 사용자 입장
void join(Person *p);
// 채팅 룸의 모든 참여자에게 메시지 송신
void broadcast(const string &origin, const string &message);
// 개인 메시지 송신
void message(const string &origin, const string &who, const string &message);
// 추가만 된다고 가정
vector<Person *> people;
};
포인터, 참조 또는 shared_ptr이든 개체에 대한 액세스는 사람에 따라 다릅니다. 벡터를 사용할 때 명심해야 할 유일한 제한 사항은 참조를 사용할 수 없다는 것입니다. 여기서는 포인터를 사용합니다.
void ChatRoom::join(Person *p) {
string join_msg = p->name + " joins the chat";
broadcast("room", join_msg);
p->room = this;
people.push_back(p);
}
void ChatRoom::broadcast(const string &origin, const string &message) {
for ( auto p : people ) {
// 자기자신 제외 메시지 전송
if ( p->name != origin ) {
p->receive(origin, message);
}
}
}
void ChatRoom::message(const string &origin, const string &who, const string &message) {
auto target = find_if(begin(people), end(people),
(&)(const Person *p) {
return p->name == who;
});
if ( target != end(people) ) {
(*target)->receive(origin, message);
}
}
위와 같이 채팅방 API가 준비되었으므로 다음과 같이 Person을 구현할 수 있습니다.
void Person::say(const string &message) const {
room->broadcast(name, message);
}
void Person::pm(const string &who, const string &message) const {
room->message(name, who, message);
}
void Person::receive(const string &origin, const string &message) {
string s(origin + ": \"" + message + "\"");
cout << "(" << name << "'s chat session) " << s << "\n";
chat_log.emplace_back(s);
}
이제 여기에서는 현재 대화방 세션에서 메시지가 언제 어디서 왔는지 기록하는 기능을 만들 것입니다. (책에는 구현되어 있지 않습니다.)
중개자 및 이벤트
대화방 예에는 일관된 주제가 있습니다. 누군가 메시지를 게시하면 참가자에게 알려야 합니다. 이벤트 진행자의 개념은 관련된 모든 사람에게 적용됩니다. 참가자는 이벤트 수신자로 등록하고 알림을 생성할 수 있습니다.
C++ 언어에서는 이벤트 기능이 개선되지 않았습니다. 따라서 Boost.Signals2 라이브러리를 사용합니다. 이벤트 사용에 꼭 필요한 기능을 제공합니다.
축구 게임과 같은 간단한 예를 들어 보겠습니다. 축구에서 선수가 골을 넣으면 코치가 그를 칭찬한다고 합시다. 이 시점에서 누가 골을 넣었고 얼마나 많은 골을 전달할 것인지에 대한 정보입니다. 이러한 정보를 제공하기 위해 이벤트 데이터를 일반화하는 기본 클래스를 다음과 같이 정의할 수 있습니다.
struct EventData {
virtual ~Eventata() = default;
virtual void print() const = 0;
};
struct PlayerScoredData : EventData {
PlayerScoredData(const string &player_name, const int goals_scored_so_far) :
player_name(player_name), goals_scored_so_far(goals_scored_so_far) {}
void print() const override {
cout << player_name << " has scored! (their " << goals_scored_so_far << " goal)" << "\n";
}
string player_name;
int goals_scored_so_far;
};
여기에 중개자를 추가해 보겠습니다. 이번에는 조치가 없습니다. 이벤트 기반 아키텍처에서 중개자는 작업을 직접 수행하지 않습니다.
struct Game {
signal<void(EventData *)> events; // 관찰자
};
극단적인 방법이지만 Game 클래스가 없어도 이벤트를 전역 변수로 설정하는 방법도 있습니다. 게임 개체의 참조를 구성 요소에 명시적으로 주입하는 코드가 있으면 이벤트에 대한 종속성 관계를 표시할 수 있다는 이점이 있습니다.
이제 플레이어 클래스를 생성하고 매치메이커 게임에 대한 참조를 가질 수 있습니다.
struct Player {
Player(const string &name, Game &game) : name(name), game(game) {}
void score() {
goals_scored++;
// 이벤트를 이용해 PlayerScoredData 생성
PlayerScoredData ps(name, goals_scored);
// 수신처로 등록된 객체들에 알림 전송
game.evnets(&ps);
}
string name;
int goals_scored = 0;
Game &game;
};
struct Coach {
explicit Coach(Game &game) : game(game) {
// game.events에 수신 등록
game.events.connect(()(EventData *e) {
PlayerScoredData *ps = dynamic_cast<PlayerScoredData *>(e);
if ( ps && ps->goals_scored_so_far < 3 ) {
cout << "coach says: well done, " << ps->player_name << "\n";
}
});
}
Game &game;
};
Coach 클래스의 Lambda 함수 매개변수 유형은 EventData*입니다. 슛 온 골 이벤트만 커버하고 싶은데 어떤 이벤트가 오는지 모르겠습니다. 따라서 대상 포트의 이벤트 유형이 dynamic_cast로 수신되는지 여부를 확인합니다. 다른 방법으로도 확인할 수 있습니다.
마법 같은 부분은 설정 단계에서 발생합니다. 각 이벤트에 대한 모든 슬롯을 명시적으로 나열할 필요는 없습니다. 처리하려는 이벤트만 처리할 수 있습니다.
요약
중간 디자인 패턴을 사용하면 시스템의 모든 구성 요소가 참조할 수 있는 중개자를 직접 참조하지 않고도 구성 요소가 통신할 수 있습니다. 중개자를 통해 직접 메모리 액세스 대신 식별자로 통신할 수 있습니다.
가장 간단한 구현은 리스트를 멤버 변수로 받아 리스트를 살펴보고 필요한 요소만 선별적으로 처리하는 함수다.
정교한 구현을 통해 개별적으로 발생하는 이벤트를 수신 대기하려는 개체가 등록할 수 있습니다.