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부 생성패턴 - 2장. 빌더(Builer) 본문

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

제 1부 생성패턴 - 2장. 빌더(Builer)

scarecrow1992 2021. 4. 16. 21:44

빌더 패턴(Builder Pattern)은 생성이 까다로운 객체를 쉽게 처리하기 위한 패턴이다.

즉, 생성자 호출 코드 단 한줄로 생성할 수 없는 객체를 다룬다.

ex) 다른 객체들의 조합, 까다로운 로직이 필요한 객체들

이러한 객체들을 생성하는 코드는 따로 분리되어 관리될 필요가 있다.

2.1 시나리오

1
2
3
4
<ul>
    <li>hello</li>
    <li>world</li>
</ul>
cs

위와 같은 웹페이지를 그리기 위한 컴포넌트를 생성해야 한다고 하자.

단순하게 단어를 나타내는 항목 두개("hello"와 "world")를 html의 비순차("<ul>") 리스트("<li>") 태그로 출력해보자.

1
2
3
4
5
6
7
string words[] = {"hello""world"};
ostringstream oss;
oss << "<ul>";
for(auto w : words)
    oss << " <li>" << w << "<\li>";
oss << "</ul>";
printf(oss.str().c_str());
cs

이 코드는 목적대로 잘 작동한다. 하지만 융통성이 없다.

가령 항목마다 앞에 점을 찍거나 순서대로 번호를 매겨야 한다면, 또는 새로운 항목을 리스트의 끝에 추가해야 한다면 이 코드를 어떻게 수정해야 하는가? 모든 oss 코드들을 수정해도 되겠지만, 코드가 확장될수록 수작업만으로는 해결이 어려워진다.

 

