이미 만들어진 객체(프로토타입, 원형)을 복사하여 새로운 객체를 생성하는 패턴입니다.
무에서 유를 창조하는 것은 드물다. 이미 존재하는 객체를 복제하여 사용하거나 일부를 수정하여 사용합니다.
이러한 것들이 프로토타입 패턴의 아이디어입니다.
프로토타입 패턴은 객체의 깊은 복제를 수행하되 매번 전체 초기화를 하는 대신 미리 부분적으로 만들어진 객체를 복제하여 약간의 수정만으로 이용할 수 있게합니다.
이 과정에서 원본 객체에 대한 걱정은 하지 않아도 되게 해줍니다.
객체 생성
복잡한 생성을 거쳐 잘 갖추어진 객체가 있다면 이 객체를 그대로 복제하는 것이 좋은 방법입니다.
프로토타입 패턴은 객체의 복제가 주요 기능이다. 복제를 하는 여러 방법들이 알아봅시다.
평범한 중복 처리
복제 대상 객체의 모든 항목이 값으로만 되어 있다면 복제를 해도 아무 문제 없으며 가장 쉽게 복제를 할 수 있다.
struct Address
{
string street;
string city;
int suite;
};
struct Contact
{
string name;
Address address; // 문제 없음
// Address* address; // 중복 처리 문제 발생
};
int main()
{
Contact worker{"", Address{ "123 East Dr", "London", 0 }};
Contact john = worker;
john.name = "John Doe";
john.address.suite = 10;
return 0;
}
Address* address로 되어 있다면 두 객체가 동일한 address를 가르키므로 문제하 발생할 수 있습니다.
john의 주소를 변경 했을 뿐인데 원본의 주소도 바뀌는 중복 처리가 된다.
복제 생성자를 통한 중복 처리
중복 처리 문제를 해결하기 위한 가장 단순한 방법은 복제 생성자로 해결 하는 것이다.
struct Contact
{
string name;
Address* address;
Contact(string name, Address* address) : name{ name }, address{ address } { }
/*
// 1. Address의 항목이 추가 또는 삭제 되었을때 곤란해 진다.
Contact(const Contact& other) : name{ other.name }
{
address = new Address{ other.address->street, other.address->city, other.address->suite };
}
// 2. Address 항목이 수정되어도 신경 쓸 것이 없다.
Contact(const Contact& other) : name{ other.name }
{
address = new Address{ *other.address };
}
// 3. 이것 또한 마찬가지다.
Contact(const Contact& other) : name{ other.name }, address{ new Address{*other.address} }
{
}
*/
};
prototype.clone() 형태의 호출은 가독성 측면에서 복제 생성자나 복제 대입 연산자 보다 명료합니다.
template<typename T> struct Cloneable
{
virtual T clone() const = 0;
};
struct Contact : public Cloneable<Contact>
{
string name;
Address* address;
Contact clone() const
{
return Contact{ name, new Address{ *address } };
}
};
Contact worker { "sss", new Address{ "123 East Dr", "London", 0 }};
Contact john = worker.clone();
4.4 직렬화(Serialize)
객체들 전체 요소들 모두를 명시적으로 복제 연산을 일일이 정의해야한다. 객체를 직렬화 하여 특별한 변환 작업 없이 객체를 복제합니다.
객체를 비트열로 나타내어 온전한 상태로 파일이나 메모리에 쓸 수 있으며, 이를 다시 읽어 Deserialize하면 객체를 온전한 상태로 복구 할 수 있습니다.
다른 언어들은 컴파일된 바이너리에 메타 데이터를 포함하며 리플렉션을 통화여 직렬화 작업을 쉽게 할 수 있지만 C 은 아쉽게도 컴파일된 바이너리에 메타 데이터도 없고 리플렉션 기능도 없다.
Boost.Serialization, 비트 단위의 데이터 포맷과 std::string으로 구성된 객체에 대하여 직렬화 할 수 있습니다
struct Address
{
string street;
string city;
int suite;
friend ostream& operator<<(ostream& os, const Address& obj)
{
return os
<< "street: " << obj.street
<< " city: " << obj.city
<< " suite: " << obj.suite;
}
private:
friend class boost::serialization::access;
template<class Ar> void serialize(Ar& ar, const unsigned int version)
{
ar& street;
ar& city;
ar& suite;
}
};
struct Contact
{
string name;
Address* address = nullptr;
friend ostream& operator<<(ostream& os, const Contact& obj)
{
return os
<< "name: " << obj.name
<< " address: " << *obj.address;
}
private:
friend class boost::serialization::access;
template<class Ar> void serialize(Ar& ar, const unsigned int version)
{
ar& name;
ar& address;
}
};
int main()
{
Contact john;
john.name = "John Doe";
john.address = new Address{ "123 East Dr", "London", 123 };
auto clone = [](const Contact& c)
{
ostringstream oss;
boost::archive::text_oarchive oa(oss);
oa << c;
istringstream iss(oss.str());
boost::archive::text_iarchive ia(iss);
Contact result;
ia >> result;
return result;
};
Contact jane = clone(john);
jane.name = "Jane Doe";
jane.address->street = "123B West Dr";
jane.address->suite = 300;
return 0;
}
Serialization은 데이터를 바이트로 스트리밍하여 메모리, 파일, DB에 저장고 다시 로드 하는 것이 중요 목적입니다.
코드만 보면 오히려 퇴보하였습니다. Boost.Serialization은 강력한 도구임에는 틀림 없습니다.
다음은 Boost.Serialization로 파일 Load/Save로 활용하는 예입니다.
struct Contact
{
string name;
Address* address = nullptr;
friend ostream& operator<<(ostream& os, const Contact& obj)
{
return os
<< "name: " << obj.name
<< " address: " << *obj.address;
}
void Save(string fileName)
{
ofstream output(fileName, ios::binary);
boost::archive::text_oarchive oa(output);
oa << *this;
output.close();
}
void Load(string fileName)
{
ifstream input(fileName, ios::binary);
boost::archive::text_iarchive ia(input);
ia >> *this;
input.close();
}
private:
friend class boost::serialization::access;
template<class Ar> void serialize(Ar& ar, const unsigned int version)
{
ar& name;
ar& address;
}
};
int main()
{
john.Save("john.dat");
Contact john2;
john2.Load("john.dat");
return 0;
}
프로토타입 팩터리
프로토타입 팩터리를 사용하면 전역 변수로 선언하여 복사해서 사용하는 것 보다 우아하고 직관적입니다.
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);
result->name = name;
result->address->suite = suite;
return result;
}
};
Contact EmployeeFactory::main{ "", new Address{ "123 East Dr", "London", 0 } };
Contact EmployeeFactory::aux{ "", new Address{ "123B East Dr", "London", 0 } };
int main()
{
auto john = EmployeeFactory::NewAuxOfficeEmployee("John Doe", 123);
auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125);
return 0;
}
코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글
브릿지 패턴(Bridge Pattern) (0) | 2019.12.08 |
---|---|
어댑터 패턴(Adapter Pattern) (0) | 2019.12.08 |
싱글턴 패턴(Singleton Pattern) (0) | 2019.12.08 |
팩토리 패턴(Factory pattern) (2) | 2019.11.17 |
빌더 패턴(Builder Pattern) (0) | 2019.11.17 |