빌더 패턴을 구현할 때 빌더 클래스가 필요한 이유는 무엇입니까?


31

빌더 패턴 (주로 Java)의 많은 구현을 보았습니다. 그들 모두는 엔티티 클래스 (클래스라고합시다 Person)와 빌더 클래스를 가지고 PersonBuilder있습니다. 빌더는 다양한 필드를 "스택" new Person하고 인수가 전달 된 a 를 리턴합니다 . 모든 빌더 메소드를 Person클래스 자체 에 넣는 대신 명시 적으로 빌더 클래스가 필요한 이유는 무엇 입니까?

예를 들면 다음과 같습니다.

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

간단히 말해 Person john = new Person().withName("John");

PersonBuilder수업이 왜 필요한 가요?

내가 볼 수있는 유일한 이점은 Person필드를로 선언하여 final불변성을 보장 한다는 것 입니다.


4
참고 :이 패턴의 일반 이름은 chainable setters다음과 같습니다. D
Mooing Duck

4
단일 책임 원칙. 논리를 Person 클래스에 넣으면 둘 이상의 작업이 있습니다.
reggaeguitar

1
귀하의 혜택은 어느 쪽이든 가능합니다 : withName이름 필드 만 변경된 사람의 사본을 반환 할 수 있습니다 . 다시 말해, 불변 인 Person john = new Person().withName("John");경우에도 작동 할 수 있습니다 Person(그리고 이것은 함수형 프로그래밍에서 일반적인 패턴입니다).
Brian McCutchon

9
@MooingDuck : 또 다른 일반적인 용어는 유창한 인터페이스 입니다.
Mac

2
@Mac 유창한 인터페이스가 좀 더 일반적이라고 생각합니다 void. 메소드가 필요 없습니다. 예를 들어, Person이름을 인쇄하는 메소드가있는 경우 에도 Fluent Interface로 체인을 연결할 수 있습니다 person.setName("Alice").sayName().setName("Bob").sayName(). 그건 그렇고, 나는 JavaDoc에있는 사람들에게 정확하게 당신의 제안으로 주석을 달았 습니다. 실행이 끝날 @return Fluent interface때 수행되는 메소드에 적용 할 때 일반적이고 명확 return this하며 상당히 분명합니다. 따라서 빌더는 유창한 인터페이스를 수행합니다.
VLAZ

답변:


27

그것은 당신이 불변 할 수 있도록명명 된 매개 변수를 시뮬레이션 같은 시간에.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

이렇게하면 상태가 설정 될 때까지 미트를 사람에게서 멀리 유지하고 일단 설정하면 변경할 수 없지만 모든 필드에는 명확하게 레이블이 지정됩니다. Java에서 단 하나의 클래스로는이 작업을 수행 할 수 없습니다.

Josh Blochs Builder Pattern에 대해 이야기하고있는 것 같습니다. . 이것은 Gang of Four Builder Pattern 과 혼동되어서는 안됩니다 . 이들은 다른 짐승입니다. 그들은 건설 문제를 해결하지만 상당히 다른 방식으로 해결합니다.

물론 다른 클래스를 사용하지 않고도 객체를 구성 할 수 있습니다. 그러나 당신은 선택해야합니다. 명명 된 매개 변수가없는 언어 (예 : Java)에서 이름 지정된 매개 변수를 시뮬레이션하는 기능이 손실되거나 오브젝트 수명 기간 동안 불변 상태로 유지되는 기능이 손실됩니다.

불변의 예, 파라미터의 이름이 없다

Person p = new Person("Arthur Dent", 42);

여기서는 하나의 간단한 생성자로 모든 것을 구축합니다. 이렇게하면 불변 상태를 유지할 수 있지만 명명 된 매개 변수의 시뮬레이션이 느슨해집니다. 많은 매개 변수로 읽기가 어렵습니다. 컴퓨터는 신경 쓰지 않지만 인간에게는 어렵습니다.

기존의 세터를 사용하여 명명 된 매개 변수 예제를 시뮬레이션했습니다. 변경할 수 없습니다.

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

여기서는 setter로 모든 것을 구축하고 명명 된 매개 변수를 시뮬레이션하지만 더 이상 변경할 수 없습니다. 세터를 사용할 때마다 객체 상태가 변경됩니다.

클래스를 추가하면 두 가지를 모두 수행 할 수 있습니다.

build()누락 된 연령 필드에 대한 런타임 오류가 충분한 경우 유효성 검사 를 수행 할 수 있습니다 . 업그레이드하고 시행 할 수 있습니다age() 하고 컴파일러 오류와 함께 호출 . Josh Bloch 빌더 패턴이 아닙니다.