대안으로 OOP 스타일을 채용할 수 있다. HtmlElement 클래스를 정의하여 각 html 태그에 대한 정보를 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HtmlElement {
public:
    string name;
    string text;
    vector<HtmlElement> elements;
 
    HtmlElement() {}
    HtmlElement(const string &name, const string& text)
        : name(name), text(text) {}
 
    string str(int indent = 0const{
        //컨텐츠를 양식에 맞추어 출력
    }
};
cs

 

이런 접근방법을 활용하여 출력 양식을 더 쉽게 드러나도록 리스트를 생성 할 수 있다.

1
2
3
4
5
string words[] = {"hello""world"};
HtmlElement list{"ul"""};
for(auto w : worlds)
    list.elements.emplace_back{HtmlElement{"li", w}};
printf(list.str().c_str());
cs

 

하지만 이 방법도 이전의 작업보다 편리하다 보기는 힘들다.

가령 HtmlElement의 생성자 구문에 변화가 일어난다면, HtmlElement의 인스턴스 생성자를 호출하는 모든 코드들을 일일이 수작업으로 고처줘야 하기 때문이다.

물론 이런 문제점은 생성자 오버로딩으로 어느정도 커버 가능하지만, 우리는 빌더 패턴으로 핵심적인 부분에서 개선을 시도해본다

 

 

2.2 단순한 빌더

개별 객체의 생성을 별도의 다른 클래스에 위임한다.

아래 예제에서는 HtmlBuilder에 그 역할을 위임하였다.

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
#include<iostream>
#include<string>
#include<vector>
#include<sstream>
 
using namespace std;
 
class HtmlBuilder;
 
class HtmlElement {
public:
    string name;
    string text;
    vector<HtmlElement> elements;
    const size_t indent_size = 2;
 
    static unique_ptr<HtmlBuilder> build(const string& root_name) {
        return make_unique<HtmlBuilder>(root_name);
    }
 
    string str(int indent = 0const {
        ostringstream oss;
        string i(indent_size * indent, ' ');
        oss << i << "<" << name << ">" << endl;
        if (text.size() > 0)
            oss << string(indent_size * (indent + 2), ' '<< text << endl;
 
        for (const auto& e : elements)
            oss << e.str(indent + 2);
 
        oss << i << "</" << name << ">" << endl;
        return oss.str();
    }
 
    HtmlElement() { }
    HtmlElement(const string& name, const string& text)
        : name(name), text(text) {}
};
 
 
class HtmlBuilder {
public:
    //operator HtmlElement() const { return root; }
    HtmlElement root;
 
    HtmlBuilder(string root_name) { root.name = root_name; }
 
    void add_child(string child_name, string child_text) {
        HtmlElement e{ child_name, child_text };
        root.elements.emplace_back(e);
    }
 
    string str() { return root.str(); }
};
 
 
int main(void) {
    HtmlBuilder builder("ul");
    builder.add_child("li""hello");
    builder.add_child("li""world");
    cout << builder.str() << endl;
 
 
    return 0;
}
 
cs

HtmlElement의 생성자에 변화가 발생해도 HtmlBuilder의 add_child 함수만 수정하면 된다.

또한 HtmlElement의 인스턴스는 HtmlBuilder 인스턴스의 생성과 함께 그 내부에 저장된다.

즉, HtmlElement는 사용자에게 노출되지 않는다.

 

 

2.3 흐름식 빌더

이전에 만들었던 단순한 빌더의 add_child()함수의 리턴값을 활용하면 흐름식 인터페이스(fluent interface) 스타일의 빌더를 만들 수 있다.

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
#include<iostream>
#include<string>
#include<vector>
#include<sstream>
 
using namespace std;
 
class HtmlBuilder;
 
class HtmlElement {
public:
    string name;
    string text;
    vector<HtmlElement> elements;
    const size_t indent_size = 2;
    
    string str(int indent = 0const {
        ostringstream oss;
        string i(indent_size * indent, ' ');
        oss << i << "<" << name << ">" << endl;
        if (text.size() > 0)
            oss << string(indent_size * (indent + 1), ' '<< text << endl;
 
        for (const auto& e : elements)
            oss << e.str(indent + 1);
 
        oss << i << "</" << name << ">" << endl;
        return oss.str();
    }
 
    HtmlElement() {}
    HtmlElement(const string& name, const string& text)
        : name(name), text(text) {}
};
 
 
class HtmlBuilder {
public:
    HtmlElement root;
 
    HtmlBuilder(string root_name) { root.name = root_name; }
 
    HtmlBuilder& add_child(string child_name, string child_text) {
        HtmlElement e{ child_name, child_text };
        root.elements.emplace_back(e);
        return *this;
    }
 
    string str() { return root.str(); }
};
 
 
int main(void) {
    HtmlBuilder builder("ul");
    builder.add_child("li""hello").add_child("li""world");
    cout << builder.str() << endl;
 
    return 0;
}
cs

add_child가 자기자신의 레퍼런스를 반환하도록 하여 55번 줄처럼 연쇄 호출이 가능하게끔 한다.

 

 

2.4 의도 알려주기

  • HtlmlElement의 생성을 전담하는 빌더클래스를 사용자에게 알려줘야함
  • 이를 위해선 빌더를 안 쓸경우 객체 생성이 불가능하도록 강제하면됨
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
#include<iostream>
#include<string>
#include<vector>
#include<sstream>
 
using namespace std;
 
class HtmlBuilder;
 
class HtmlElement {
    // HtmlBuilder에서 HtmlElement의 생성자에 접근해야 하므로 friend를 선언해주었다.
    // 가능하면 friend 선언 없이 구현해보고싶다.
friend class HtmlBuilder;   
public:
    string name;
    string text;
    vector<HtmlElement> elements;
    const size_t indent_size = 2;
 
    // 2. 생성자를 숨긴 대신,
    // HtmlElement 자체에 팩터리 메서드를 두어 빌더를 생성할 수 있게 한다.
    static unique_ptr<HtmlBuilder> build(const string& root_name) {
        return make_unique<HtmlBuilder>(root_name);
    }
 
    HtmlElement& add_child(string child_name, string child_text) {
        HtmlElement e{ child_name, child_text };
        elements.push_back(e);
        return *this;
    }
 
    string str(int indent = 0const {
        ostringstream oss;
        string i(indent_size * indent, ' ');
        oss << i << "<" << name << ">" << endl;
        if (text.size() > 0)
            oss << string(indent_size * (indent + 2), ' '<< text << endl;
 
        for (const auto& e : elements)
            oss << e.str(indent + 2);
 
        oss << i << "</" << name << ">" << endl;
        return oss.str();
    }
 
protected// 1. 생성자를 외부에서 못 보도록 숨긴다
    HtmlElement() {}
    HtmlElement(const string& name, const string& text)
        : name(name), text(text) {}
};
 
 
class HtmlBuilder {
public:
    operator HtmlElement() const { return root; }
    HtmlElement root;
 
    HtmlBuilder(string root_name) { root.name = root_name; }
    
    HtmlBuilder& add_child(string child_name, string child_text) {
        HtmlElement e{ child_name, child_text };
        root.elements.emplace_back(e);
        return *this;
    }    
 
    string str() { return root.str(); }
    friend class HtmlElement;
};
 
int main(void) {
    
    unique_ptr<HtmlBuilder> builder = HtmlElement::build("ul");
    builder->add_child("li""hello").add_child("li""world");
    cout << builder->str() << endl;
    
    cout << "\n\n";
 
    HtmlElement e = HtmlElement::build("ul")->
        add_child("li""hello")
        .add_child("li""world");
    cout << e.str() << endl;
 
    return 0;
}
cs

 

위 코드에서 의도를 알리기 위한 방법은 크게 2가지 축으로 이루어젔다.

  1. HtmlElement의 생성자를 숨기기 위해 모두 private로 선언했다.
  2. 생성자를 숨긴 대신 HtmlBuilder를 만들기 위한 static 빌드 함수를 HtmlElement내에 자체적으로 만들어 주었다.
    •  HtmlElement 클래스의 static 함수를 통해 HtmlBuilder 인스턴스가 생성된다.

이외에도 55번째 줄에서 변환생성자를 통해 HtmlBuilder인스턴스가 HtmlElement 변수로 대입 가능하게끔 하였다.

 

생성자를 숨기고, static Build()함수를 통해 사용자에게 HtmlElement를 만들기 위해선 HtmlBuilder를 만들라고 최대한 유도하고 있지만, API를 사용하라고 명시적으로 알려줄 방법은 없다.

 

위 코드에서 우리는 builder 인스턴스에 속한 element를 얻기위해 add_child 함수를 사용하고 있지만, element를 얻기 위한 보다 직관적인 함수인 build() 함수를 추가해보자.

1
2
3
HtmlElement HtmlBuilder::build() const {
    return root;    // std::move를 적용할 수 있다.
}
cs

????

 

 

2.5 그루비-스타일 빌더

참조

 

 

 

 

2.6 컴포지트 빌더

하나의 객체를 생성하는데 복수의 빌더를 사용할때 유용한 패턴

가령 개인 신상정보를 저장하는 프로그램이 있다고 하자.

1
2
3
4
5
6
7
8
9
10
11
class Person {
public:
    //주소
    string street_address, post_code, city;
 
    //직업
   string company_name, position;
    int annual_income = 0;
 
    Person() {}
};
cs

person 에는 '주소'와 '직업'이라는 두종류의 정보가 있다. 빌더를 각 정보마다 따로 두고 싶다면 API를 어떻게 짜야하는가? 이를 해결하기 위해 컴포지트 빌더가 쓰인다. 우선 위의 사항을 해결하기 위해 4개의 클래스로 빌더를 정의한다.

 

 

 

 

첫번째 빌더는 PersonBuilderBase 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PersonBuilderBase {
public:
protected:
    Person & person;
    explicit PersonBuilderBase(Person& person) : person(person) {}
 
public:
    operator Person() {
        return move(person);
    }
 
    PersonAddressbuilder lives() const;
    PersonJobBuilder works() const;
};
 
cs

레퍼런스 person : 현재 생성되고 있는 객체에 대한 정보를 담는 참조 변수

 

 

사용자가 실제 사용할 빌더를 만든다.

1
2
3
4
5
6
7
8
class PersonBuilder : public PersonBuilderBase {
private:
    Person p;
 
public:
    PersonBuilder() : PersonBuilderBase(p) {}
};
 
cs

여기서 실제 객체가 생성된다. 빌더 베이스를 상속받을 필요는 없지만 빌더 구동 절차를 초기화 하기 쉽도록 변의상 상속받는다.

 

 

 

 

 

 

 

 

Comments