[아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라

ds_chanin

·

2020. 1. 16. 10:19


[아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라

점층적 생성자 패턴

  • 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
public class Gradation {
    private String name;
    private int number;
    private long money;
    private double temperature;

    public Gradation(String name, int number, long money, double temperature) {
        this.name = name;
        this.number = number;
        this.money = money;
        this.temperature = temperature;
    }

    public Gradation(String name, int number, long money) {
        this(name, number, money, 0);
    }

    public Gradation(String name, int number) {
        this(name, number, 0, 0);
    }

    public Gradation(String name) {
        this(name, 0, 0, 0);
    }

    public Gradation() {
        this("", 0, 0, 0);
    }
}
생성자가 많아질수록 클라이언트는 코드를 해석하기 힘들어 진다.

Java Beans 패턴

  • 객체 하나를 생성하기 위해 메서드를 여러번 호출해야한다.
  • 객체를 완전히 생성하기 전까지 일관성이 무너진 상태에 놓인다.
    • 이는 클래스를 불변으로 만들 수 없음을 의미한다.
public class JavaBeans {
    private String name;
    private int number;
    private long money;
    private double temperature;

    public void setName(String name) {
        this.name = name;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public void setMoney(long money) {
        this.money = money;
    }

    public void setTemperature(double temperature) {
        this.temperature = temperature;
    }
}

class Beans {
    public JavaBeans create() {
        JavaBeans javaBeans = new JavaBeans();

        javaBeans.setName("name");
        javaBeans.setNumber(0);
        javaBeans.setMoney(0);
        javaBeans.setTemperature(0);

        return javaBeans;
    }
}
create 메서드를 보면 단점이 드러난다.

기본적인 Builder 패턴

public class Basic {
    private String name;
    private double height;
    private double weight;
    private int age;

    public static class Builder {
        private final String name; // final 예약어를 붙이게 된다면 필수 매개변수가 된다.
        private double height;
        private double weight;
        private int age;

        public Builder(String name) {
            this.name = name;
        }

        public Builder height(double height) {
            this.height = height;
            return this;
        }

        public Builder weight(double weight) {
            this.weight = weight;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

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

    private Basic(Builder builder) {
        this.name = builder.name;
        this.height = builder.height;
        this.weight = builder.weight;
        this.age = builder.age;
    }
}

계층적으로 설계된 클래스의 Builder 패턴

public abstract class Hamburger {
    public enum Patty {PORK, BEEF, CHICKEN, FISH;}

    private final Set<Patty> patties;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Patty> patties = EnumSet.noneOf(Patty.class);

        public T addPatty(Patty patty) {
            patties.add(patty);
            return self();
        }

        abstract Hamburger build();

        protected abstract T self(); // 자식 클래스에서 형변환 없이 메서드 연쇄를 지원한다.
    }

    Hamburger(Builder<?> builder) {
        patties = builder.patties.clone();
    }
}

class McDonaldsHamBurger extends Hamburger {
    private final boolean sauceInside;

    public static class Builder extends Hamburger.Builder {
        private final boolean sauceInside;

        private Builder(boolean sauceInside) {
            this.sauceInside = sauceInside;
        }

        @Override
        public McDonaldsHamBurger build() { // 구체 클래스를 반환하지 않고 구체의 하위 클래스를 반환함으로써 형변환에 신경쓰지 않아도 된다. 이를 공변 변환 타이핑이라 한다.
            return new McDonaldsHamBurger(this);
        }

        @Override
        protected Builder self() {
            return this; // this 를 반환해야 메서드 연쇄가 가능해진다.
        }
    }

    public static Builder builder(boolean sauceInside){
        return new Builder(sauceInside);
    }

    McDonaldsHamBurger(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

class HamburgerStore {
    public Hamburger make(){

        Hamburger hamburger = McDonaldsHamBurger.builder(true)
                .addPatty(Hamburger.Patty.BEEF)
                .addPatty(Hamburger.Patty.CHICKEN)
                .build();

        return hamburger;
    }
}

Builder 패턴은 위에 살펴본 기존의 패턴들 보다 유연하다는 장점을 가지고 있지만, Builder를 사용하기 위해 장황한 코드를 구현해야 한다는 단점 또한 존재한다.
따라서 Builder 패턴은 매개변수가 4개가 넘어가는 시점에서 그 장점을 발휘한다고 한다.
그러나 보통 클래스는 매개변수가 늘어가는 성향이 있으므로 처음부터 Builder를 구현해 놓고 사용해도 좋을 듯 하다.