개발/Architecture & Design Pattern

C++ - 빌더 패턴 (Builder Pattern)

피로물든딸기 2024. 3. 1. 17:36
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

- 클래스 다이어그램 그리기

- 이동 생성자 move constructor

 

빌더 패턴 (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객체 생성을 시작하고, equipsetup으로 하위 빌더의 인터페이스를 얻는다.

그리고 각 인터페이스에서 값을 설정할 수 있다.

	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     
```
반응형