Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

제 1부 생성패턴 - 3장. 팩토리(Factory) - 작성중 본문

소프트웨어 공학/디자인패턴

제 1부 생성패턴 - 3장. 팩토리(Factory) - 작성중

scarecrow1992 2021. 5. 22. 01:54
문제를 풀려고 자바를 썼는데 ProblemFactory를 얻었다.    -철지난 JAVA 유머-

 

  • 여기서는 두 개의 GOF 패턴, factory method와 abstract factory를 동시에 알아본다.
  • 두 패턴은 서로 긴밀한 연관이 있다.

 


3.1 시나리오

2차원 직교좌표계의 좌표점 정보를 저장하려 할 경우, 아래와 같은 구현이 쉽게 떠올르것이다.

1
2
3
4
5
class Point{
public:
    float x, y;
    Point(const float x, const float y) : x{x}, y{y} {}
};
cs

 

그렇다면 극 좌표계로 좌푯값을 저장해야 할 경우 어떻게 하면 될까?

아래처럼 극 좌표계용 생성자를 추가하면 어떨까?.

1
2
3
4
Point(const float r, cnst float theta){
    x = r * cos(theta);
    y = r * sin(theta);
}
cs

 

하지만 슬프게도 직교좌표계용 생성자와 극좌표계용 생성자 둘다 2개의 float 값을 파라미터로 하기 때문에 컴파일러는 2개의 생성자를 서로 구분할 수 없다.

3번째 매개변수를 추가하고 이를 key 삼아 하나의 생성자 함수 내에서 분기점을 만들 수 있지만, 이는 생성자의 직관적인 사용방법에 있어서 실패한 code 작성법이다.

기능적으로 문제는 없지만, 훌륭한것과는 거리가 멀다.

그럼 이를 어떻게 해결하면 될까?

문제를 해결하기 전에 이런 문제가 발생하는 이유에 대해 짚어보자

생성자는 타입과 같은 이름을 가저야하고, 매개변수가 같을 경우 함수를 구분할 수 없다는데서 발생한 문제다.

이를 개선하는 방법에 대해 논하는것이 팩토리 패턴의 핵심이다.

 

 


3.2 팩토리

팩토리 : 특정 클래스의 인스턴스를 생성하는 함수들을 몰아 넣은 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
 
using namespace std;
 
class Point {
public:
    float x, y;
    friend class PointFactory;
 
private:
    Point(float x, float y) : x(x), y(y) {}
};
 
class PointFactory {
public:
    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) };
    }
 
};
 
int main(void) {
    Point cartesianPoint = PointFactory::NewCartesian(5 * cos(M_PI_4), 5 * sin(M_PI_4));
    Point polarPoint = PointFactory::NewPolar(5, M_PI_4);
 
    return 0;
}
cs

눈여겨 볼 부분이 두가지 있다.

  1. Point의 생성자는 private로 선언되어 사용자가 직접 생성자를 호출하는 경우가 없게 한다.
  2. Point는 PointFactory를 friend 클래스로 선언하여 Point의 생성자에 접근 가능케 한다.

 


3.3 팩토리 메서드

팩토리 메서드의 구현법

  1. 생성자의 접근지정자를 protected로 하여 생성자를 클래스 내에서 만 참조가능하도록 외부로 부터 숨김
  2. 대신 Point 인스턴스를 만들어 리턴하는 static 함수를 제공
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iostream>
 
using namespace std;
 
class Point {
protected:
    Point(const float x, const float y) : x{ x }, y{ y } {}
 
public:
    int x, y;
 
    static Point NewCartesian(float x, float y) {
        return { x, y };
    }
 
    static Point NewPolar(float r, float theta) {
        return { r * cos(theta), r * sin(theta) };
    }
    // ...
};
 
int main(void) {
    Point cartesianPoint = Point::NewCartesian(5 * cos(M_PI_4), 5 * sin(M_PI_4));
    Point polarPoint = Point::NewPolar(5, M_PI_4);
 
    return 0;
}
cs

위 코드는 가독성도 좋고, 구현방법도 간단하다.

 

 


3.4 내부 팩토리

  • 내부 팩터리 : 생성할 타입의 내부 클래스로서 존재하는 간단한 팩터리
  • C#, JAVA등 friend 키워드에 해당하는 문법이 없는 프로그래밍 언어에서 흔히 사용됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<iostream>
 
using namespace std;
 
class Point {
private:
    Point(float x, float y) : x(x), y(y) {}
 
    class PointFactory {
    private:
        PointFactory() {}
 
    public:
        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) };
        }
    };
 
public:
    float x, y;
    static PointFactory Factory;
};
 
int main(void) {
    Point cartesianPoint = Point::Factory.NewCartesian(5 * cos(M_PI_4), 5 * sin(M_PI_4));
    Point polarPoint = Point::Factory.NewPolar(5, M_PI_4);
 
    return 0;
}
cs

내부 팩터리의 단점 : 팩터리가 생성할 수 있는 클래스는 단 한종류 뿐이다.

