의도

  • 원형이 되는(prototypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성

 

언제 쓰는가 ?

  • 제품의 생성, 복합, 표현 방법에 독립적인 제품을 만들고자 할 때
  • 인스턴스화할 클래스를 런타임에 지정할 때
  • 제품 클래스 계통과 병렬적으로 만드는 팩토리 클래스를 피하고 싶을 때
  • 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나일 때

 

구조

GoF의 디자인 패턴 중 P.171

  • Prototype : 자신을 복제하는 데 필요한 인터페이스를 정의
  • Concrete Prototype : 자신을 복제하는 연산을 구현
  • Client : 원형에 자기 자신의 복제를 요청하여 새로운 객체를 생성

 

협력 방법

  • 사용자는 원형 클래스에 스스로를 복제하도록 요청

 

결과

  1. 런타임에 새로운 제품을 추가, 삭제 : 원형을 등록하면 새로운 객체 추가, 삭제 용이
  2. 값들을 다양화함으로써 새로운 객체를 명세 : 변수를 다르게 하여 새로운 객체를 정의
  3. 구조를 다양화함으로써 새로운 객체를 명세 : 응용 프로그램에서 사용자의 편의를 위해 복잡합 구성요소를 복합하여 Clone 연산을 지원하는 경우가 많음
  4. 서브클래스의 수를 줄임 : 팩토리 메서드는 처리할 제품 클래스의 계통과 병렬적으로 서브클래스의 수가 늘어남. 원형 패턴은 Creator 클래스에 만들어달라고 요청하는 것이 아닌 자기 자신을 복제하는 연산을 자신에게 요청하기 때문에 서브클래스의 수가 늘어나지 않음
  5. 동적으로 클래스에 따라 응용프로그램을 설정할 수 있음

 

구현

  1. 원형 관리자를 사용 : 원형의 수가 정해지지 않은 때라면 가능한 원형이 등록된 레지스트리 (원형 관리자, Prototype Manager) 를 관리. 원형 관리자는 어떤 키에 부합되는 원형을 저장하고, 찾아서 반환, 삭제하는 기능을 담당하는 저장소.
  2. Clone() 연산을 구현 : 어려움. 객체 구조가 환형 참조(Circular reference)를 포함할 때라면 더욱. 얕은 복사를 해야 할지, 깊은 복사를 해야 할지 변수마다 정해야 하기 때문.
  3. 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

+ Recent posts