식품 포장의 영양정보를 표현하는 클래스를 작성한다고 하면 지방, 트랜스지방, 포화지방, 나트륨, 콜레스테롤...등등등 20개가 넘는 선택 항목으로 이뤄진다.
이런 것을 아래와 같은 패턴을 사용한다면..
private final
private
cocaCola.setCalories(100);
- 요런것을 10성분이면 10줄 작성해야함😩다행히 우리는 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴을 쓸 수 있어용🌝
//빌더 패턴을 사용한 NutritionFacts 클래스
public class NutritionFacts {
//private final -> 불변 클래스를 만들기 위함이쥬?
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder { //빌더 클래스입니다
// 필수 매개변수 //아래 두값은 빌더 생성자 호출시 한번만 set하기 때문에 private final
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;
}
//필요시에만 함수 호출하여 정보 넣기
//빌더 set 함수들은 빌더 본인을 반환하기 때문에 연쇄적으로 호출이 가능하다 calories().fat() 요런식으루!
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 함수 딱 호출하면 NutritionFacts 클래스 private 생성자 호출가능!
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
//요것이 실제로 우리에게 필요한 객체를 만들수 있는 생성자
//왼쪽값이 빌더 클래스를 감싸고 있는 NutritionFacts의 매개변수임
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
(세가지 방식 모두 클래스 길이가 길긴 매한가지지만 클라이언트에서 연쇄적으로 값을 설정할 수 있는 빌더 패턴을 사용하는 것이 사용성에서는 훨씬 좋아 보인다)
위 예제는 성분 하나만을 위해서 작성된 빌더 패턴이지만 사실 빌더 패턴은
계층적이라 하면 여러개의 종류의 하위 클래스가 존재하는 추상 클래스같은 것이쥬
//계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴을 사용한 Pizza 추상 클래스
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } //필요한 정보만을 추상 클래스에 작성
final Set<Topping> toppings;
//재귀적 타입 한정(item-30)을 이용하는 제네릭 타입
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"를 반환하도록 해야 한다. ==> 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.
//==> self 타입이 없는 자바를 위한 이 우회 방법이 simulated self-type 관용구
protected abstract T self();
}
//item-50 참조
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
해당 피자 추상 클래스의 하위클래스에는 size를 필수로 받는 뉴욕 피자와 소스를 안에 넣을지 선택하는 칼초네 피자 두개가 있다.
//size를 필수로 받는 뉴욕 피자
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); }
//구체하위클래스인 NyPizza 반환하도록 선언(공변 반환 타이핑)
@Override public NyPizza build() {
return new NyPizza(this);
}
//뉴욕피자 본인 반환
@Override protected Builder self() { return this; }
}
//private 생성자 - build함수에서 호출됨
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
//왼쪽은 Pizza의 size임
}
//toString 오버라이드 하여 원하는 문자열 출력 가능
@Override public String toString() {
return toppings + "로 토핑한 뉴욕 피자";
}
}
private final
임도 확인 가능하다. NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
//소스를 안에 넣을지 선택하는 칼초네 피자
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // 기본값 - 소스를 안에 넣을거니?
public Builder sauceInside() { //이 함수를 호출해야만 소스를 안에 넣게끔 변경 가능
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
//toString 오버라이드 하여 원하는 문자열 출력 가능
@Override public String toString() {
return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
toppings, sauceInside ? "안" : "바깥");
}
}
private
임도 확인 가능하다.Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
T<A>
가 T<B>
의 하위타입이면 T가 공변성질을 가졌다고 한다고 한다.생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.