객체 생성과 파괴 item02

이팩티브 자바 3판을 읽고 정리하는 글입니다

생성자에 매개변수가 많다면 빌더를 고려하라

static 팩터리와 생성자 둘다 매개변수가 많이 필요한 경우에 불편해지는 문제가 있다.
해당 클래스는 몇몇 반드시 필요한 필드와 선택적인 필드를 가질 수 있는데, 그런 경우에 필수적인 매개변수를 가진 생성자에 부가적인 필드를 하나씩 추가하며 생성자를 추가로 생성한다.

해결책

1. 점층적 생성자 패턴

public class NutritionFacts{
	private final int servingSize;
    ...
    
    public NutritionFacts(int servingSize){};
    public NutritionFacts(int servingSize,int newVal){};
    ...
}

NutritionFacts cocaCola=new NutritionFacts(240,9,100,0,35,27);

이런 생성자를 쓰다보면 필요없는 매개변수도 넘겨야하는 경우가 발생하고 이런 코드는 작성하기도 어렵고 읽기도 힘들다.

2. 자바빈즈 패턴

아무런 매개변수를 받지 않는 생성자를 사용해서 인스턴스를 만들고 setter를 사용해서 필요한 필드만 설정하는 방법

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

점층적 생성자 패턴의 단점들이 자바빈즈 패턴에는 더이상 보이지 않지만 코드가 길어졌다.
최종적인 인스턴스를 만들기까지 여러번의 호출을 거쳐야 하기 때문에 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다. 즉 일관성이 무너진 상태에 놓일 수 있다. 그래서 클래스를 불변으로 만들 수 없다.

위의 단점을 해결하기 위해 객체를 수동으로 freezing?? 이해가안된다.

3. 빌더 패턴

점층적 생성자 패턴과 자바빈즈 패턴의 장점만을 취함
만들려는 객체를 바로 만들지 않고 클라이언트가 필요한 객체를 직접 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그런 다음 setter를로 원하는 매개변수를 설정한다.

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;
    
  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; }

          public NutritionFacts build() {
              return new NutritionFacts(this);
          }
      }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

//클라이언트 코드
public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }

위의 클래스는 불변이며 모든 매개변수의 기본값들을 한곳에 모았다. 빌더의 생성자나 메소드에서 유효성 확인을 할 수 있고 여러 매개변수를 혼합해서 확인해야 하는 경우에는 build메서드에서 호출하는 생성자에서 할 수 있다.
-> 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사 검증에 실패하면IllegalArgumentException을 던지고 에러메시지로 자세한 내용을 알려준다.
계층적으로 설계된 클래스와 함께 쓰기 좋다
추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서는 추상 클래스를 상속받으며 각 하위 클래스용 빌더도 추상 빌더를 상속받아 만들 수 있다.

// 참고: 여기서 사용한 '시뮬레이트한 셀프 타입(simulated self-type)' 관용구는
// 빌더뿐 아니라 임의의 유동적인 계층구조를 허용한다.

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"를 반환하도록 해야 한다.
        //self-type 개념을 사용해서 메서드 체이닝을 사용
        protected abstract T self(); 
    }
    
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // 아이템 50 참조
    }
}

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;
    }
}
public class Calzone extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauseInside = false;

        public Builder sauceInde() {
            sauseInside = 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.sauseInside;
    }

}

pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제너릭 타입이다.
self를 이용해서 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(Covariant 리턴 타이핑)을 사용
-> 클라이언트 코드에서 타입 캐스팅을 할 필요가 없어진다.

NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.ONION)
    .build();

Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInde()
    .build();

빌더를 이용하면 가변인수 매개변수를 여러개 사용할 수 있다. addTopping 처럼 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 있다.

단점 : 객체를 만들려면 그에 앞서 빌더부터 만들어야 된다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될수 있다. 매개변수 4개 이상에서 좋은 효과를 불러 일으킨다.
처음부터 빌더패턴을 이용해서 만드는 것이 효율적일 수도??

좋은 웹페이지 즐겨찾기