이를 위해서는 내부 도메인 특정 언어 가 필요합니다 (iDSL) .

이것은 당신이 그들이 전화를 요구할 수 있습니다 age()name()호출하기 전에 build(). 하지만 당신은 돌아 오는 것만으로는 할 수 없습니다this 매번 . 반환되는 각 항목은 다음 항목을 호출하도록하는 다른 항목을 반환합니다.

사용은 다음과 같습니다.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

하지만 이것은:

Person p = personBuilder
    .age(42)
    .build()
;

age()의해 반환 된 형식을 호출하는 것이 유효하기 때문에 컴파일러 오류가 발생합니다.name() .

이 iDSL은 매우 강력하고 ( 예 : JOOQ 또는 Java8 스트림 ) 특히 코드 완성 기능이있는 IDE를 사용하는 경우 사용하기에 매우 좋지만 설정 작업이 상당히 복잡합니다. 나는 그것들에 대해 작성된 약간의 소스 코드가있는 것들을 위해 그것들을 저장하는 것이 좋습니다.


3
그리고 누군가 왜 당신 나이 전에 이름을 제공 해야하는지 묻습니다 . 유일한 조합은 "조합 폭발 때문에"입니다. C ++과 같은 적절한 템플릿이 있거나 소스에 다른 유형을 계속 사용하면 소스에서 피할 수 있다고 확신합니다.
중복 제거기

1
새는 추상화 를 사용하는 방법을 알고 할 수있는 근본적인 복잡성의 지식이 필요합니다. 그것이 제가 여기 보는 것입니다. 자체 빌드 방법을 모르는 클래스는 반드시 추가 종속성이있는 빌더와 결합되어야합니다 (빌더 개념의 목적 및 특성). 한 번 (밴드 캠프가 아님) 그러한 클러스터 프랙 만 실행 취소하려면 3 개월이 걸리는 객체를 인스턴스화해야합니다. 난 당신이 아니에요.
radarbob

구성 규칙을 모르는 클래스의 장점 중 하나는 규칙이 변경 될 때 신경 쓰지 않는다는 것입니다. AnonymousPersonBuilder자체 규칙 집합이 있는 것을 추가 할 수 있습니다 .
candied_orange

클래스 / 시스템 디자인은 "응집을 최대화하고 커플 링을 최소화하는"심층적 인 응용을 거의 보여주지 않습니다. 디자인 및 런타임의 레거시는 확장 가능하며 결함이 프랙탈 또는 "거북이입니다"라는 느낌으로 OO 최대 값 및 지침을 적용한 경우에만 결함을 수정할 수 있습니다.
radarbob

1
@radarbob 빌드중인 클래스는 여전히 자체 빌드 방법을 알고 있습니다. 생성자는 객체의 무결성을 보장합니다. 생성자는 종종 많은 매개 변수를 사용하기 때문에 빌더 클래스는 생성자를 돕는 도우미에 불과합니다.
No U

57

빌더 클래스를 사용 / 제공하는 이유 :

  • 불변의 객체를 만들기 위해 – 이미 알고있는 이점. 구성이 여러 단계를 거치는 경우에 유용합니다. FWIW에서, 불변성은 유지 관리 가능하고 버그가없는 프로그램을 작성하려는 우리의 탐구에서 중요한 도구가되어야합니다.
  • 최종 (불변) 객체의 런타임 표현이 읽기 및 / 또는 공간 사용에 최적화되어 있지만 업데이트에는 적합하지 않은 경우 String과 StringBuilder가 좋은 예입니다. 반복적으로 문자열을 연결하는 것은 그리 효율적이지 않으므로 StringBuilder는 추가하기에는 좋지만 공간 사용에는 좋지 않으며 일반 String 클래스처럼 읽고 사용하기에는 좋지 않은 다른 내부 표현을 사용합니다.
  • 생성 된 객체를 생성중인 객체와 명확하게 분리합니다. 이 접근법은 공사중에서 공사로 명확하게 전환해야합니다. 컨슈머의 경우, 언더 컨스트럭션 오브젝트와 생성 된 오브젝트를 혼동 할 방법이 없습니다. 유형 시스템이이를 시행합니다. 즉, 우리는 때때로이 접근 방식을 사용하여 "성공의 구덩이에 빠지게"할 수 있으며 다른 사람 (또는 자신)이 API 또는 계층과 같이 사용할 추상화를 할 때 매우 유용 할 수 있습니다. 의회.