내부 팩터리의 장점

  • 원본 객체 자체에서 팩터리를 얻을 수 있게 하면 API의 사용성에 크게 도움이 된다.
    • 즉, 사용자 입장에서 인스턴스의 생성이 보다 직관적이다.
    • 가령 "Point:": 까지 입력하면 코드 자동 완성 목록에 Point 인스턴스를 생성하기 위한 수단이 자동으로 나올것이다.

 

PointFactory를 public으로 꺼내면서 static으로 선언하면, 별도의 인스턴스 없이 클래스에 바로 접근할 수도 있다.

 


3.5 추상 팩터리

그동안 한 종류의 클래스에 대한 인스턴스를 생성하는 경우만 살펴봤다. 여러 종류의 클래스에 대한 인스턴스를 생성하는 경우에 대해 알아보자.

추상 팩터리는 복잡한 시스템에서만 필요성이 떠오른다. 흔하게 쓰이지는 않지만 알아두는것이 좋다.

 

다음과 같은 상황을 생각해보자. 뜨가운차와 커피 그리고 차가운 주스와 콜라를 판매한다. 각각의 음료들안 각기 다른 장비를 이용해 만들어진다. 이 부분을 팩토리로 모델링해보자.

 

먼저 직관적으로 코드를 작성하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include<iostream>
 
using namespace std;
 
class Drink {
public:
    int preference;
    Drink(int preference) : preference(preference) {}
    Drink() : Drink(-1) {}
 
    virtual void prepare(int volume, int time) = 0;
};
 
class HotDrink : public Drink {
public:
    HotDrink(int preference) : Drink(preference){}
    HotDrink() : Drink() {}
 
    virtual void prepare(int volume, int time) = 0;
};
 
class Tea : public HotDrink {
public:
    Tea(int preference) : HotDrink(preference){}
    Tea() : HotDrink(5) {}
 
    void prepare(int volume, int time) override {
        cout << "Take tea bag, boil water, pour " << volume << "ml, add some lemon" << ", preference : " << preference  << endl;
    }
};
 
class Coffee : public HotDrink {
public:
    Coffee(int preference) : HotDrink(preference) {}
    Coffee() : HotDrink(10) {}
 
    void prepare(int volume, int time) override {
        cout << "Grinde coffee bean, boil water" << time << " minutes, pour " << volume << ", preference : " << preference << endl;
    }
};
 
class IceDrink : public Drink {
public:
    IceDrink(int preference) : Drink(preference) {}
    IceDrink() : Drink(7) {}
 
    virtual void prepare(int volume, int cnt) = 0;
};
 
class Juice : public IceDrink {
public:
    Juice(int preference) : IceDrink(preference) {}
    Juice() : IceDrink(3) {}
 
    virtual void prepare(int volume, int cnt) {
        cout << "Squeeze fruit, put " << cnt << " ice, pour" << volume << "ml" << ", preference : " << preference << endl;
    }
};
 
class Coke : public IceDrink {
public:
    Coke(int preference) : IceDrink(preference) {}
    Coke() : IceDrink() {}
 
    virtual void prepare(int volume, int cnt) {
        cout << "pour coke, put " << cnt << " ice, pour" << volume << "ml" << ", preference : " << preference << endl;
    }
};
 
unique_ptr<HotDrink> make_hot_drink(string type) {
    unique_ptr<HotDrink> drink;
    if (type == "tea") {
        drink = make_unique<Tea>();
        drink->prepare(2002);
    }
    else {
        drink = make_unique<Coffee>();
        drink->prepare(503);
    }
 
    return drink;
}
 
unique_ptr<IceDrink> make_ice_drink(string type) {
    unique_ptr<IceDrink> drink;
    if (type == "juice") {
        drink = make_unique<Juice>();
        drink->prepare(2005);
    }
    else {
        drink = make_unique<Coke>();
        drink->prepare(5010);
    }
 
    return drink;
}
 
 
int main(void) {
    unique_ptr<HotDrink> tea    = make_hot_drink("tea");
    unique_ptr<HotDrink> coffee    = make_hot_drink("coffee");
    unique_ptr<IceDrink> juice    = make_ice_drink("juice");
    unique_ptr<IceDrink> coke    = make_ice_drink("coke");
 
    return 0;
}
cs

여기서 중요한것은 drink는 자신의 부피, 얼음의 갯수, 물 데우는 시간 등을 알 수 없다. 생산 공정에서만 알 수 있다.

하지만 drink는 자신의 선호도가 얼마인지 직접 알 수 있다. 이는 생성과 함께 결정되며, 프로그램 내부에서 수정할 방법은 없다. 각 음료수 인스턴스가 자신이 속한 음료수 종류에 대한 선호도를 가진다는게 굉장히 비실용적이지만, 일단은 이렇게 두자.

위 예에서는 뜨거운 음료와 차가운 음료를 만드는 prepare 함수의 원형이 같기에 굳이 구분할 필요가 없지만, 그 의미는 명확히 다르기에 추후 원활한 유지보수를 위해서 다른 클래스로 만들었다.

앞서 음료 종류마다 생산공정이 다르다 소개했으며, 이를 hot drink와 ice drink에서 매개변수 arg를 통해 분기점을 형성하도록 하여 각기 다르게 생산하도록 구현했다.

