Observer Pattern
엑셀 시트에서 어떤 표에 값을 입력하면 그래프에 값이 즉각 반영되어 그래프의 그림들이 변하는 것을 볼 수 있다. 이렇게 하나의 데이터가 변경 되었을때 이 데이터와 연결된 모든 객체들이 자동으로 업데이트되도록 하는 기법을 Observer pattern이라고 한다.
객체 사이의 1:N의 종속성을 정의하고 한 객체의 상태가 변하면 종속된 다른 객체에 통보가 가고 자동으로 수정이 일어 나게 한다.
#include <iostream>
#include <vector>
using namespace std;
class Table
{
int data;
public:
void SetData(int d) { data = d; }
};
class PieGraph
{
public:
void Draw(int n)
{
cout << "Pie Graph : ";
for (int i = 0; i < n; i++)
{
cout << "*";
}
}
};
int main()
{
}
위의 코드는 패턴 적용을 위한 간단한 시작점이 되는 예제이다.
- Table 클래스에는 단 하나의 데이터(int data)가 존재하고 SetData 함수를 통해 입력된다.
- PieGraph는 입력되는 값(int n)만큼의 그래프를 문자(*)로 출력해준다.
위의 예에서 데이터 변화의 통보를 받는 측을 관찰자(Observer)라고 하고, Table은 Subject(관찰의 대상)라고 한다. 이 예제를 조금 발전 시켜보자.
개선 수정 1
1. 테이블은 모든 그래프들의 포인터를 알아야 한다.
2. 테이블에 모든 그래프들을 담을 수 있는 vector같은 컨테이너가 필요 하다.
-> vector<Graph*> v
3. 테이블은 모든 형태의 그래프를 담을 수 있어야 하므로 공통의 인터페이스 IGraph를 상속 받는다.
-> vector<IGraph*> v
4. 모든 그래프에는 Update가 있어야한다.
5. 테이블은 그래프들을 연결하고 분리할 수 있어야 한다.
-> attach(IGraph*), detach(IGraph*)
위의 개선점을 적용하면 위의 소스코드를 아래와 같이 수정할 수 있다.
#include <iostream>
#include <vector>
using namespace std;
struct IGraph
{
virtual void update(int) = 0;
virtual ~IGraph() {}
};
class Table
{
// 모든 그래프를 담을 컨테이너가 필요
// 공통의 인터페이스(IGraph)를 담는 컨테이너
vector<IGraph*> v;
int data;
public:
// 그래프를 연결, 분리
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
void SetData(int d)
{
data = d;
// 데이터가 입력되었음을 모든 그래프에 알림
for (auto p : v)
{
p->update(data);
}
}
};
class PieGraph : public IGraph
{
public:
virtual void update(int n)
{
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Pie Graph : ";
for (int i = 0; i < n; i++)
{
cout << "*";
}
cout << endl;
}
};
// 다른 유형의 그래프 추가
class BarGraph : public IGraph
{
public:
virtual void update(int n)
{
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Bar Graph : ";
for (int i = 0; i < n; i++)
{
cout << "+";
}
cout << endl;
}
};
int main()
{
// 그래프 생성
BarGraph barGraph;
PieGraph pieGraph;
// 테이블 생성, 테이블에 그래프 연결
Table t;
t.attach(&barGraph);
t.attach(&pieGraph);
// 사용자 입력된 값으로 그래프 출력
while (1)
{
int n;
cin >> n;
t.SetData(n);
}
}
개선 수정 2
그런데 지금의 Table은 하나의 숫자만 입력 받을 수 있는 너무나도 간단한 형태이므로 이를 개선하여 더 복잡한 형태의 테이블도 입력 받을 수 있게 해보자.
class Table : public Subject
{
int data; // 더 복잡한 데이터->더 복잡한 형테의 테이블 데이터를 다뤄야한다면?
public:
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
void SetData(int d)
{
data = d;
notify(data);
}
};
// 더 복잡한 형태의 데이터를 다루는 Table3D 추가
class Table3D : public Subject
{
int data[10];
public:
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
// vector v가 Subject클래스로 옮겨지면서 v접근이 불가능해졌고, 모든 테이블의 공통 코드가 되므로 알림 기능도 Subject로 이동
void SetData(int d)
{
/*data = d;
for (auto p : v)
{
p->update(data);
}*/
}
};
더 복잡한 형태의 data를 다룰 수 있는 Table3D 클래스가 추가 되었다. 그런데 이 클래스들에는 관찰자 패턴을 위한 코드가 있고 테이블 자체를 관리하는 코드도 있다. 관찰자 패턴을 위한 코드들은 모든 테이블에서 똑같으므로 같은 코드를 계속 만드는 것보다 관잘자 패턴을 제공하는 기반 클래스를 만들어야 하겠다.
개선 수정 3
// 관찰자의 기본기능을 제공하는 클래스
class Subject
{
vector<IGraph*> v;
public:
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
void notify(int data)
{
for (auto p : v)
{
p->update(data);
}
}
};
class Table : public Subject
{
int data; // 더 복잡한 데이터->더 복잡한 형테의 테이블 데이터를 다뤄야한다면?
public:
void SetData(int d)
{
data = d;
notify(data);
}
};
class Table3D
{
int data[10];
public:
// vector v가 Subject클래스로 옮겨지면서 v접근이 불가능해졌고, 모든 테이블의 공통 코드가 되므로 알림 기능도 Subject로 이동
void SetData(int d)
{
/*data = d;
for (auto p : v)
{
p->update(data);
}*/
}
};
Table, Table3D 내의 컨테이너 객체와 attach, dettach 함수가 Subject 클래스로 옮겨졌고, Table, Table3D는 이 클래스의 파생클래스가 되었다. 이에 더해서 다른 유형의 그래프도 추가 해보자.
아래의 소스코드는 이에 대한 전체 코드이다.
#include <iostream>
#include <vector>
using namespace std;
struct IGraph
{
virtual void update(int) = 0;
virtual ~IGraph() {}
};
// 관찰자의 기본기능을 제공하는 클래스
class Subject
{
vector<IGraph*> v;
public:
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
void notify(int data)
{
for (auto p : v)
{
p->update(data);
}
}
};
class Table : public Subject
{
int data; // 더 복잡한 데이터->더 복잡한 형테의 테이블 데이터를 다뤄야한다면?
public:
void SetData(int d)
{
data = d;
notify(data);
}
};
class Table3D
{
int data[10];
public:
void SetData(int d)
{
/*data = d;
for (auto p : v)
{
p->update(data);
}*/
}
};
class PieGraph : public IGraph
{
public:
virtual void update(int n)
{
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Pie Graph : ";
for (int i = 0; i < n; i++)
{
cout << "*";
}
cout << endl;
}
};
// 다른 유형의 그래프 추가
class BarGraph : public IGraph
{
public:
virtual void update(int n)
{
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Bar Graph : ";
for (int i = 0; i < n; i++)
{
cout << "+";
}
cout << endl;
}
};
int main()
{
BarGraph barGraph;
PieGraph pieGraph;
Table t;
t.attach(&barGraph);
t.attach(&pieGraph);
while (1)
{
int n;
cin >> n;
t.SetData(n);
}
}
개선 수정 4
테이블은 "값이 변경 되었다." 뿐만 아니라 "어떤 값이 어떤 값으로 변했다."를 알려야 한다.
변경된 값을 전달하는 방법에는 다음과 같이 두가지가 있다.
1. 변화를 통보할 때 값을 같이 전달하는 방법 (Push, 위의 예제의 방식)
- 데이터가 하나 일때는 괜찮지만 데이터가 많거나 복잡하면 사용이 곤란하다.
2. 변화되었다는 사실만 전달하고, Graph에서 Table의 멤버 함수를 통해서 접근하는 방법 (Pull)
- 그래프의 입장에서는 테이블의 포인터를 전달 받아야 한다.
- 이런 방식은 데이터가 아무리 복잡해도 해당 Getter만 있으면 어떤 자료도 받아올 수 있는 장점이 있다.
기존의 방식(1->Push)을 (2->Pull)의 방식으로 수정해보자.
#include <iostream>
#include <vector>
using namespace std;
class Subject;
struct IGraph
{
//virtual void update(int) = 0;
virtual void update(Subject*) = 0;
virtual ~IGraph() {}
};
class Subject
{
vector<IGraph*> v;
public:
void attach(IGraph* p) { v.push_back(p); }
void detach(IGraph* p)
{
// 생략...
}
//void notify(int data)
void notify() // 변화된 사실만 전달
{
for (auto p : v)
{
p->update(this); // 주소로 함수를 통해 데이터를 받을 수 있도록 한다.
}
}
};
class Table : public Subject
{
int data; // 더 복잡한 데이터->더 복잡한 형테의 테이블 데이터를 다뤄야한다면?
public:
int GetData() { return data; }
void SetData(int d)
{
data = d;
notify();
}
};
class Table3D
{
int data[10];
public:
// vector v가 Subject클래스로 옮겨지면서 v접근이 불가능해졌고, 모든 테이블의 공통 코드가 되므로 알림 기능도 Subject로 이동
void SetData(int d)
{
/*data = d;
for (auto p : v)
{
p->update(data);
}*/
}
};
class PieGraph : public IGraph
{
public:
virtual void update(Subject* p)
{
// table에 접근해서 data를 꺼내온다.
//int n = p->GetData(); // Error -> GetData()는 Subject가 아닌 Table의 것이어서 불가능 -> 캐스팅
int n = static_cast<Table*>(p)->GetData();
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Pie Graph : ";
for (int i = 0; i < n; i++)
{
cout << "*";
}
cout << endl;
}
};
// 다른 유형의 그래프 추가
class BarGraph : public IGraph
{
public:
virtual void update(Subject* p)
{
// table에 접근해서 data를 꺼내온다.
//int n = p->GetData(); // Error -> GetData()는 Subject가 아닌 Table의 것이어서 불가능 -> 어쩔수 없이 캐스팅
int n = static_cast<Table*>(p)->GetData();
Draw(n); // 그래프를 다시 그린다.
}
void Draw(int n)
{
cout << "Bar Graph : ";
for (int i = 0; i < n; i++)
{
cout << "+";
}
cout << endl;
}
};
int main()
{
BarGraph barGraph;
PieGraph pieGraph;
Table t;
t.attach(&barGraph);
t.attach(&pieGraph);
while (1)
{
int n;
cin >> n;
t.SetData(n);
}
}
IGraph의 변화
1. IGraph - update 함수의 파라미터가 int에서 Subject* 로 변경되었다.
2. Subject - notify의 파라미터가 없어졌다. 즉 값의 변화 사실만 알리고, update 함수에 자신의 포인터를 넘긴다.
3. BarGraph - update에서 subject 포인터를 사용할 수 있게 되었으나 이 포인터에서 직접 GetData를 호출 할 수 없기 때문에 casting 과정이 필요하다.
'프로그래밍 이야기 > C++ 기초' 카테고리의 다른 글
함수 오브젝트와 람다 표현식 (0) | 2021.09.17 |
---|---|
[Design Pattern] Container (0) | 2021.02.05 |
[Design Pattern] PIMPL (Pointer to Implementation) (0) | 2021.02.03 |
[Design Pattern] Bridge (0) | 2021.02.02 |
[DesignPattern] Facade (0) | 2021.01.19 |