생성자에 매개변수가 많다면 빌더를 고려하라
선택적 매개변수가 많아진다면, 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, ... 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식으로 점층적 생성자 패턴 방식을 사용했다. 이 방식을 사용한다면 다음과 같은 단점이 존재한다.
(1) 각 값의 의미가 무엇인지 헷갈린다.
(2) 매개변수가 몇 개인지도 주의해서 세어보아야 한다.
(3) 순서가 바뀌더라도 컴파일러가 알아채지 못해 런타임 상에서 예상치 못한 결과를 얻을 수 있다.
또 다른 방식으로는 자바빈즈 패턴이 존재한다. 이 방식은 매개변수가 없는 생성자로 객체를 만든 후, Setter를 호출해 각 값을 설정하는 방식이다. 마찬가지로 이 방식을 사용한다면 아래와 같은 단점이 존재한다.
(1) 객체 하나를 만들기 위해 Setter를 여러 개 호출해야 한다.
(2) 객체가 완전히 생성되기 전까진 일관성이 무너진 상태에 놓이게 된다.
따라서 이러한 단점들을 보완하기 위해 빌더 패턴이 존재한다.
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그 후, 빌더 객체가 제공하는 Setter로 원하는 선택 매개 변수를 설정한 후 build 메서디를 호출해 객체를 생성하도록 한다.
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값 초기화
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
// build() 호출로 최종 불변 객체를 얻는다.
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
클라이언트 코드는 다음과 같다.
NutritionFacts cocaCola = new NutriFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(30).build();
Builder 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. self라는 추상 메서드를 더해 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.
public abstract class Pizza{
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 overriding하여
// this를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
//클라이언트 코드
NYPizza pizza = new NYPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
따라서 형 변환에 신경쓰지 않고 빌더를 사용할 수 있게 해준다.
단, Builder 패턴은 성능에 민감한 상황에 문제가 될 수 있으므로 사용한다면 매개 변수가 4개 이상은 되어야 값어치를 한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있어 애초에 빌더로 시작하는 편이 나을 때가 많다.