이부분을 팩터리로 개선해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include<iostream>
#include<map>
 
using namespace std;
 
class Drink {
public:
    int preference;
    Drink(int preference) : preference(preference) {}
    Drink() : Drink(-1) {}
 
    virtual void prepare(int volume, int time) = 0;
};
 
class HotDrink : public Drink {
public:
    HotDrink(int preference) : Drink(preference){}
    HotDrink() : Drink() {}
 
    virtual void prepare(int volume, int time) = 0;
};
 
class Tea : public HotDrink {
public:
    Tea(int preference) : HotDrink(preference){}
    Tea() : HotDrink(5) {}
 
    void prepare(int volume, int time) override {
        cout << "Take tea bag, boil water, pour " << volume << "ml, add some lemon" << ", preference : " << preference  << endl;
    }
};
 
class Coffee : public HotDrink {
public:
    Coffee(int preference) : HotDrink(preference) {}
    Coffee() : HotDrink(10) {}
 
    void prepare(int volume, int time) override {
        cout << "Grinde coffee bean, boil water" << time << " minutes, pour " << volume << ", preference : " << preference << endl;
    }
};
 
class IceDrink : public Drink {
public:
    IceDrink(int preference) : Drink(preference) {}
    IceDrink() : Drink(7) {}
 
    virtual void prepare(int volume, int cnt) = 0;
};
 
class Juice : public IceDrink {
public:
    Juice(int preference) : IceDrink(preference) {}
    Juice() : IceDrink(3) {}
 
    virtual void prepare(int volume, int cnt) {
        cout << "Squeeze fruit, put " << cnt << " ice, pour" << volume << "ml" << ", preference : " << preference << endl;
    }
};
 
class Coke : public IceDrink {
public:
    Coke(int preference) : IceDrink(preference) {}
    Coke() : IceDrink() {}
 
    virtual void prepare(int volume, int cnt) {
        cout << "pour coke, put " << cnt << " ice, pour" << volume << "ml" << ", preference : " << preference << endl;
    }
};
 
class HotDrinkFactory {
public:
    virtual unique_ptr<HotDrink> make() const = 0;
};
 
class TeaFactory : public HotDrinkFactory {
public:
    unique_ptr<HotDrink> make() const override {
        return make_unique<Tea>();
    }
};
 
class CoffeeFactory : public HotDrinkFactory {
public:
    unique_ptr<HotDrink> make() const override {
        return make_unique<Coffee>();
    }
};
 
class IceDrinkFactory {
public:
    virtual unique_ptr<IceDrink> make() const = 0;
};
 
class JuiceFactory : public IceDrinkFactory {
public:
    unique_ptr<IceDrink> make() const override {
        return make_unique<Juice>();
    }
};
 
class CokeFactory : public IceDrinkFactory {
public:
    unique_ptr<IceDrink> make() const override {
        return make_unique<Coke>();
    }
};
 
class DrinkFactory {
private:
    map<string, unique_ptr<HotDrinkFactory>> hot_factories;
    map<string, unique_ptr<IceDrinkFactory>> ice_factories;
 
public:
    DrinkFactory() {
        hot_factories["coffee"= make_unique<CoffeeFactory>();
        hot_factories["tea"= make_unique<TeaFactory>();
 
        ice_factories["juice"= make_unique<JuiceFactory>();
        ice_factories["coke"= make_unique<CokeFactory>();
    }
 
    unique_ptr<Drink> make_drink(const string& name, int volume, const void* arg) {
        if (hot_factories[name] != NULL) {
            unique_ptr<HotDrink> drink = hot_factories[name]->make();
            drink->prepare(volume, (int)arg);
            return drink;
        }
        else {
            unique_ptr<IceDrink> drink = ice_factories[name]->make();
            drink->prepare(volume, (int)arg);
            return drink;
        }
 
    }
};
 
int main(void) {
    DrinkFactory factory;
 
    unique_ptr<Drink> tea = factory.make_drink("tea"200, (void*)2) ;
    unique_ptr<Drink> coffee = factory.make_drink("coffee"50, (void*)3);
    unique_ptr<Drink> juice = factory.make_drink("juice"200, (void*)5);
    unique_ptr<Drink> coke = factory.make_drink("coke"50, (void*)10);
 
    return 0;
}
cs

찬음료와 뜨거운 음료를 만들기 위한 factory를 따로 만든 후에 이를 상속받는 각 음료수를 만들기위한 factory도 따로 만든다.

그리고 DrinkFactory에 뜨거운 음료와 찬 음료로 구분된 음료수들의 factory의 인스턴스를 형성한 후, make_drink 함수를 호출하여 적절한 음료수를 생산한다.

 

main문 내에서 사용자는 찬음료수와 뜨거운 음료수를 구분하여 주문할 필요가 없으며, HotDrink와 IceDrink를 구분하지 않고 음료를 받고 있다.

솔직히 이부분에 대해서는 Drink로 받아야 할지 아니면 Hot/IceDrink로 받아야할지 무엇이 정답인지는 정확히 모르겠다.

 

 


3.6 함수형 팩토리

-작성-

 

 

 

 

Comments