빌더 패턴 : 실패시기


45

빌더 패턴을 구현할 때, 나는 종종 건물이 실패하게 할 때 혼란스러워하며 며칠마다 문제에 대해 다른 입장을 취할 수도 있습니다.

먼저 몇 가지 설명 :

  • 으로 초기 실패 나 잘못된 매개 변수가 전달 될 때 객체를 구축하는 것은 곧 실패 할 것을 의미한다. 그래서 내부 SomeObjectBuilder.
  • 늦게 실패 하면 빌드 할 오브젝트 build()의 생성자를 내재적으로 호출하는 호출 에서만 오브젝트 작성이 실패 할 수 있음을 의미합니다 .

그런 다음 몇 가지 주장이 있습니다.

  • 늦게 실패에 찬성하여 : 빌더 클래스는 단순히 값을 보유하는 클래스 이상이어야합니다. 또한 코드 중복이 줄어 듭니다.
  • 조기 실패에 대한 찬성 : 소프트웨어 프로그래밍의 일반적인 접근 방식은 가능한 한 빨리 문제를 감지하여 빌더 클래스의 생성자 인 '세터'와 궁극적으로 빌드 메소드에서 확인하는 것이 가장 논리적입니다.

이것에 대한 일반적인 의견은 무엇입니까?


8
늦게 실패하는 데는 이점이 없습니다. 누군가가 빌더 클래스가 "좋은"디자인을하는 것은 좋은 디자인보다 우선하지 않으며, 버그를 일찍 잡는 것이 항상 늦게 잡는 것보다 낫습니다.
Doval

3
이를 보는 또 다른 방법은 빌더가 유효한 데이터가 무엇인지 알 수 없다는 것입니다. 이 경우 초기에 실패 하면 오류가 있음 을 알게 되는 즉시 실패하는 것 입니다. 일찍 실패 하지 않으면 빌더 null가에 문제가있을 때 객체를 반환합니다 build().
Chris

빌더를 수정하는 경고 및 오퍼 수단을 발행하는 방법을 추가하지 않으면 늦게 실패 할 필요가 없습니다.
Mark

답변:


34

유효성 검사 코드를 배치 할 수있는 옵션을 살펴 보겠습니다.

  1. 빌더의 세터 내부.
  2. build()방법 내부 .
  3. 생성 된 엔티티 내부 : build()엔티티가 작성 될 때 메소드 에서 호출됩니다 .

옵션 1을 사용하면 문제를 조기에 감지 할 수 있지만 전체 컨텍스트 만있는 입력의 유효성을 검사하여 build()방법 의 유효성 검사 중 적어도 일부를 수행하는 복잡한 경우가있을 수 있습니다 . 따라서 옵션 1을 선택하면 한 부분에서 유효성 검사의 일부가 수행되고 다른 부분에서 다른 부분이 수행되는 코드가 일치하지 않게됩니다.

옵션 2 는 옵션 1보다 크게 나쁘지 않습니다. 일반적으로 빌더의 setter는 build(), 특히 유창한 인터페이스에서 바로 호출되기 때문 입니다. 따라서 대부분의 경우 문제를 조기에 발견 할 수 있습니다. 그러나 빌더가 오브젝트를 작성하는 유일한 방법이 아닌 경우, 오브젝트를 작성하는 모든 위치에 있어야하기 때문에 유효성 검증 코드가 복제됩니다. 이 경우 가장 논리적 인 해결책은 유효성 검사를 생성 된 개체, 즉 그 내부에 최대한 가깝게하는 것입니다. 그리고 이것은 옵션 3 입니다.

SOLID 관점에서 빌더에 유효성 검증을하는 것도 SRP를 위반합니다. 빌더 클래스는 이미 오브젝트를 구성하기 위해 데이터를 집계해야합니다. 유효성 검사는 자체 내부 상태로 계약을 수립하는 것이며 다른 개체의 상태를 확인하는 것은 새로운 책임입니다.

따라서 제 관점에서 볼 때 디자인 관점에서 늦게 실패하는 것이 좋을뿐만 아니라 빌더 자체가 아닌 생성 된 엔티티 내부에서 실패하는 것이 좋습니다.

UPD : 이 의견 은 빌더 (옵션 1 또는 2) 내부의 유효성 검사가 의미가있을 때 한 가지 가능성을 상기시킵니다. 빌더가 작성중인 오브젝트에 대한 자체 계약이 있으면 의미가 있습니다. 예를 들어, 특정 컨텐츠 (예 : number ranges list)로 문자열을 구성하는 빌더가 있다고 가정하십시오 1-2,3-4,5-6. 이 빌더에는와 같은 메소드가있을 수 있습니다 addRange(int min, int max). 결과 문자열은 이러한 숫자에 대해 전혀 알지 못하며 알 필요도 없습니다. 빌더 자체는 문자열의 형식 및 숫자의 제한 조건을 정의합니다. 따라서 메소드 addRange(int,int)는 입력 숫자의 유효성을 검사하고 max가 min보다 작은 경우 예외를 발생시켜야합니다.

