빌더 패턴은(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를 생성하는 작업이 편리하지 않다. 이 부분을 빌더 패턴을 활용해보자.
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;
다음 과같이 빌더 자기 자신을 리턴하도록 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;
}
};
riptutorial.com/cplusplus/example/30166/builder-pattern-with-fluent-api
필자가 주로 사용하는 빌더 패턴 코드이다. 위 사이트의 코드를 참고한다.
#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;
}
빌더 패턴은 안드로이드(Java)에서 매우 편하게 사용하였었는데 C++에서는 조금 구현에 애매한 점이 있었다. 개인적으로 C++에서 주로 흐름식(Feunt) 빌더 패턴을 선호한다.
RAII idiom (0) | 2021.05.07 |
---|---|
핌플 이디엄 Pimpl idiom (0) | 2021.05.07 |
싱글턴 패턴 SingleTon pattern (0) | 2021.05.07 |
빌더 패턴 사용 이유 (0) | 2021.05.07 |
모노스테이트 패턴 Monostate pattern (0) | 2021.05.07 |
댓글 영역