[모던C++디자인패턴] 3. 팩터리

짜장범벅·2022년 6월 4일
0

모던CPP디자인패턴

목록 보기
2/4

3.1 시나리오

아래와 같이 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의 Initializer에서 a, b가 아니라 x, y 혹은 r, theta로 입력 받는 것이 직관적이다. 단, 위 C++에서는 생성자의 parameter의 이름으로 오버로딩이 불가능하기 때문에 안된다.
  • (개인적으로) enum class를 좋아하지 않는다;;

3.2 팩터리 메서드

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
}

NewCartesianNewPolar는 Point의 객체를 생성해 리턴한다. 이를 팩터리 메서드라 부른다. 3.1에서의 코드와는 다르게 인자로 x, y 혹은 r, theta를 입력받기 때문에 더 직관적이다.

이제 아래와 같이 Polar 좌표점을 생성할 수 있다.

auto p = Point::n=NewPolar(5, M_PI_4/* pi/4 */);

3.3 팩터리

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

위 코드에서

  • Point의 생성자는 private로 선언되어 사용자가 직접 생성자를 호출하지 않도록 한다.
  • PointFactory를 Point의 friend 클래스로 선언한다. 즉 PointFactory가 Point의 생성자에 접근할 수 있도록 한다. 또한 팩터리 클래스와 생성될 클래스가 동시에 만들어짐을 강제한다.

3.4 내부 팩터리

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종류 일 때 유용하다. 만약 팩터리가 여러 타입의 객체를 생성해야 한다면 내부 팩터리는 적합하지 않다.

3.5 추상 팩터리

내부 팩터리는 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도 가능할 것이다.

3.6 함수형 팩터리

함수 블록을 이용하면 함수형 팩터리를 아래와 같이 만들 수 있다.

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;
        }
    }
}
profile
큰일날 사람

0개의 댓글