C++ - 빌더 패턴 (Builder Pattern)
Architecture & Design Pattern 전체 링크
참고
빌더 패턴 (Builder Pattern) - 생성 패턴
- 객체 생성 과정을 추상화하여 복잡한 객체를 조립하는 패턴
- 객체를 생성하는 방법을 클라이언트로부터 숨기고, 생성 과정을 단계적으로 나눈다.
구현
- Builder : 객체를 생성하기 위한 인터페이스를 정의, 객체의 각 부분을 생성하기 위한 메서드가 포함된다.
- ConcreteBuilder : Builder 인터페이스를 구현하여 실제로 객체를 생성한다.
- Director : 객체를 생성하는 방법을 결정하고, Builder를 사용하여 객체를 조립한다.
- Product : Builder를 통해 생성되는 최종 객체, 빌더 패턴을 통해 조립되어 만들어진다.
장점
- SRP(단일 책임 원칙), 제품 생성 코드를 분리할 수 있다.
- 객체의 생성 프로세스가 명확하게 표현되어 가독성이 향상된다.
- Director 클래스가 객체 생성 과정을 제어하기 때문에, 객체의 구성을 변경하거나 다양한 방식으로 조립할 수 있다.
단점
- 클래스의 수가 증가한다.
- 객체 생성이 단순한 경우, 빌더 패턴을 적용하면 오버헤드가 증가한다.
- 팩토리 메서드보다 클라이언트가 객체에 대해 더 많이 알아야 한다.
데스크탑과 노트북
컴퓨터를 만들기 위해서는 CPU, RAM, 그래픽 카드 등의 스펙을 결정해야 한다.
class Computer
{
public:
Computer(string c, int r, string gc) : cpu(c), ram(r), graphicsCard(gc) {}
void info()
{
cout << " CPU : " << cpu << endl;
cout << " RAM : " << ram << endl;
cout << "Graphics Card : " << graphicsCard << endl;
}
private:
string cpu;
int ram;
string graphicsCard;
};
데스크탑이나 노트북을 만들려면, 클라이언트가 일일이 스펙을 알아야 한다.
Computer* desktop = new Computer("Intel Core i7", 16, "NVIDIA GeForce RTX 3080");
Computer* laptop = new Computer("AMD Ryzen 9", 32, "NVIDIA GeForce RTX 3070");
예시 코드는 다음과 같다.
#include <iostream>
#include <string>
using namespace std;
class Computer
{
public:
Computer(string c, int r, string gc) : cpu(c), ram(r), graphicsCard(gc) {}
void info()
{
cout << " CPU : " << cpu << endl;
cout << " RAM : " << ram << endl;
cout << "Graphics Card : " << graphicsCard << endl;
}
private:
string cpu;
int ram;
string graphicsCard;
};
int main()
{
Computer* desktop = new Computer("Intel Core i7", 16, "NVIDIA GeForce RTX 3080");
cout << "Desktop Computer Info :" << endl;
desktop->info();
cout << endl;
Computer* laptop = new Computer("AMD Ryzen 9", 32, "NVIDIA GeForce RTX 3070");
cout << "Laptop Computer Info :" << endl;
laptop->info();
return 0;
}
빌더 패턴 적용
빌더 패턴은 객체를 만드는 일을 캡슐화한다.
빌더 인터페이스에서 공통적인 제품 생성 단계를 정의하고, 실제 ConcreteBuilder가 구현한다.
빌더가 구현을 제공한다면, 디렉터가 빌더 단계 순서를 정의할 수 있다.
Computer 클래스에서 생성자는 삭제하고, set 메서드를 이용해 스펙을 정의하도록 하자.
class Computer
{
public:
void setCPU(const string& c) { cpu = c; }
void setRAM(int r) { ram = r; }
void setGraphicsCard(const string& gc) { graphicsCard = gc; }
void info()
{
cout << " CPU : " << cpu << endl;
cout << " RAM : " << ram << endl;
cout << "Graphics Card : " << graphicsCard << endl;
}
private:
string cpu;
int ram;
string graphicsCard;
};
빌더 인터페이스는 각 스펙을 build하도록 정의하고, 제품을 return하는 메서드도 제공하도록 하였다.
class ComputerBuilder
{
public:
virtual ~ComputerBuilder() { }
virtual void buildCPU() = 0;
virtual void buildRAM() = 0;
virtual void buildGraphicsCard() = 0;
virtual Computer* getProduct() = 0;
};
Desktop을 만드는 ConcreteBuilder는 아래와 같이 구현할 수 있다.
class DesktopComputerBuilder : public ComputerBuilder
{
public:
DesktopComputerBuilder() : computer(new Computer()) {}
void buildCPU() override { computer->setCPU("Intel Core i7"); }
void buildRAM() override { computer->setRAM(16); }
void buildGraphicsCard() override { computer->setGraphicsCard("NVIDIA GeForce RTX 3080"); }
Computer* getProduct() override { return computer; }
private:
Computer* computer;
};
디렉터는 특정 빌더(데스크탑이나 노트북)와 구성 관계가 된다.
선택된 빌더를 이용해 컴퓨터의 스펙을 설정하고 제품을 제공한다.
class ComputerDirector
{
public:
ComputerDirector(ComputerBuilder* builder) : builder(builder) {}
void setBuilder(ComputerBuilder* cb) { builder = cb; }
Computer* build()
{
builder->buildCPU();
builder->buildRAM();
builder->buildGraphicsCard();
return builder->getProduct();
}
private:
ComputerBuilder* builder;
};
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
using namespace std;
class Computer
{
public:
void setCPU(const string& c) { cpu = c; }
void setRAM(int r) { ram = r; }
void setGraphicsCard(const string& gc) { graphicsCard = gc; }
void info()
{
cout << " CPU : " << cpu << endl;
cout << " RAM : " << ram << endl;
cout << "Graphics Card : " << graphicsCard << endl;
}
private:
string cpu;
int ram;
string graphicsCard;
};
// Interface
class ComputerBuilder
{
public:
virtual ~ComputerBuilder() { }
virtual void buildCPU() = 0;
virtual void buildRAM() = 0;
virtual void buildGraphicsCard() = 0;
virtual Computer* getProduct() = 0;
};
class DesktopComputerBuilder : public ComputerBuilder
{
public:
DesktopComputerBuilder() : computer(new Computer()) {}
void buildCPU() override { computer->setCPU("Intel Core i7"); }
void buildRAM() override { computer->setRAM(16); }
void buildGraphicsCard() override { computer->setGraphicsCard("NVIDIA GeForce RTX 3080"); }
Computer* getProduct() override { return computer;}
private:
Computer* computer;
};
class LaptopComputerBuilder : public ComputerBuilder
{
public:
LaptopComputerBuilder() : computer(new Computer()) {}
void buildCPU() override { computer->setCPU("AMD Ryzen 9"); }
void buildRAM() override { computer->setRAM(32); }
void buildGraphicsCard() override { computer->setGraphicsCard("NVIDIA GeForce RTX 3070"); }
Computer* getProduct() override { return computer; }
private:
Computer* computer;
};
class ComputerDirector
{
public:
ComputerDirector(ComputerBuilder* builder) : builder(builder) {}
void setBuilder(ComputerBuilder* cb) { builder = cb; }
Computer* build()
{
builder->buildCPU();
builder->buildRAM();
builder->buildGraphicsCard();
return builder->getProduct();
}
private:
ComputerBuilder* builder;
};
int main()
{
DesktopComputerBuilder* desktopBuilder = new DesktopComputerBuilder();
ComputerDirector* director = new ComputerDirector(desktopBuilder);
Computer* desktop = director->build();
cout << "Desktop Computer Info :" << endl;
desktop->info();
cout << endl;
LaptopComputerBuilder* laptopBuilder = new LaptopComputerBuilder();
director->setBuilder(laptopBuilder);
Computer* laptop = director->build();
cout << "Laptop Computer Info :" << endl;
laptop->info();
return 0;
}
컴포지트 빌더 (Composite Builder)
위의 예시를 두 개의 빌더(복수의 빌더)로 분리해 보자. (CPU/RAM 빌더 + 그래픽카드 빌더)
그리고 아래와 같이 빌더를 사용해 보자.
Computer desktop = ComputerBuilder::create()
.equip().equipCPU("Intel Core i7").equipRAM(16)
.setup().setGraphicsCard("NVIDIA GeForce RTX 3080");
컴퓨터 클래스는 get/set을 제공하고 이동 생성자를 이용해서 객체의 소유권을 이전하도록 하자.
class Computer
{
public:
Computer() = default;
Computer(Computer&& other)
: cpu(move(other.cpu)), ram(move(other.ram)), graphicsCard(move(other.graphicsCard)) {}
Computer& operator=(Computer&& other)
{
if (this == &other) return *this;
cpu = move(other.cpu);
ram = move(other.ram);
graphicsCard = move(other.graphicsCard);
return *this;
}
void equipCPU(const string& c) { cpu = c; }
void equipRAM(int r) { ram = r; }
void setGraphicsCard(const string& gc) { graphicsCard = gc; }
string getCPU() { return cpu; }
int getRAM() { return ram; }
string getGraphicsCard() { return graphicsCard; }
private:
string cpu;
int ram;
string graphicsCard;
};
ComputerBuilderBase에서 자식 빌더의 인터페이스를 가져온다. (equip, setup)
그리고 computer를 참조하기만 할 뿐, 실제 객체를 가지지는 않는다.
class ComputerBuilderBase
{
public:
operator Computer() const { return move(computer); }
ComputerCPURAMBuilder equip() const;
ComputerGraphicsCardBuilder setup() const;
protected:
Computer& computer;
ComputerBuilderBase(Computer& c) : computer(c) {}
};
실제 객체는 ComputerBuilder가 가진다.
class ComputerBuilder : public ComputerBuilderBase
{
public:
ComputerBuilder() : ComputerBuilderBase(c) {}
static ComputerBuilder create() { return ComputerBuilder(); }
private:
Computer c; // 생성 중인 객체
};
그리고 실제 객체의 멤버를 설정하는 빌더에서 컴퓨터의 정보를 설정한다.
class ComputerCPURAMBuilder : public ComputerBuilderBase
{
public:
ComputerCPURAMBuilder(Computer& c) : ComputerBuilderBase(c) {}
ComputerCPURAMBuilder& equipCPU(const string cpu)
{
computer.equipCPU(cpu);
return *this;
}
ComputerCPURAMBuilder& equipRAM(int ram)
{
computer.equipRAM(ram);
return *this;
}
};
ComputerBuilder에서 create로 객체 생성을 시작하고, equip과 setup으로 하위 빌더의 인터페이스를 얻는다.
그리고 각 인터페이스에서 값을 설정할 수 있다.
Computer desktop = ComputerBuilder::create()
.equip().equipCPU("Intel Core i7").equipRAM(16)
.setup().setGraphicsCard("NVIDIA GeForce RTX 3080");
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
using namespace std;
class Computer
{
public:
Computer() = default;
Computer(Computer&& other)
: cpu(move(other.cpu)), ram(move(other.ram)), graphicsCard(move(other.graphicsCard)) {}
Computer& operator=(Computer&& other)
{
if (this == &other) return *this;
cpu = move(other.cpu);
ram = move(other.ram);
graphicsCard = move(other.graphicsCard);
return *this;
}
void equipCPU(const string& c) { cpu = c; }
void equipRAM(int r) { ram = r; }
void setGraphicsCard(const string& gc) { graphicsCard = gc; }
string getCPU() { return cpu; }
int getRAM() { return ram; }
string getGraphicsCard() { return graphicsCard; }
private:
string cpu;
int ram;
string graphicsCard;
};
class ComputerCPURAMBuilder;
class ComputerGraphicsCardBuilder;
class ComputerBuilderBase
{
public:
operator Computer() const { return move(computer); }
ComputerCPURAMBuilder equip() const;
ComputerGraphicsCardBuilder setup() const;
protected:
Computer& computer;
ComputerBuilderBase(Computer& c) : computer(c) {}
};
class ComputerBuilder : public ComputerBuilderBase
{
public:
ComputerBuilder() : ComputerBuilderBase(c) {}
static ComputerBuilder create() { return ComputerBuilder(); }
private:
Computer c; // 생성 중인 객체
};
class ComputerCPURAMBuilder : public ComputerBuilderBase
{
public:
ComputerCPURAMBuilder(Computer& c) : ComputerBuilderBase(c) {}
ComputerCPURAMBuilder& equipCPU(const string cpu)
{
computer.equipCPU(cpu);
return *this;
}
ComputerCPURAMBuilder& equipRAM(int ram)
{
computer.equipRAM(ram);
return *this;
}
};
class ComputerGraphicsCardBuilder : public ComputerBuilderBase
{
public:
ComputerGraphicsCardBuilder(Computer& c) : ComputerBuilderBase(c) {}
ComputerGraphicsCardBuilder& setGraphicsCard(const string gc)
{
computer.setGraphicsCard(gc);
return *this;
}
};
ComputerCPURAMBuilder ComputerBuilderBase::equip() const
{
return ComputerCPURAMBuilder(computer);
}
ComputerGraphicsCardBuilder ComputerBuilderBase::setup() const
{
return ComputerGraphicsCardBuilder(computer);
}
void printInfo(Computer& c)
{
cout << " CPU : " << c.getCPU() << endl;
cout << " RAM : " << c.getRAM() << endl;
cout << "Graphics Card : " << c.getGraphicsCard() << endl;
}
int main()
{
Computer desktop = ComputerBuilder::create()
.equip().equipCPU("Intel Core i7").equipRAM(16)
.setup().setGraphicsCard("NVIDIA GeForce RTX 3080");
cout << "Desktop Computer Info :" << endl;
printInfo(desktop);
cout << endl;
Computer laptop = ComputerBuilder::create()
.equip().equipCPU("AMD Ryzen 9").equipRAM(32)
.setup().setGraphicsCard("NVIDIA GeForce RTX 3070");
cout << "Laptop Computer Info :" << endl;
printInfo(laptop);
return 0;
}
ClassDiagram.md
```mermaid
classDiagram
class Director {
build()
setBuilder()
}
class Builder {
buildA()*
buildB()*
getProduct()*
}
class ConcreteBuilderA {
buildA()
buildB()
getProduct()
}
class ConcreteBuilderB {
buildA()
buildB()
getProduct()
}
class ProductA {
}
class ProductB {
}
<<interface>> Builder
Director --> Builder
Builder <|.. ConcreteBuilderA
Builder <|.. ConcreteBuilderB
ConcreteBuilderA --> ProductA
ConcreteBuilderB --> ProductB
```