의도
- 원형이 되는(prototypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성
언제 쓰는가 ?
- 제품의 생성, 복합, 표현 방법에 독립적인 제품을 만들고자 할 때
- 인스턴스화할 클래스를 런타임에 지정할 때
- 제품 클래스 계통과 병렬적으로 만드는 팩토리 클래스를 피하고 싶을 때
- 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나일 때
구조
- Prototype : 자신을 복제하는 데 필요한 인터페이스를 정의
- Concrete Prototype : 자신을 복제하는 연산을 구현
- Client : 원형에 자기 자신의 복제를 요청하여 새로운 객체를 생성
협력 방법
- 사용자는 원형 클래스에 스스로를 복제하도록 요청
결과
- 런타임에 새로운 제품을 추가, 삭제 : 원형을 등록하면 새로운 객체 추가, 삭제 용이
- 값들을 다양화함으로써 새로운 객체를 명세 : 변수를 다르게 하여 새로운 객체를 정의
- 구조를 다양화함으로써 새로운 객체를 명세 : 응용 프로그램에서 사용자의 편의를 위해 복잡합 구성요소를 복합하여 Clone 연산을 지원하는 경우가 많음
- 서브클래스의 수를 줄임 : 팩토리 메서드는 처리할 제품 클래스의 계통과 병렬적으로 서브클래스의 수가 늘어남. 원형 패턴은 Creator 클래스에 만들어달라고 요청하는 것이 아닌 자기 자신을 복제하는 연산을 자신에게 요청하기 때문에 서브클래스의 수가 늘어나지 않음
- 동적으로 클래스에 따라 응용프로그램을 설정할 수 있음
구현
- 원형 관리자를 사용 : 원형의 수가 정해지지 않은 때라면 가능한 원형이 등록된 레지스트리 (원형 관리자, Prototype Manager) 를 관리. 원형 관리자는 어떤 키에 부합되는 원형을 저장하고, 찾아서 반환, 삭제하는 기능을 담당하는 저장소.
- Clone() 연산을 구현 : 어려움. 객체 구조가 환형 참조(Circular reference)를 포함할 때라면 더욱. 얕은 복사를 해야 할지, 깊은 복사를 해야 할지 변수마다 정해야 하기 때문.
- Clone()을 초기화 : Initialize 메서드 혹은 Setter 등을 사용자에게 제공해야 함
예제 코드
- Monster를 편하게 스폰하기 위한 MonsterFactory를 만들었음
- MonsterFactory에는 미리 몬스터가 가지는 속성을 Register 해주고 복제하기 원하는 순간에 Clone 메서드를 통해 몬스터를 스폰할 수 있음
- 같은 몬스터 종류여도 이동속도, 공격력 등에 차이가 있을 수 있음에 가정을 두고 예제를 작성했음
- 위 구현의 3가지 경우 중 1번만 수행한 예제라 할 수 있음 -> 깊은 복사는 할 필요가 없었고 Initialize, Setter 등은 패턴의 본질을 흐릴 것 같아 추가하지 않음
#include "Prototype1.h"
int main()
{
MonsterFactory monster_factory;
monster_factory.Register<BlueSnail>(10.0f, 10.0f);
monster_factory.Register<BlueSnail>(20.0f, 10.0f);
monster_factory.Register<BlueSnail>(20.0f, 20.0f);
monster_factory.Register<RedDrake>(30.0f, 30.0f);
monster_factory.Register<RedDrake>(20.0f, 20.0f);
monster_factory.Register<RedDrake>(40.0f, 50.0f);
std::cout << "Registered MonsterSize : " << monster_factory.RegisterdMonsterSize() << std::endl << std::endl;
Monster* m1 = monster_factory.Clone(2);
Monster* m2 = monster_factory.Clone(5);
m1->ShowInfo();
m2->ShowInfo();
return 0;
}
- 실행 결과를 먼저 확인해 보면 Clone(2), Clone(5)에 해당하는 각각 3번째 6번째 추가한 몬스터의 속성을 잘 보여주고 있음
- 또한 등록해둔 몬스터의 갯수만큼 6을 잘 찍어냄
- 이 패턴의 논점이 흐려질수도 있는 예제인 것 같아 미리 말하면 Clone 메서드를 통해 미리 생성되어 있던 인스턴스를 '복사'한 것이 이 패턴의 사용 이유임
//Prototype1.h
#pragma once
#include <vector>
#include "Prototype1_Components.h"
#include <cassert>
class MonsterFactory
{
public:
template<typename T>
void Register(float speed, float attack_damage)
{
static_assert(_STD is_base_of_v<Monster, T>, "T is not derived from Monster Class");
MonsterPrototypes.push_back(new T(speed, attack_damage));
}
Monster* Clone(int index)
{
assert(index >= 0 && index < MonsterPrototypes.size());
return MonsterPrototypes[index]->Clone();
}
int RegisterdMonsterSize()
{
return MonsterPrototypes.size();
}
private:
std::vector<Monster*> MonsterPrototypes;
};
- 새로운 종류의 몬스터를 Register 하는 과정에선 Monster Class를 상속받은 경우에만 코드로 작성할 수 있게 static_assert로 체크
- 미리 만들어 둔 인자를 2개 받는 생성자가 있고 speed와 attack_damage를 몬스터 별 차이점으로 가정하여 새로운 몬스터 등록
- 등록된 몬스터를 Clone하고 싶을 경우 배열에 인덱스로 접근하여 그 몬스터의 클론을 얻어올 수 있음
- 배열에서 인덱스로 얻어오는 방법이 후지다면 vector 컨테이너 대신 multimap 컨테이너로 구성하여 몬스터를 타입별로 저장하고 타입에 맞는 몬스터를 얻어오게 끔 바꿔도 괜찮을 것 같음
//Prototype1_Components.h
#pragma once
#include <iostream>
class Monster
{
public:
Monster(float speed, float attack_damage)
: speed(speed), attack_damage(attack_damage)
{}
//Clone 연산 시 복사 생성자에 의존 -> 포인터 변수가 있을 시 깊은 복사를 수행해줘야 함(매커니즘에 따라 아닐 수도)
Monster(const Monster& rhs)
{
speed = rhs.speed;
attack_damage = rhs.attack_damage;
}
virtual Monster* Clone() = 0;
void ShowInfo()
{
std::cout << "speed : " << speed << std::endl;
std::cout << "attack_damage : " << attack_damage << std::endl << std::endl;
}
private:
float speed;
float attack_damage;
};
class BlueSnail : public Monster
{
public:
BlueSnail(float speed, float attack_damage)
: Monster(speed, attack_damage)
{}
virtual Monster* Clone() override { return new BlueSnail(*this); }
};
class RedDrake : public Monster
{
public:
RedDrake(float speed, float attack_damage)
: Monster(speed, attack_damage)
{}
virtual Monster* Clone() override { return new RedDrake(*this); }
};
- 보통 Prototype 패턴을 구성하기 위해 복사 생성자를 재정의하는 편이라 함(깊은 복사를 수행해야 할 수가 있음). 하지만 우리 변수는 사실 복사 생성자가 없었어도 의도한 대로 잘 돌아갔을 것.(복사 생성자를 재정의하는 경우가 많다하여 작성)
'디자인 패턴 > 생성' 카테고리의 다른 글
단일체(Singleton) (0) | 2023.12.19 |
---|---|
팩토리 메서드(Factory Method) (2) | 2023.12.18 |
빌더(Builder) (2) | 2023.12.17 |
추상 팩토리(Abstract Factory) (0) | 2023.12.15 |
생성 패턴 (0) | 2023.12.15 |