모던 c++ 디자인 패턴 을 읽고 인상 깊었던 내용을 정리합니다.
복제 대상 객체의 모든 항목이 값으로만 되어 있다면 복제하는데 문제될 것이 전혀 없다. 하지만 내부 객체가 포인터로 된 경우라면 하나의 값을 변경했을 뿐인데 다른쪽 값도 변경되게 된다.
이를 해결하기 위한 방법은 여러가지가 있지만, 복제 생성자를 배제하고 아래와 같은 인터페이스를 별도로 두는 방법도 있다.
template <typename T>
struct Cloneable
{
virtual T clone() const = 0;
}
이 인터페이스를 구현하여 복제가 필요할 때 prototype.clone()을 호출한다.
자주 복제해서 사용할 기본 객체들이 미리 정해져 있다면 그 객체들을 어디에 저장해 두어야 할까?
가장 쉽게 생각나는 방법은 전역 변수로 두고 사용하는 쪽에서 가져다 복제해 쓰면 된다.
하지만 좀 더 우아하고 직관적인 방법은 프로토타입을 저장할 별도의 클래스를 두고 목적에 맞는 복제본을 요구받는 시점에 만들어 제공하는 것이다.
struct EmployeeFactory
{
static Contact main;
static Contact aux;
static unique_ptr<Contact> NewMainOfficeEmployee(string name, int suite)
{
return NewEmployee(name, suite, main);
}
static unique_ptr<Contact> NewAuxOfficeEmployee(string name, int suite)
{
return NewEmployee(name, suite, aux);
}
private:
static unique_ptr<Contact> NewEmployee(string name, int suite, Contact& proto)
{
auto result = make_unique<Contact>(proto);
resut->name = name;
resut->address->suite = suite;
return result;
}
};
auto john = EmployeeFactory::NewAuxOfficeEmployee("John Doe", 123);
auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125);
싱글턴 구현 문제는 다른 싱글턴 컴포넌트에서 또 다른 싱글턴을 사용할 때 나타난다.
서로 다른 여러 도시의 인구수의 합을 계산하는 싱글턴 컴포넌트와 도시의 이름과 그 인구수의 목록을 담고 있는 데이터베이스 싱글턴 컴포넌트가 있다고 하자. 이때 인구수의 합을 계산하는 컴포넌트가 데이터베이스 컴포넌트에 의존한다.
class SingletonDatabase : public Database
{
SingletonDatabase() {}
std::map<std::string, int> capitals;
public:
SingletonDatabase(SingletonDatabase const&) = delete;
void operator=(SingletonDatabase const&) = delete;
static SingletonDatabase& get()
{
static SingletonDatabase db; //c++ 11이후부터는 스레드 안정성 보장
return db;
}
int get_population(const std::string& name) override
{
return capitals[name];
}
};
struct SingletonRecordFinder
{
int total_population(std::vector<std::string> names)
{
int result = 0;
for (auto& name : names)
result += SingletonDatabase::get().get_population(name);
return result;
}
}
문제는 SingletonRecordFinder가 SingletonDatabase에 밀접하게 의존한다는 것이다.
이는 SingletonRecordFinder를 단위테스트 하기 어렵게 만든다. 특정 클래스를 직접 사용하는 것보다 해당 클래스의 인터페이스에 의존하도록 만드는 것이 낫다.
struct ConfigurableRecordFinder
{
explicit ConfigurableRecordFinder(Database& db) : db{db} {}
int total_population(std::vector<std::string> names)
{
int result = 0;
for (auto& name : names)
result += db.get_population(name);
return resut;
}
Database& db;
};
이제 ConfigurableRecordFinder를 테스트할 때 아래와 같은 더미 데이터를 지정할 수 있게 된다.
class DummyDatabase : public Database
{
std::map<std::string, int> capitals;
public:
DummyDatabase()
{
capitals["alpha"] = 1;
capitals["beta"] = 2;
capitals["gamma"] = 3;
}
int get_population(const std::string& name) override {
return capitals[name];
}
}
이제 실제 데이터베이스를 사용하지 않기 때문에 실 데이터가 변경될 때마다 단위 테스트 코드를 수정해야 할 일이 없어진다.