즉, 일반적인 규칙은 빌더 자체에서 정의한 계약 만 유효성 검증하는 것입니다.


옵션 1 이 "일관되지 않은"검사 시간으로 이어질 수 있지만 모든 것이 "가능한 한 빨리"일관된 것으로 간주 될 수 있다는 점에 주목할 가치가 있다고 생각 합니다. 빌더의 변형 인 StepBuilder가 대신 사용되면 "가능한 한 빨리"더 명확하게 만드는 것이 조금 더 쉽습니다.
Joshua Taylor

널 (null) 문자열이 전달되면 URI 빌더에서 예외가 발생하면 이것이 SOLID를 위반합니까? 쓰레기
Gusdor

@Gusdor 예, 예외 자체를 던지면. 그러나 사용자 관점에서 볼 때 모든 옵션은 예외로 보입니다.
Ivan Gammel

그렇다면 build ()에 의해 호출되는 validate ()가없는 이유는 무엇입니까? 이렇게하면 중복, 일관성 및 SRP 위반이 거의 없습니다. 또한 빌드하지 않고 데이터를 검증 할 수 있으며 검증은 작성에 가깝습니다.
StellarVortex

이 경우 @StellarVortex는 builder.build ()에서 한 번 두 번 유효성 검사되며 데이터가 유효하고 해당 생성자에서 객체의 생성자로 진행합니다.
Ivan Gammel

34

Java를 사용한다고 가정하면 Java 오브젝트 작성 및 삭제 기사 (아래 인용의 굵은 글꼴은 내 글임)에서 Joshua Bloch가 제공 한 권위 있고 자세한 지침을 고려하십시오 .

생성자와 마찬가지로 빌더는 매개 변수에 불변을 부과 할 수 있습니다. 빌드 메소드는 이러한 불변량을 확인할 수 있습니다. 매개 변수를 빌더에서 오브젝트로 복사 한 후 점검해야하며 빌더 필드 (항목 39)가 아닌 오브젝트 필드에서 점검해야합니다 . 변형이 위반되면 빌드 메소드는 IllegalStateException(Item 60)을 발생 시켜야합니다 . 예외의 detail 메소드는 위반 된 위반을 표시해야합니다 (항목 63).

다중 매개 변수를 포함하는 불변을 적용하는 또 다른 방법은 setter 메소드가 일부 불변이 보유해야하는 전체 매개 변수 그룹을 가져 오는 것입니다. 불변 값이 만족되지 않으면 setter 메소드는를 던집니다 IllegalArgumentException. 이는 빌드가 호출되기를 기다리는 대신 유효하지 않은 매개 변수가 전달되 자마자 변하지 않는 실패를 감지하는 이점이 있습니다.

이 기사의 편집자 설명 에 따르면 위 인용문의 "항목"은 Effective Java, Second Edition에 제시된 규칙을 나타냅니다 .

이 기사에서는 이것이 권장되는 이유를 자세히 설명하지는 않지만 생각하면 그 이유는 분명합니다. 이것을 이해하는 일반적인 팁은 빌더 개념이 생성자의 개념에 연결되는 방법에 대한 설명에서 기사의 바로 거기에 제공되며 클래스 불변은 호출을 선행 / 준비 할 수있는 다른 코드가 아닌 생성자에서 확인해야합니다.

빌드를 호출하기 전에 불변량을 검사하는 것이 왜 잘못된 지에 대한보다 구체적인 이해를 위해서는 CarBuilder 의 일반적인 예를 고려하십시오 . 빌더 메소드는 임의의 순서로 호출 될 수 있으며, 결과적으로 특정 매개 변수가 빌드 전까지 유효한지 알 수 없습니다.

스포츠카에 2 인용 좌석이있을 수 없다고 생각하십시오. setSeats(4)괜찮은지 어떻게 알 수 있습니까? setSportsCar()호출 여부를 확실하게 알 수있을 때만 빌드에 있으며 , 던질 지 여부를 의미합니다 TooManySeatsException.


3
던질 예외 유형, 내가 찾던 것을 권장하는 +1.
Xantix

대안을 얻지 못했습니다. 변이체가 그룹에서만 검증 될 수있을 때만 순수하게 이야기하는 것 같습니다. 빌더는 다른 속성이없는 경우 단일 속성을 허용하고 그룹 자체에 불변이있는 경우에만 속성 그룹을 허용합니다. 이 경우 단일 속성이 빌드 전에 예외를 발생시켜야합니까?
Didier A.

19

허용되지 않기 때문에 유효하지 않은 잘못된 값은 제 의견으로는 즉시 알려야합니다. 즉, 양수 만 허용하고 음수가 전달 된 경우 build()가 호출 될 때까지 기다릴 필요가 없습니다 . 이 메소드가 처음에 메소드를 호출하기위한 전제 조건이기 때문에 이러한 유형의 문제점을 고려하지 않을 것입니다. 즉, 특정 매개 변수 설정 실패에 의존 하지 않을 것 입니다. 아마도 매개 변수가 올바른 것으로 가정하거나 직접 확인해야 할 것입니다.

