컴퓨터 언어/디자인패턴

빌더 패턴 Build pattern

cepiloth 2021. 5. 7. 11:47
728x90

 

 빌더 패턴은(Build pattern)은 생성이 까다로운 객체를 쉽게 처리하기 위한 패턴이다. 생성자 호출 코드 단 한 줄로 생성할 수 없는 객체를 다룬다. 그러한 타입의 객체들은 다른 객체들의 조합으로 구성되거나, 상식적인 것을 벗어난 까다로운 로직을 필요로 한다. 이러한 객체를 생성하는 코드는 따로 분리되어 관리될 필요가 있다.

 

예제

 웹 페이지를 그리기 위한 컴포넌트들을 생성해야 한다고 하자. 먼저, 단순하게 단어를 나타내는 항목 두 개("hello"와 "world")를 html의 비 순차("<ul>") 리스트 ("<li>") 태그로 출력해보자. 가장 단순하게 구현한다면 다음과 같이 할 수 있다.​

string words[] = {"hello", "world"};
osstringstream oss;
oss << "<ul>";

for(auto w : words)
{
oss << "  <li>" << w << "</li>";
oss << "</ul>";
}

print(oss.str().c_str());

 

 이 코드는 목적한 대로 출력을 하기는 한다. 하지만 융통성은 없다. 항목마다 앞에 점을 찍거나 순서대로 번호를 매겨야 한다면 이 코드를 어떻게 수정해야 할까? 새로운 항목을 리스트의 끝에 추가해야 한다면 코드를 어떻게 수정해야 할까? 이렇게 고정된 형태로는 손쉽게 대응하기 어렵다.

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

// 선언부
struct HtmlElement
{
	std::string name;
	std::string text;

	std::vector<HtmlElement> elements;

	HtmlElement() {}
	HtmlElement(const std::string& name, const std::string& text)
		: name(name)
		, text(text) {}

	std::string str(int indent = 0) const 
	{
		std::string s;
		for (auto e : elements) {
			s += e.name;
			s += " tag : ";
			s += e.text;
			s += "\n";
		}
		return s;
	}
};

// USAGE
string words[] = {"hello", "world"};
HtmlElement list{"ul", ""};

for (auto w : words)
	list.elements.emplace_back{HtmlElement{"li", w}};

print(list.str().c_str());

 위 코드는 OOP에 기반하여 항목 리스트를 표현한다. 양식 제어하기가 좀 더 쉬우면서도 목적하는 출력을 할 수 있다. 하지만 각각의 HtmlElement를 생성하는 작업이 편리하지 않다. 이 부분을 빌더 패턴을 활용해보자.

 

Simle Builder

 HtmlBuilder는 HTML 구성 요소의 생성만을 전담하는 클래스이다. add_child() 메서드는 현재 요소에 자식 요소를 추가하는 목적으로 사용된다. 각자식 요소는 이름/텍스트 쌍을 가진다. 이 클래스는 아래와 같이 이용된다.

struct HtmlBuilder
{
	HtmlElement root;

	HtmlBuilder(std::string root_name) { root.name = root_name; }

	void add_child(std::string child_name, std::string child_text) {
		HtmlElement e{ child_name, child_text };
		root.elements.emplace_back(e);
	}

	std::string str() { 
		return root.str(); 
	}
};

 

add_child() 메서드는 리턴 값은 사용되는 곳 없이 void 선언되어 있다. 리턴 값을 활용하면 좀 더 편리한 흐름식 인터페이스(fluent interface) 스타일의 빌더를 만들 수 있다.

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

 

Fluent Builder

다음 과같이 빌더 자기 자신을 리턴하도록 add_child()의 정의를 수정해보자.

struct HtmlFluentBuilder
{
	HtmlElement root;

	HtmlFluentBuilder(std::string root_name) { root.name = root_name; }

	HtmlFluentBuilder& add_child(std::string child_name, std::string child_text) {
		HtmlElement e{ child_name, child_text };
		root.elements.emplace_back(e);
		return *this;
	}

	std::string str() {
		return root.str();
	}
};

 

 빌더 자기 자신이 참조로서 리턴되기 때문에 다음과 같이 메서드들이 꼬리를 무는 호출이 가능해진다. 이러한 형태로 호출하는 것을 흐름식 인터페이스(fluent interface)라고 부른다.

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

 