4
마지막 글 머리 기호에 관해서. 따라서 PersonBuildergetter가 없으며 현재 값을 검사하는 유일한 방법은을 호출 .Build()하여 호출 하는 것 Person입니다. 이 방법으로 .Build가 올바르게 구성된 객체의 유효성을 검사 할 수 있습니까? 즉, 이것은 "공사중"개체의 사용을 방지하기위한 메커니즘입니까?
AaronLS

@AaronLS, 그렇습니다. 복잡한 유효성 검사는 Build작업이 수행되어 불량 및 / 또는 구성이 잘못된 개체를 방지 할 수 있습니다 .
Erik Eidt

2
객체의 생성자 내에서 객체의 유효성을 검사 할 수도 있습니다. 환경 설정은 개인적이거나 복잡성에 따라 달라질 수 있습니다. 무엇을 선택하든, 요점은 불변의 객체를 얻은 후에는 객체가 올바르게 빌드되고 예기치 않은 값이 없다는 것을 알 수 있습니다. 상태가 잘못된 불변 객체는 발생하지 않아야합니다.
Walfrat

2
@AaronLS 빌더는 getter를 가질 수 있습니다. 요점은 그것이 아니다. 반드시 필요한 것은 아니지만 빌더에 게터가있는 경우이 특성이 아직 지정되지 않은 경우 getAge빌더 를 호출 하면 리턴 null될 수 있음을 알아야합니다 . 반대로, Person클래스는 연령을 절대로 만들 수없는 불변량을 강제 할 수 있는데, 이는 연령대없이 행복하게 만드는 nullOP의 new Person() .withName("John")예 와 같이 빌더와 실제 객체가 혼합되어있을 때 불가능 Person합니다. 세터가 불변을 강요 할 수 있지만 초기 값을 강요 할 수 없으므로 변경 가능한 클래스에도 적용됩니다.
Holger

1
@Holger 당신의 요점은 사실이지만, 주로 빌더가 게터를 피해야한다는 주장을 지원합니다.
user949300

21

한 가지 이유는 전달 된 모든 데이터가 비즈니스 규칙을 따르는 지 확인하는 것입니다.

귀하의 예는 이것을 고려하지 않지만 누군가 빈 문자열이나 특수 문자로 구성된 문자열을 전달했다고 가정 해 봅시다. 당신은 그들의 이름이 실제로 유효한 이름인지 (실제로는 매우 어려운 작업 임) 확인하는 것에 근거하여 일종의 논리를 원할 것입니다.

특히 논리가 매우 작은 경우 (예 : 나이가 음수가 아닌지 확인) 논리가 커질수록이를 분리하는 것이 좋습니다.


7
이 질문의 예는 간단한 규칙 위반을 제공합니다. 연령대가 없습니다john
Caleth

6
+1 그리고 빌더 클래스없이 두 필드에 대해 동시에 규칙을 수행하는 것은 매우 어렵습니다 (필드 X + Y는 10보다 클 수 없음).
레지날드 블루

7
@ReginaldBlue "x + y is even"에 묶여 있으면 더 어려워집니다. 상태가 (0,0) 인 초기 조건은 정상이지만 상태 (1,1)도 정상이지만 상태를 유지하고 (0,0)에서 (1)로 이동시키는 단일 변수 업데이트 시퀀스는 없습니다. ,1).
Michael Anderson

이것이 가장 큰 장점이라고 생각합니다. 다른 모든 사람들은 훌륭합니다.
Giacomo Alzetta

5

다른 답변에서 볼 수있는 것과 약간 다른 각도입니다.

withFoo여기에서 의 접근 방식은 setter처럼 동작하지만 클래스가 불변성을 지원하는 것처럼 보이도록 정의되어 있기 때문에 문제가됩니다. Java 클래스에서 메소드가 특성을 수정하는 경우 'set'으로 메소드를 시작하는 것이 일반적입니다. 나는 그것을 표준으로 사랑한 적이 없지만 다른 일을하면 사람들 을 놀라게 할 것이고 좋지 않습니다. 여기에있는 기본 API로 불변성을 지원할 수있는 또 다른 방법이 있습니다. 예를 들면 다음과 같습니다.

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

부분적으로 구성된 객체를 부적절하게 제작하는 것을 막지 못하지만 기존 객체의 변경을 방지합니다. 아마도 이런 종류의 일에는 어리석은 일입니다 (JB Builder도 마찬가지입니다). 예, 더 많은 객체를 만들지 만 비싸지 않습니다. 생각 .

