GGym's Practice Notes

2-1. 빌더_빌더 패턴 본문

Design Pattern/Modern C++ 디자인패턴

2-1. 빌더_빌더 패턴

GGym_ 2020. 3. 30. 19:39

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

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

- 시나리오

    웹 페이지를 그리기위한 컴포넌트들을 생성해야 한다고 할때 

    각각의 HtmlElement를 생성하는 과정을 간소화 한다.

 

객체 지향 스타일을 채용하여 HtmlElement 클래스를 정의하여 각 html 태그에 대한 정보를 저장한다.

struct HtmlElement{
    string name;
    string text;
    vector<HtmlElement> elements;

    string str(int indent = 0) const{
        // 컨텐츠를 양식에 맞추어 출력
    }
   
    HtmlElement(){}
    HtmlElement(const string& name, const string& text) 
        : name(name), text(text){}

};

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

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

양식을 제어하기가 좀 더 쉽고 목적하는 출력을 할 수 있지만 각각의 HtmlElement를 생성하는 작업이 그렇게 편리하다고 볼 수 없다.

 

- 단순한 빌더

빌더 패턴은 단순하게 개별 객체의 생성을 별도의 다른 클래스에 위임한다.

struct HtmlBuilder{
    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();}
};

 

HtmlBuilder는 HTML 구성요소의 생성만을 전담하는 클래스이다.

add_child()는 현재 요소에 자식요소를 추가하는 목적으로 사용된다.

이 클래스는 출력부분이 아래와 같이 사용된다.

HtmlBuilder builder{"ul"};
builder.add_child("li", "hello");
builder.add_child("li", "world");
cout << builder.str() << endl;

 

- 흐름식 빌더

다음과 같이 자기 자신을 리턴하도록 수정한다.

HtmlBuilder& add_child(string child_name, string child_text){
	HtmlElement e{child_name, child_text};
    root.elements.emplace_back(e);

    return *this;
}

빌더 자기자신이 리턴되기 때문에 다음과 같이 메서드들이 꼬리를 무는 호출이 가능하다.

이러한 형태로 호출하는 것을 흐름식 인터페이스라고 부른다.

HtmlBuilder builder{"ul"};
builder.add_child("li", "hello").add_child("li", "world");
cout << builder.str() << endl;

리턴을 포인터로 할지 참조로 할지는 개발자의 마음대로다.

 

- 의도 알려주기

사용자가 빌더 클래스를 사용해야 한다는 것을 알게하기 위해서 빌더를 사용하지 않으면 객체 생성을 불가능하게 한다.

(모든 생성자를 protected로 숨겨서 객체생성을 불가능하게 한다.)

이 접근 방법은 두가지 축으로 이루어진다. 첫번째로 모든 생성자를 숨겨서 사용자가 접근할 수 없게 하는 것이다. 두번째로 생성자를 숨긴 대신 HtmlElement 자체에 팩터리 메서드를 두어 빌더를 생성할 수 있게 한다. 팩터리 메서드 역시 static 메서드로 만든다.

 

- 전체 코드

더보기
#include<iostream>
#include<sstream>
#include<vector>
#include<string>
#include<memory>
using namespace std;

struct HtmlBuilder;

class HtmlElement{
public:
    string name;
    string text;
    vector<HtmlElement> elements;
    const size_t indent_size = 2;

    string str(int indent = 0) const{
        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();
    }

    static unique_ptr<HtmlBuilder> build(const string& root_name){
        return make_unique<HtmlBuilder>(root_name);
    }
    friend HtmlBuilder;
protected:
    HtmlElement(){}
    HtmlElement(const string& name, const string& text) 
        : name(name), text(text){}
    int num = 10;

};

class HtmlBuilder{
public:
    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();}
    
    operator HtmlElement() const {return root;}
    HtmlElement root;
    
};

int main(){
    auto builder = HtmlElement::build("ul")
    ->add_child("li","Hello")->add_child("li","World");
    cout << builder->str() << endl;

    return 0;
}