리턴을 참조로 할지 포인터로 할지는 개발자의 자유이다. 호출 체인에 -> 연산자를 사용하고 싶다면 다음과 같이 add_child()를 수정한다.

HtmlFluentBuilder* add_child(std::string child_name, std::string child_text) {
	HtmlElement e{ child_name, child_text };
	root.elements.emplace_back(e);
	return this;
}

 

그러면 다음과 같이 이용할 수 있다.

HtmlFluentBuilder *builder = new HtmlBuilder("ui");
builder->add_child("li", "hello")->add_child("li", "world");
cout << builder << endl;

 

객체 생성 의도 설정

 HTML 구성 요소의 생성을 전담하는 빌더 클래스를 만들었다. 그런데 사용자가 빌더 클래스를 사용해야 한다는 것을 어떻게 알 수 있을까? 한 가지 방법은 빌더를 사용 안 하면 객체 생성이 불가능하도록 강제하는 것이다.

struct HtmlElement
{
	std::string name;
	std::string text;

	std::vector<HtmlElement> elements;

	static std::unique_ptr<HtmlElement> buildd(const std::string& root_name)
	{
		return std::make_unique<HtmlElement>(root_name);
	}

protected: // 모든 생성자 숨기기
	HtmlElement() {}
	HtmlElement(const std::string& name, const std::string& text)
		: name(name)
		, text(text) {}

	std::string str(int indent = 0) const 
	{
		std::string s;
		for (auto e : elements) {
			s += e.name;
			s += " tag : ";
			s += e.text;
			s += "\n";
		}
		return s;
	}
};

 

흐름식 빌더(Fluent Build) 예시 2

riptutorial.com/cplusplus/example/30166/builder-pattern-with-fluent-api

 

C++ - Builder Pattern with Fluent API | c++ Tutorial

c++ documentation: Builder Pattern with Fluent API

riptutorial.com

 

필자가 주로 사용하는 빌더 패턴 코드이다. 위 사이트의 코드를 참고한다.

#pragma once
#include <iostream>
#include <sstream>
#include <string>

using namespace std;

// Forward declaring the builder
class EmailBuilder;

class Email
{
public:
    friend class EmailBuilder;  // the builder can access Email's privates

    static EmailBuilder make();

    string to_string() const {
        stringstream stream;
        stream << "from: " << m_from
            << "\nto: " << m_to
            << "\nsubject: " << m_subject
            << "\nbody: " << m_body;
        return stream.str();
    }

private:
    Email() = default; // restrict construction to builder

    string m_from;
    string m_to;
    string m_subject;
    string m_body;
};

class EmailBuilder
{
public:
    EmailBuilder& from(const string& from) {
        m_email.m_from = from;
        return *this;
    }

    EmailBuilder& to(const string& to) {
        m_email.m_to = to;
        return *this;
    }

    EmailBuilder& subject(const string& subject) {
        m_email.m_subject = subject;
        return *this;
    }

    EmailBuilder& body(const string& body) {
        m_email.m_body = body;
        return *this;
    }

    operator Email && () {
        return std::move(m_email); // notice the move
    }

private:
    Email m_email;
};

EmailBuilder Email::make()
{
    return EmailBuilder();
}

// Bonus example!
std::ostream& operator <<(std::ostream& stream, const Email& email)
{
    stream << email.to_string();
    return stream;
}

 

실제 코드를 사용할때는 아래와 같이 사용할 수 있다.

#include <iostream>
using namespace std;

#include "EmailBuilder.h"

int main()
{
	// 흐름식 빌더
    Email mail = Email::make().from("me@mail.com")
        .to("you@mail.com")
        .subject("C++ builders")
        .body("I like this API, don't you?");

    cout << mail << endl;
	return 0;
}

 

Discussion

빌더 패턴은 안드로이드(Java)에서 매우 편하게 사용하였었는데 C++에서는 조금 구현에 애매한 점이 있었다. 개인적으로 C++에서 주로 흐름식(Feunt) 빌더 패턴을 선호한다.

728x90
반응형