옵저버 패턴은 어떤 객체의 상태가 변할 때 함께 동작하고 싶은 즉, 그 객체에 의존성을 가진
객체들이 자동으로 감지하고 갱신될 수 있게 하는 패턴이다.
1. 의존 관계 분리
보통 이러한 패턴들을 접해보지 않은 분들( 혹은 패턴을 접했어도, 그 중요성(존재 이유)을 잘 모르는 분들)은
어떤 객체의 변화에 따라 함께 변화해야할때 다음과 같이 작성한다.
class Player
{
public:
void move(void) noexcept
{
// 소유하고 있는 인형을 움직여 준다.
_puppet.move();
}
private:
Puppet _puppet;
};
void main()
{
Player player;
player.move();
}
player의 움직임에 따라 player가 보유 및 소환한 퍼펫들이 있다면, 퍼펫들의 move() 메소드를 같이 호출한다.
player가 이러한 아주 간단한 클래스 라면 문제가 없지만, 점점 복자해지고 수많은 필드를 가지게 되면
문제가 발생한다.
void move(void) noexcept
{
// 소유하고 있는 인형을 움직여 준다.
_puppet.move();
// 카메라 위치를 적절히 바꾼다.
_camera.move();
// 캐릭터를 중심으로 표시하는 미니맵 정보들
_minimap.updateActors();
// 캐릭터를 따라다니는 status 정보
_status.updateInfos();
// ... 수많은 의존 객체들 ...
}
좀 극단적인 예 이지만, 이러한 코드 작성이 있을법한 얘기이다. (게임 뿐만 아니라, 응용 프로그램, 웹 등에서도....)
요러한 의존 관계를 옵저버 패턴을 통해 분리할 수 있다.
2. 옵저버 패턴 구현
객체의 상태변화가 여러개 라면 옵저버들은 어떤건 받고 어떤건 받고 싶지 않을 때가 있다. 그래서 옵저버들의
"관심사"를 표현할 event type 객체를 만든다.
class EventType
{
public:
EventType(const std::string& typeName) noexcept
: _typeName(typeName)
{
}
const std::string& getTypeName(void) const noexcept
{
return _typeName;
}
private:
std::string _typeName;
};
그 다음은 주체(Subject, 위 예제에서 Player)의 상태가 변경이 일어나면 자동으로 감지하여
update()가 호출 되는는 단순 Observer 객체이다. update() 메소드는 재구현 되도록 virtual로 만든다.
class Observer
{
public:
virtual void update(const EventType* eventType) noexcept
{
std::cout << "event 처리중" << eventType->getTypeName().c_str() << std::endl;
}
};
옵저버들을 등록하고 관리할 Subject 객체 이다. (편의를 위해 detach()및 메모리 해제는 작성하지 않았다)
class Subject
{
public:
void attach(const EventType* eventType, Observer* observer) noexcept
{
TObservers* observers = nullptr;
TAccessor accessor = _mapper.find(eventType->getTypeName());
//1) find 실패시 TObservers 새로 만들어서 넣어두자
if (accessor == _mapper.end())
{
observers = new TObservers;
_mapper.insert(make_pair(eventType->getTypeName(), observers));
}
else
{
observers = accessor->second;
}
//2) event type에 대한 observer 목록에 넣는다.
observers->push_back(observer);
}
void notify(const EventType* eventType) noexcept
{
TObservers* observers = nullptr;
TAccessor accessor = _mapper.find(eventType->getTypeName());
//1) find 실패시 아무것도 안함
if (accessor == _mapper.end())
{
return;
}
//2) observer 목록을 돌며 update
observers = accessor->second;
for (int i = 0; i < observers->size(); ++i)
{
observers->at(i)->update(eventType);
}
}
private:
using TObservers = std::vector<Observer*>;
using TEventObserverMapper = std::unordered_map<std::string, TObservers*>;
using TAccessor = TEventObserverMapper::iterator;
TEventObserverMapper _mapper;
};
event type객체의 type 명으로 _mapper에 옵저버 그룹을 등록을 한다. 이제 옵저버들은 관심사(event type)에 따라
원하는대로 통지를 받을 수 있다. notify() 메소드가 그 구현 예이다.
이제 Player의 move() 메소드를 간단하게 만들자. 기존에는 Player 객체에서 "강한 결합"상태로 직접 호출 시켜 줬기
때문에, 필드(멤버)로 가지고 있거나 어딘가에서 get() 해 와서 update()를 호출해야 했다.
이제는 해당 필드를 제거 하고 다음과 같이 한다.
class PlayerEvent
{
public:
static EventType _MOVE_;
};
EventType PlayerEvent::_MOVE_("Move");
class Player : public Subject
{
public:
void move(void) noexcept
{
notify(&PlayerEvent::_MOVE_);
}
};
Player 객체가 Subject를 상속하고, move()시 notify()를 호출하도록 했다. 이제 Player객체의 move에 관심이 있는
객체들은 다음과 같이 구현 하면 된다.
class Puppet : public Observer
{
public:
virtual void update(const EventType* eventType) noexcept
{
move();
}
void move(void) noexcept
{
std::cout << "Puppet::move() 호출됨!" << std::endl;
}
};
void main()
{
Player player;
Puppet puppet;
//1) 옵저버 등록
player.attach(&PlayerEvent::_MOVE_, &puppet);
//2) Puppet::move()도 자동으로 호출된다.
player.move();
}
Subject 구현시 mapper("관심사"와 옵저버들 매핑)를 기본으로 구현했는데, GOF 책에서는 단순 옵저버 리스트로
설명하고 있다. 나는 좀더 일반적 event system의 모습으로 구현하고 싶어서 그렇게 하였다.
모든 객체를 "복합체"로 구성하면 "통지"를 쉽게 해줄 수 있지만,
프로젝트 전체에 걸쳐서 모든 객체를 복합체로 만들 수도 없는 노릇이다..
그냥 유명 프레임워크(보편적 event 시스템을 갖추고 있다) 없이 밑바닥 부터 시스템을 만든다면,
무조건 옵저버 패턴의 구현체를 만들어두자!! (제발..ㅠ)
'프로그래밍 > C++' 카테고리의 다른 글
Visitor Pattern (0) | 2021.08.21 |
---|---|
Bridge Pattern (0) | 2021.07.15 |
Prototype Pattern (0) | 2021.07.13 |
Adaptor Pattern (0) | 2021.06.30 |
Factory Pattern - Simple Factory (1) | 2021.02.21 |