CopyOnWriteArrayList 와 같은 동시 데이터 구조와 함께 사용되는 이러한 종류의 접근 방식이 대부분 보입니다 . 그리고 이것은 불변성이 중요한 이유를 암시합니다. 코드를 스레드로부터 안전하게하려면 불변성을 거의 항상 고려해야합니다. Java에서 각 스레드는 가변 상태의 로컬 캐시를 유지할 수 있습니다. 하나의 스레드가 다른 스레드의 변경 사항을 보려면 동기화 된 블록 또는 다른 동시성 기능을 사용해야합니다. 이 중 어느 것도 코드에 약간의 오버 헤드를 추가합니다. 그러나 변수가 최종적인 경우 할 일이 없습니다. 값은 항상 초기화 된 값이므로 모든 스레드가 무엇이든 동일한 것을 봅니다.


2

여기에 명시 적으로 언급되지 않은 또 다른 이유는 build()모든 필드가 '필드에 유효한 값 (직접 설정되거나 다른 필드의 다른 값에서 파생 됨)을 포함하고 있는지를 확인할 수 있기 때문입니다. 그렇지 않으면 발생합니다.

또 다른 이점은 Person객체가 더 단순한 수명과 단순한 불변 세트를 갖게 된다는 것입니다. 당신은 일단 당신이, Person p그리고 당신은 p.name유효한 것을 알고 있습니다 p.age. "연령이 설정되었지만 이름이 지정되지 않은 경우 또는 이름이 설정되었지만 연령이 지정되지 않은 경우"와 같은 상황을 처리하도록 어떤 방법도 설계하지 않아도됩니다. 이것은 클래스의 전반적인 복잡성을 줄입니다.


"[...]는 모든 필드가 설정되어 있는지 확인할 수 있습니다 [...]"빌더 패턴의 요점은 모든 필드를 직접 설정할 필요가 없으며 합리적인 기본값으로 돌아갈 수 있다는 것입니다. 어쨌든 모든 필드를 설정해야하는 경우 해당 패턴을 사용하지 않아야합니다!
Vincent Savard

@VincentSavard 실제로, 내 생각에, 그것은 가장 일반적인 사용법입니다. 일부 "핵심"값 세트가 설정되어 있는지 확인하고 일부 선택적 값 (기본값 설정, 다른 값에서 값 파생) 등을 처리합니다.
Alexander-복원 모니카

'모든 필드가 설정되어 있는지 확인하십시오'라고 말하는 대신 '모든 필드에 유효한 값이 포함되어 있는지 확인합니다 (때로는 다른 필드의 값에 따라 다름)'(예 : 필드는 값에 따라 특정 값만 허용 할 수 있음)로 변경합니다 다른 분야의). 이것은 각 세터에서 수행 될 수 있지만, 객체가 완성되고 빌드 될 준비가 될 때까지 예외가 발생하지 않고 누군가가 객체를 쉽게 설정할 수 있습니다.
yitzih

빌드되는 클래스의 생성자는 궁극적으로 인수의 유효성에 대한 책임이 있습니다. 빌더의 유효성 검증은 기껏해야 중복되며 생성자의 요구 사항과 쉽게 맞지 않을 수 있습니다.
아니요 U

@TKK 참으로. 그러나 그들은 다른 것을 검증합니다. 필자가 빌더를 사용한 많은 경우에 빌더의 임무는 결국 생성자에게 제공되는 입력을 구성하는 것이 었습니다. 예를 들어, 빌더는 a URI, a File또는 a 로 구성되며 궁극적으로 인수로 생성자 호출에 들어가는 것을 FileInputStream얻기 위해 제공된 모든 것을 사용합니다FileInputStream
Alexander-Reinstate Monica

2

인터페이스 또는 추상 클래스를 리턴하도록 빌더를 정의 할 수도 있습니다. 빌더를 사용하여 오브젝트를 정의 할 수 있으며 빌더는 설정된 특성 또는 설정된 특성에 따라 리턴 할 구체적인 서브 클래스를 판별 할 수 있습니다.


2

빌더 패턴은 특성을 설정하여 단계별 로 오브젝트를 빌드 / 작성하는 데 사용되며 모든 필수 필드가 설정되면 빌드 메소드를 사용하여 최종 오브젝트를 리턴합니다. 새로 만든 개체는 변경할 수 없습니다. 여기서 주목할 점은 최종 빌드 메소드가 호출 될 때만 오브젝트가 리턴된다는 것입니다. 이렇게하면 모든 특성이 오브젝트로 설정되므로 오브젝트가 빌더 클래스에 의해 리턴 될 때 오브젝트가 일관성이없는 상태 가되지 않습니다 .

