Study/Effective-Java
[item#02] 정적 팩터리와 생성자에 선택적 매개변수가 많을 때 고려할 수 있는 방안
hongeeii
2023. 11. 27. 17:44
728x90
반응형
대안 1. 생성자 체이닝 또는 점층적 생성자 패턴
package com.example.demo.chaper01.item02;
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 NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
super();
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
위 패턴의 문제점은 코드를 작성하기나 읽기 어렵다.
대안 2. 자바 빈즈 패턴
// 기본값이 있다면 기본값으로 초기화된다.
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
public int getServingSize() {
return servingSize;
}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public int getServings() {
return servings;
}
public void setServings(int servings) {
this.servings = servings;
}
public int getCalories() {
return calories;
}
public void setCalories(int calories) {
this.calories = calories;
}
public int getFat() {
return fat;
}
public void setFat(int fat) {
this.fat = fat;
}
public int getSodium() {
return sodium;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public int getCarbohydrate() {
return carbohydrate;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
public static void main(String[] args) {
NutritionFacts facts = new NutritionFacts();
facts.setServings(240);
}
장점 : 객체 생성이 간단해진다.
단점 :
- 필수적으로 필요로 하는 필수값들이 세팅이 안된상태로 그냥 사용이 될 수 있다. (일관성이 무너진 상태가 될 수 있다.)
- 어디까지 setting을 해주어야 하는지 문서화가 필요하다.
- 불변 객체로 만들 수 없다.
두 가지 방법을 혼용해서 필수적인 필드들은 생성자로 넘겨받도록 강제성을 주고, Optional한 필드들은 setter로 설정을 하도록한다.
이 방법도 여전히 하나의 단점이 있는데, 불변 객체로(immutable) 만들기 어렵다.
불변객체로 만들려면 setter를 사용하지 말아야한다.
왜? 객체의 일관성이 깨지기 때문에 => freezing이라는 기술도 사용할 수 있지만, 실전에서는 거의 쓰이지 않음.
대안 3. 권장하는 방법 : 빌더 패턴
- 플루언트 API 또는 메서드 체이닝을 한다.
- 계층적으로 설계된 클래스와 함께 사용하기 좋다.
- 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
단점:
- 반드시 좋은건 아니다.
- immutable로 만들고 싶을 때가 있을 수 있음
- 코드가 굉장히 많다. (중복 필드 포함) => lombok의 @Builder를 사용하면 Builder를 알아서 만들어줌.
package com.example.demo.chaper01.item02;
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
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) {
// 자바 빈즈는 아니라 set메서드는 아니지만 setter와 동일한 역할
// setter는 return type void
// Builder는 return type 자기 자신
// 이 때문에 Fluent API 또는 메서드 연쇄(method chaining)이 가능
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 facts = new NutritionFacts.Builder(240, 8)
.calories(2)
.carbohydrate(5)
.sodium(23)
.build();
}
}
@Builder를 사용한 예시
장점 : 간결하다.
단점 :
- 모든 파라미터를 받는 생성자가 기본으로 생긴다. (@AllArgsConstructor(access = AccessLevel.PRIVATE))를 사용하면 외부에서는 생성자를 못쓴다.(내부적으로 Builder만 사용가능)
- 필수 값을 지정해 줄 수 없다. => 위 코드를 사용해야함(Builder를 직접 만드는 패턴)
import lombok.Builder;
@Builder(builderClassName = "Builder")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public static void main(String[] args) {
NutritionFacts facts = new NutritionFactsBuilder()
.servingSize(100)
.servings(10)
.build();
// 빌더에 name을 주면 아래와 같이 사용가능
NutritionFacts facts = new NutritionFacts.BuilderNameTest().build();
}
}
!! Annotation Processor
Annotation자체만으론 주석에 가까움.
컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것을 의미.
컴파일 단계에서 실행되기 때문에, 빌드 단계에서 에러를 출력하게 할 수 있고, 소스코드 및 바이트 코드를 생성할 수도 있다.
https://roadj.tistory.com/9
Builder 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
public abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> { // Builder자신의 하위 클래스를 받도록 재귀적인 타입 제한
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
public Builder<T> addTopping2(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
// return self();
// 왜 this를 return 시키면 안되는 가?
// 하위 클래스의 빌더들은 자기자신빌더를 리턴해야한다.
// 하위 타입을 리턴할수 있도록 self()를 호출하도록 한 것.
return this;
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의 하여 this를 반환하도록 해야함.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
addTopping2 메서드에서 return this를 하면 하위 클래스를 build할 때 타입 캐스팅을 해야하므로 불편함.
public class PizzaTest {
public static void main(String[] args) {
NyPizza nyPizza2 = new NyPizza.Builder(NyPizza.Size.SMALL)
.addTopping(Pizza.Topping.HAM)
.addTopping(Pizza.Topping.MUSHROOM).build();
NyPizza nyPizza = (NyPizza) new NyPizza.Builder(NyPizza.Size.SMALL) // 타입 캐스팅해야함.
.addTopping2(Pizza.Topping.HAM)
.addTopping2(Pizza.Topping.MUSHROOM).build();
}
}
self()를 사용하여 형변환을 줄일 수 있고 BuilderFactory 계층 구조를 재활용 할 수 있다라는 걸 책에서 말하고 있다.
728x90
반응형