본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 4. 9.
728x90

* 선택적 매개변수가 많은 경우

  • 점층적 생성자 패턴
    • 필수 매개변수, 필수+선택 1개, 필수+선택n개 ... 형태로 생성자를 늘려가는 방식.
    • 즉, 매개변수를 최소한으로 사용하는 생성자를 만드는 것이다.
    • 사용자가 설정하고자 하지 않는 매개변수에도 값을 지정해 주어야 함. 보통 0을 넘긴다.
    • 클라이언트 코드를 작성하고, 읽는 것이 어렵다.
/* 점층적 생성자 패턴 */
public static class BookGuide1{
    private final int day;
    private final int price;
    private final int publish;
    private final int order;

    public BookGuide1(int day, int price) { // 필수 매개변수
        this(day, price, 0);
    }

    // ... 이런 식으로 2개, 3개, 4개... n개까지 다 만든다.

    public BookGuide1(int day, int price, int publish) { // 필수 + 선택
        this(day, price, publish, 0);
    }

    public BookGuide1(int day, int price, int publish, int order) { // All
        this.day = day;
        this.price = price;
        this.publish = publish;
        this.order = order;
    }
}
  • JavaBeans 패턴
    • 아무 매개변수가 없는 생성자로 객체를 만든 후, setter를 통해 원하는 매개변수의 값을 설정하는 방식.
    • 인스턴스를 만들기 쉽고, 읽기가 쉽다. (점층적 생성자 패턴의 단점을 보완)
    • 객체 하나를 만들기 위해 메서드를 여러 번 호출해야 하기 때문에, 객체가 완성되기 전 까지 javabeans를 중간에 사용하는 경우 안정성, 일관성을 보장할 수 없다. 따라서 locking같은 추가 기능을 따로 구현해야 한다.
    • 따라서 클래스를 불변 클래스로 만들 수 없다.
    • 객체는 반드시 해당 객체의 builder로만 생성할 수 있다.
/* JavaBeans 패턴 */
public static class BookGuide2 {
    private int day = -1;
    private int price = -1;
    private int publish = 0;
    private int order = 0;

    public BookGuide2(){}

    public void setDay(int val){
        day = val;
    }
    public void setPrice(int val){
        price = val;
    }
    public void setPublish(int val){
        publish = val;
    }
    public void setOrder(int val){
        order = val;
    }
}
  • Builder 패턴
    • 클라이언트는 필수 매개변수만으로 생성자(또는 정적 팩토리)를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 setter 메소드로 선택 매개변수를 추가해준 뒤, 최종적으로 builder 메서드(매개변수 없음)를 호출해 불변 객체를 생성한다.
    • 점층적 생성자 패턴의 안정성, 자바빈 패턴의 가독성을 모두 얻은 방법.
/* Builder 패턴 */
public static class BookGuide3 {
    private final int day;
    private final int price;
    private final int publish;
    private final int order;


    public static class Builder{
        //필수 매개변수
        private final int day;
        private final int price;

        //선택 매개변수 (초기화)
        private int publish =0;
        private int order =0;

        //빌더 객체를 사용하기 위해 필수 매개변수 입력
        public Builder(int day, int price){
            this.day = day;
            this.price = price;
        }

        //선택 매개변수 설정
        public Builder publish(int val){
            publish = val;
            return this;
        }
        public Builder order(int val){
            order = val;
            return this;
        }

        //완성 메소드. 매개변수가 없는 빌더 메서드를 호출.
        public BookGuide3 build(){
            return new BookGuide3(this);
        }
    }

    // 드뎌 객체 생성!
    private BookGuide3(Builder builder){
        day = builder.day;
        price = builder.price;
        publish = builder.publish;
        order = builder.order;
    }
}
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 클래스 계층 구조를 잘 활용할 수 있다는 의미!
  • 추상 빌더를 가지고 있는 추상 클래스를 만들고, 하위 클래스에서 추상 클래스를 상속받으면 된다.( 하위 클래스 빌더도 추상 클래스 빌더 상속 가능)

* 추상 빌더를 가지고 있는 추상 클래스 만들기

/** Book.java */

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

public abstract class Book {

    // 모든 하위 타입들의 공통 타입
    public enum Information {
        publish, price
    }

    final Set<Information> infos;

    // 재귀 타입 파라미터를 가진 generic
    abstract static class Builder<T extends  Builder<T>> {
        private Class<Information> Book;
        EnumSet<Information> infos = EnumSet.noneOf(Book);

        //하위 타입의 권한 정의
        public T addInfo(Information information) {
            infos.add(Objects.requireNonNull(information));
            return self();
        }

        abstract Book build();

        //하위 클래스는 자기 자신을 반환하는 메서드를 오버라이드 해야 한다.
        protected abstract T self();
    }

    Book(Builder<?> builder) {
        infos = builder.infos.clone();
    }

    /** main */

    public static void main(String[] args) {

        //클라이언트 코드!
        //원하는 대로 추가할 수 있다.

        
        Magazine magazine = new Magazine.Builder("Vogue")
                .addInfo(Information.price)
                .addInfo(Information.publish).build();

        
        Note note = new Note.Builder("Algorithm", "20220409")
                .addInfo(Information.price)
                .addInfo(Information.publish).build();
        
    }
}

*  하위 클래스에서는 추상 클래스를 상속받고, 하위 클래스용 빌드도 추상 클래스 빌드를 상속 받아 사용한다.

/** Magazine.java */

import java.util.Objects;

public class Magazine extends Book {

    private final String name;

    public static class Builder extends Book.Builder<Builder> {
        private final String name;

        // extend하는 객체가 가지는 instance 변수
        public Builder(String name)
        {
            this.name = Objects.requireNonNull(null);
        }

        // 마지막으로 호출 되는 build 완성 메서드 (오버라이드 해야 함)
        @Override
        Magazine build() {
            return new Magazine(this);
        }

        // 부모 타입에서 필요한 타입 참조
        @Override
        protected Builder self() {
            return this;
        }
    }

    private Magazine(Builder builder){
        super(builder);
        name = builder.name;
    }
}

*  재미있는 상속 받기.

/** Note.java */

public class Note extends Book {
    private final String name;
    private final String date;

    public static class Builder extends Book.Builder<Builder> {
        private String name;
        private String date;

        //필수 속성은 빌더 생성자에서 바로 받는다.
        public Builder(String name, String date) {
            this.name = name;
            this.date = date;
        }

        //최종 빌더
        @Override
        Note build() {
            return new Note(this);
        }

        //부모 타입에서 필요한 메서드
        @Override
        protected Builder self() {
            return null;
        }
    }

    public Note(Builder builder) {
        super(builder);
        name = builder.name;
        date = builder.date;
    }
}

 

728x90

댓글