빌더 클래스를 사용하지 않고 모든 빌더 클래스 메소드를 Person 클래스 자체에 직접 넣는 경우 먼저 오브젝트를 작성한 후 작성된 오브젝트에서 setter 메소드를 호출하여 작성간에 오브젝트의 불일치 상태를 초래해야합니다. 개체 및 속성 설정.

따라서 빌더 클래스 (즉, Person 클래스 자체가 아닌 일부 외부 엔티티)를 사용하여 오브젝트가 일관성이없는 상태에 있지 않도록합니다.


@DawidO 당신은 내 요점을 얻었다. 내가 여기에 넣고 자하는 주요 강조는 일관성이없는 상태입니다. 이것이 빌더 패턴을 사용하는 주요 이점 중 하나입니다.
Shubham Bondarde

2

빌더 오브젝트 재사용

다른 사람들이 언급했듯이, 불변성 및 모든 필드의 비즈니스 로직을 검증하여 오브젝트를 유효성 검증하는 것이 별도의 빌더 오브젝트의 주요 이유입니다.

그러나 재사용 성은 또 다른 이점입니다. 매우 유사한 많은 객체를 인스턴스화하려면 빌더 객체를 약간 변경하고 인스턴스화를 계속할 수 있습니다. 빌더 오브젝트를 다시 작성할 필요가 없습니다. 이 재사용을 통해 빌더는 많은 불변 오브젝트를 작성하기위한 템플리트로 작동 할 수 있습니다. 작은 이점이지만 유용한 이점이 될 수 있습니다.


1

실제로 클래스 자체에 빌더 메소드 를 사용할 수 있으며 여전히 불변성을 가질 수 있습니다. 단지 빌더 메소드가 기존 오브젝트를 수정하는 대신 새 오브젝트를 리턴 함을 의미합니다.

초기 (유효한 / 유용한) 객체를 얻는 방법이있는 경우에만 (예 : 모든 필수 필드를 설정하는 생성자 또는 기본값을 설정하는 팩토리 메소드에서) 추가 빌더 메소드가 수정 된 오브젝트를 기반으로 리턴하는 경우에만 작동합니다. 기존에. 이러한 빌더 메소드는 진행 중 유효하지 않거나 일치하지 않는 오브젝트를 얻지 않도록해야합니다.

물론 이것은 새로운 객체가 많이 있다는 것을 의미하며 객체를 만드는 데 비용이 많이 드는 경우에는 그렇게하지 않아야합니다.

테스트 코드에서 비즈니스 객체 중 하나에 대한 Hamcrest 매처 를 만드는 데 사용했습니다 . 정확한 코드는 기억 나지 않지만 다음과 같이 보입니다 (간체).

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

그런 다음 단위 테스트에서 적절한 정적 가져 오기를 사용하여 다음과 같이 사용합니다.

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));

1
이것은 사실입니다. 매우 중요한 시점이라고 생각하지만 마지막 문제는 해결하지 못합니다. 객체가 구성 될 때 객체가 일관된 상태에 있는지 확인할 기회가 없습니다. 일단 생성되면 호출자가 모든 메소드를 설정했는지 확인하기 위해 비즈니스 메소드 전에 유효성 검증기를 호출하는 것과 같은 속임수가 필요합니다 (끔찍한 연습이 될 것입니다). 왜 이런 방식으로 빌더 패턴을 사용 하는지를 알기 위해서는 이런 종류의 일을 통해 추론하는 것이 좋습니다.
Bill K

@BillK true-본질적으로 불변의 setter가있는 객체 (매번 새 객체를 반환 함)는 반환 된 각 객체가 유효해야 함을 의미합니다. 유효하지 않은 객체를 반환하는 경우 (예 : 사람이 name있지만 개인이 없는 경우 age) 개념이 작동하지 않습니다. 글쎄, 당신은 실제로 PartiallyBuiltPerson유효하지 않은 것처럼 무언가를 반환 할 수 있지만 빌더를 가리는 해킹처럼 보입니다.
VLAZ

@BillK 이것이 "초기 (유효한 / 유용한) 객체를 얻는 방법이있는 경우" 라는 의미입니다 – 초기 객체와 각 빌더 함수에서 반환 된 객체가 모두 유효하고 일관된 것으로 가정합니다. 이러한 클래스를 만든 사람은 일관된 변경을 수행하는 빌더 메소드 만 제공해야합니다.
Paŭlo Ebermann
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.