정의
복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴.
상황
추상 팩토리 에서 다뤘던 상황을 다시 확인해보자.
PrintStar 클래스에서 map을 생성하고 출력하는 프로그램을 작성했었다.
별의 종류를 늘리는 경우, 이를 하드코딩 하는것을 방지하기 위해 팩토리 객체를 선언하여 해결했다.
이것으로 만족할 수도 있지만, 생각해보면 PrintStar가 하는일이 매우 많다.
map을 선언하고 팩토리를 통해 특정 좌표에 별을 생성한 다음, 나머지 공간을 공백으로 채운다.
PrintStar는 위 과정에 대한 책임을 모두 떠안게 된다.
PrintStar가 map을 꾸미고 출력하는 클래스이긴 하지만, map이라는 객체의 생성까지 떠안는 것은 무겁다고 할 수 있다.
어떻게하면 map의 생성을 분리할 수 있을까?
솔루션
빌더(Builder) 패턴은 위와 같은 문제를 해결하기에 적합한 패턴이다.
위 상황을 요약하자면, map이라는 복합객체(Star, Blank 등이 섞인)의 생성을 PrintStar로부터 분리하는 것이다.
빌더 패턴에서는 이런 복합 객체를 생성하는 클래스를 제공하여 그 책임을 사용자로부터 분리시켜준다.
코드를 확인해보자.
class Item
{
public virtual void print() { }
}
class Star : Item
{
public override void print() { Console.Write("*"); }
}
class Blank : Item
{
public override void print() { Console.Write(" "); }
}
class MoneyStar : Star
{
public override void print() { Console.Write("$"); }
}
class StarBuilder
{
protected Item[,] map;
public virtual StarBuilder setMap(int mapSize)
{
map = new Item[mapSize, mapSize];
return this;
}
public virtual StarBuilder setStar(int y, int x)
{
map[y, x] = new Star();
return this;
}
public virtual StarBuilder setEmptyToBlank()
{
for(int i=0; i < map.GetLength(0); i++)
for(int j=0; j < map.GetLength(1); j++)
if(map[i, j] == null)
map[i, j] = new Blank();
return this;
}
public virtual Item[,] getMap()
{
return map;
}
}
class MoneyStarBuilder : StarBuilder
{
public override StarBuilder setStar(int y, int x)
{
map[y, x] = new MoneyStar();
return this;
}
}
class PrintStar
{
const int MapSize = 10;
private Item[,] map;
public PrintStar(StarBuilder builder)
{
builder.setMap(MapSize)
.setStar(3, 3)
.setStar(4, 2)
.setStar(4, 4)
.setStar(5, 3)
.setEmptyToBlank();
map = builder.getMap();
}
public void print()
{
for (int i = 0; i < MapSize; i++)
{
for (int j = 0; j < MapSize; j++)
map[i, j].print();
Console.WriteLine();
}
}
}
추상 팩토리와 다른점은 다음과 같다.
- 추상 팩토리에서는 Star, Blank 등의 객체의 생성을 추상화 하였다. 즉, 각각의 객체들이 개별적으로 의미를 가진다.
- 빌더에서는 map이라는 복합 객체만이 의미를 갖는다. Star, Blank는 어디까지나 복합객체를 수식하는 객체일 뿐이다.
눈여겨 볼 점은 StarBuilder 클래스에서 map을 protected로 정의한 점이다.
private로 정의하면 상속받은 클래스에서 사용이 불가능하고, public으로 정의하면 외부 접근을 허용하므로 protected가 적당하다.
또한 builder의 객체 생성 메소드들의 반환을 this로 지정하여 좀 더 깔끔한 코드를 가능하게 했다.
다음은 main에서의 코드이다.
class Test
{
static void Main(string[] args)
{
PrintStar printStar = new PrintStar(new StarBuilder());
printStar.print();
printStar = new PrintStar(new MoneyStarBuilder());
printStar.print();
}
}
장단점
장점
- 제품에 대한 내부 표현을 다양하게 변화할 수 있다.
- 생성과 표현에 필요한 코드를 분리한다.
- 복합 객체를 생성하는 절차를 좀 더 세밀하게 나눌 수 있다.
- 객체의 생성자에 매개변수가 너무 많아서 사용이 힘든경우, 빌더 패턴을 이용하면 step by step으로 이를 해결할 수 있다.
단점
???
클래스 다이어그램
다이어그램은 추상 팩토리와 매우 흡사하다.
실제로 두 패턴은 구조가 매우 유사하고 중점을 어디에 두는가 차이이기 때문이다.
결론
빌더 패턴은 복합 객체의 생성과정을 추상화 하여 사용자에게서 책임을 분리하는 패턴이다.