그러나 쉽게 확인할 수없는보다 복잡한 문제의 경우에 전화를 걸면 알려지지 않을 수 있습니다 build(). 이에 대한 좋은 예는 데이터베이스에 연결하기 위해 제공 한 연결 정보를 사용하는 것입니다. 이 경우 기술적 으로 이러한 조건을 확인할 있지만 더 이상 직관적이지 않으며 코드가 복잡해집니다. 내가 알다시피, 이것들은 실제로 일어날 수 있고 시도하기 전에는 실제로 예상 할 수없는 유형의 문제이기도합니다. 문자열을 정규 표현식과 일치시켜 int로 구문 분석 될 수 있는지 확인 하고 단순히 구문 분석하려고 시도하여 결과적으로 발생할 수있는 잠재적 예외를 처리 하는 것의 차이점 입니다.

나는 일반적으로 매개 변수를 설정할 때 예외가 발생하는 것을 포착해야하기 때문에 예외를 던지는 것을 싫어합니다. 따라서의 유효성 검사를 선호합니다 build(). 따라서 이러한 이유로 RuntimeException을 사용하는 것이 좋습니다. 다시 전달 된 매개 변수의 오류는 일반적으로 발생하지 않아야합니다.

그러나 이것은 무엇보다 모범 사례입니다. 귀하의 질문에 답변이 되었기를 바랍니다.


11

내가 아는 한, 일반적인 관행 (합의가 있는지 확실하지 않은)은 오류를 쉽게 발견 할 수있는 한 빨리 실패하는 것입니다. 또한 실수로 API를 오용하기가 더 어려워집니다.

음수가 아니어야하는 용량 또는 길이와 같이 입력에서 확인할 수있는 사소한 속성 인 경우 즉시 실패하는 것이 가장 좋습니다. 오류를 막 으면 실수와 피드백 사이의 거리가 멀어 지므로 문제의 원인을 찾기가 더 어려워집니다.

속성의 유효성이 다른 속성에 의존하는 상황에서 불행한 경우 두 가지 선택이 있습니다.

  • 두 개 이상의 속성을 동시에 제공해야합니다 (예 : 단일 메소드 호출).
  • 더 이상 변경 사항이 수신되지 않는다는 것을 알게되는 즉시 유효성을 테스트하십시오 build().

대부분의 경우와 마찬가지로 이는 상황에서 내려진 결정입니다. 컨텍스트가 초기에 실패하기 어색하거나 복잡하게 만드는 경우 나중에 검사를 연기하기 위해 절충이 이루어질 수 있지만 실패시 기본값이 기본값이어야합니다.


요약하자면, 가능한 한 빨리 객체 / 기본 유형으로 커버 될 수있는 모든 것을 검증하는 것이 합리적이라고 말하고 있습니까? 마찬가지로 unsigned, @NonNull
skiwi

2
@ skiwi 그렇습니다. 도메인 검사, null 검사, 그런 종류의 것. 나는 그것보다 훨씬 더 많은 것을 넣는 것을 옹호하지 않을 것이다. 건축업자는 일반적으로 간단한 것들이다.
JvR

1
한 매개 변수의 유효성이 다른 매개 변수의 값에 의존하는 경우, 다른 매개 변수 가 "실제로"설정되어 있음을 알고있는 경우 에만 매개 변수 값을 합법적으로 거부 할 수 있습니다 . 매개 변수 값을 여러 번 설정하는 것이 허용되는 경우 (마지막 설정이 우선) 개체를 설정하는 가장 자연스러운 방법은 매개 변수 X를 현재 값이 지정된 경우 유효하지 않은 값 으로 설정하는 것일 수 있습니다 Y. 그러나 호출하기 전에 유효한 값으로 build()설정 하십시오. YX
supercat

예를 들어 건물을 건축 Shape중이고 건축업자가 소유 WithLeft하고있는 WithRight특성을 가지고 있고 다른 장소에 물체를 건설하기 위해 건축업자를 조정하고자한다면 WithRight, 물체를 오른쪽으로 움직일 때 먼저 호출 WithLeft해야하고 왼쪽으로 움직일 때 불필요하게 복잡해집니다. 허용 비교 WithLeft가 제공 이전 우단 오른쪽 왼쪽 모서리를 설정 WithRight우단 전에 수정 build불린다.
supercat

0

기본 규칙은 "실패"입니다.

약간 더 발전된 규칙은 "가능한 빨리 실패"입니다.

속성이 본질적으로 유효하지 않은 경우 ...

CarBuilder.numberOfWheels( -1 ). ...  

... 그러면 즉시 거절합니다.

다른 경우에는 값을 조합 하여 확인해야 하고 build () 메소드에 더 잘 배치 될 수 있습니다.

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.