아래와 같이 Cartesian, Polar 좌표계를 초기화한다고 가정하자.
enum class PointType{
cartesian,
polar
};
struct Point{
float x;
float y;
Point(const float a, const float b, PointType type = PointType::cartesian)
{
if (type == PointType::cartesian)
{
x = a;
y = b;
}
else
{
x = a * cos(b); //in math.h
y = a * sin(b); //in math.h
}
}
};
위 코드의 문제점은 아래와 같다.
Point 객체를 만들어 리턴하는 static 함수를 제공
struct Point{
protected:
Point(const float x, const float y): x{x}, y{y}
public:
static Point NewCartesian(float x, float y){ return {x, y}; } //factory method for cartesian
static Point NewPolar(float r, float theta){ return r*cos(theta), r*sin(theta) }; } //factory method for polar
}
NewCartesian
과 NewPolar
는 Point의 객체를 생성해 리턴한다. 이를 팩터리 메서드라 부른다. 3.1에서의 코드와는 다르게 인자로 x, y 혹은 r, theta를 입력받기 때문에 더 직관적이다.
이제 아래와 같이 Polar 좌표점을 생성할 수 있다.
auto p = Point::n=NewPolar(5, M_PI_4/* pi/4 */);
Point를 생성하는 함수들을 별도의 클래스에 몰아넣을 수 있다. 그러한 클래스를 팩터리라 부른다. 팩터리를 적용하기 위해 새로 Point 클래스를 정의하자.
struct Point{
float x;
float y;
friend class PointFactory;
private:
Point(float x, float y) : x(x), y(y) {}
}
struct PointFactory{
static Point NewCartesian(float x, float y){
return Point{ x, y };
}
static Point NewPolar(float r, float theta){
return Point{ r*cos(theta), r*sin(theta) };
}
}
...
auto my_Point = PointFactory::NewCartesian(3, 4);
위 코드에서
private
로 선언되어 사용자가 직접 생성자를 호출하지 않도록 한다.3.3에서 PointFactory를 friend
키워드로 상속 형태를 지정했는데 다른 언어에서는 없는 기능이다. 이 때 다른 언어에서는 내부 팩터리를 사용하는데 이를 C++에서도 구현할 수 있다.
struct Point{
private:
Point(float x, float y) : x(x), y(y) {}
struct PointFactory{
private:
PointFactory(){}
public:
static Point NewCartesian(float x, float y){
return { x, y }; //can access Point constructor
}
static Point NewPolar(float r, float theta){
return { r*cos(theta), r*sin(theta) };
}
}
public:
float x;
float y;
static PointFactory Factory;
};
...
auto pp = Point::Factory.NewCartesian(2,3);
// auto pp2 = Point::Factory::NewCartesian(2,3); //if PointFactory is defined as public
typedef PointFactory Factory;
auto pp3 = Point::Factory.NewPolar(2, 0.5);
여기서 PointFactory는 Point의 private 멤버에 접근이 가능하고, 반대로 Point도 PointFactor의 private 멤버에 접근이 가능하다.
참고로, 이와 같은 Nested Class의 접근 권한은 아래와 같다.
1. 내부 Class가 private: Class에서 접근 가능, 외부에서 접근 불가능
2. 내부 Class가 protected: Class 및 파생 클래스에서 접근 가능, 외부에서 접근 불가능
3. 내부 Class가 public: Class와 외부에서 접근 가능
Nested Class의 장점은 굳이 외부에서 사용되지 않을 Class를 선언해 Class Naming에 방해를 하지 않도록 하는 장점이 있다.
다시 본문으로 돌아가, 내부 팩터리는 팩터리가 생성해야 할 클래스가 단 1종류 일 때 유용하다. 만약 팩터리가 여러 타입의 객체를 생성해야 한다면 내부 팩터리는 적합하지 않다.
내부 팩터리는 1종류의 클래스만 생성이 가능했는데 추상 팩터리를 사용하면 여러 종류의 객체를 생성할 수 있다. 다만 자주 사용되는 패턴은 아니다.
struct HotDrink{
virtual void prepare(int volume) = 0;
}
struct Tea : HotDrink{
void prepare(int volumn) override{
std::cout << "Make tea - volumn:" << volumn << std::endl;
}
}
unique_ptr<HotDrink> make_drink(string type){
unique_ptr<HotDrink> drink;
if (type == "tea"){
drink = make_unique<Tea>();
drink->prepare(200);
}
else{
drink = make_unique<Coffee>();
drink->prepare(50);
}
return drink;
}
여기서 HotDrink
의 Factory를 아래와 같이 만들 수 있다.
struct HotDrinkFactory{
virtual unique_ptr<HotDrink> make() const = 0;
}
struct CoffeeFactory : HotDrinkFactory{
unique_ptr<HotDrink> make() const override{
return make_unique<Coffe>();
}
}
여기서 음료의 종류별로 Factory를 만들어줘야 하는데, map을 이용해 팩터리를 참조로 내부에 가지도록 할 수 있다.
class DrinkFactory{
map<string, unique_ptr<HotDrinkFactory>> hot_factories;
public:
DrinkFactory(){
hot_factories["coffee"] = make_unique<CoffeeFactory>();
hot_factories["tea"] = make_unique<TeaFactory>();
}
unique_ptr<HotDrink> make_drink(const string& name){
auto drink = hot_factories[name]->make();
drink->prepare(200); //note that tea=200 and coffee=50
return drink;
}
}
map을 사용하지 않고 enum을 사용한 vector도 가능할 것이다.
함수 블록을 이용하면 함수형 팩터리를 아래와 같이 만들 수 있다.
class DrinkWithVolumnFactory{
map<string, function<unique_ptr<HotDrink>()>> factories;
public:
DrinkWithVolumnFactory(){
factories["tea"] = []{
auto tea = make_unique<Tea>();
tea->prepare(200);
return tea;
}
factories["coffee"] = []{
auto coffee = make_unique<Coffee>();
coffee->prepare(50);
return coffee;
}
}
}