[모던 c++ 디자인 패턴] 4장, 5장

hyng·2023년 4월 18일
0

모던 c++ 디자인 패턴 을 읽고 인상 깊었던 내용을 정리합니다.

프로토타입

4.2 평범한 중복 처리

복제 대상 객체의 모든 항목이 값으로만 되어 있다면 복제하는데 문제될 것이 전혀 없다. 하지만 내부 객체가 포인터로 된 경우라면 하나의 값을 변경했을 뿐인데 다른쪽 값도 변경되게 된다.

이를 해결하기 위한 방법은 여러가지가 있지만, 복제 생성자를 배제하고 아래와 같은 인터페이스를 별도로 두는 방법도 있다.

template <typename T> 
struct Cloneable
{
	virtual T clone() const = 0;
}

이 인터페이스를 구현하여 복제가 필요할 때 prototype.clone()을 호출한다.

4.5 프로토타입 팩터리

자주 복제해서 사용할 기본 객체들이 미리 정해져 있다면 그 객체들을 어디에 저장해 두어야 할까?

가장 쉽게 생각나는 방법은 전역 변수로 두고 사용하는 쪽에서 가져다 복제해 쓰면 된다.

하지만 좀 더 우아하고 직관적인 방법은 프로토타입을 저장할 별도의 클래스를 두고 목적에 맞는 복제본을 요구받는 시점에 만들어 제공하는 것이다.

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);

싱글턴

5.3 싱글턴의 문제

싱글턴 구현 문제는 다른 싱글턴 컴포넌트에서 또 다른 싱글턴을 사용할 때 나타난다.

서로 다른 여러 도시의 인구수의 합을 계산하는 싱글턴 컴포넌트와 도시의 이름과 그 인구수의 목록을 담고 있는 데이터베이스 싱글턴 컴포넌트가 있다고 하자. 이때 인구수의 합을 계산하는 컴포넌트가 데이터베이스 컴포넌트에 의존한다.

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];
		}
}

이제 실제 데이터베이스를 사용하지 않기 때문에 실 데이터가 변경될 때마다 단위 테스트 코드를 수정해야 할 일이 없어진다.

profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글