확장 성이 뛰어난 클래스에서 사용하기에 Bloch의 빌더 패턴을 개선하는 방법


34

나는 Joshua Bloch의 Effective Java book (2 판)에 크게 영향을 받았으며 아마도 내가 읽은 프로그래밍 책보다 더 많은 영향을 미쳤을 것입니다. 특히 그의 빌더 패턴 (항목 2)이 가장 큰 영향을 미쳤습니다.

Bloch의 빌더가 지난 10 년 동안의 프로그래밍보다 몇 달 더 멀어 졌음에도 불구하고 여전히 같은 벽에 부딪 히고 있습니다. 제네릭은 활동하기 시작하고, --especially 때 특히자기 참조 제네릭 (예 Comparable<T extends Comparable<T>>).

내가 가지고있는 두 가지 주요 요구 사항이 있습니다. 두 번째 질문 만이이 질문에 집중하고 싶습니다.

  1. 첫 번째 문제는 "각각의 단일 클래스에서 다시 구현할 필요없이 자체 리턴 메소드 체인을 공유하는 방법"입니다. 궁금한 분은이 답변 게시물의 맨 아래에서이 부분을 다루었지만 여기서 초점을 맞추고 싶지는 않습니다.

  2. 두 번째 문제는 "다른 많은 클래스에서 확장하려는 클래스에서 빌더를 어떻게 구현할 수 있습니까?"입니다. 빌더로 클래스를 확장하는 것은 자연스럽게 클래스를 확장하는 것보다 자연스럽게 어렵습니다. 또한 구현Needable 하는 빌더 있고 이에 따라 중요한 제네릭 이있는 클래스를 확장하는 것은 다루기 힘들다.

그건 내 문제입니다 그래서 : 어떻게 내가에 빌더를 부착 자유롭게 할 수 있도록 (내가 부르는)에 블로흐 빌더를 향상시킬 수 있는 클래스 - 클래스는 "기본 클래스"일 수하기위한 것입니다 경우에도 빌더 (및 잠재적 인 제네릭)가 부과하는 여분의 수하물로 인해 미래의 자아 또는 내 라이브러리 사용자를 실망시키지 않고 여러 번 확장 및 하위 확장 ?


부록
내 질문은 위의 2 부에 중점을 두었지만 문제를 다루는 방법을 포함하여 문제 1에 대해 조금 자세히 설명하고 싶었습니다.

첫 번째 문제는 "각각의 단일 클래스에서 다시 구현할 필요없이 자체 리턴 메소드 체인을 공유하는 방법"입니다. 이것은 확장 클래스 가 이러한 체인을 다시 구현해야하는 것을 막기위한 것이 아니며 , 물론 이러한 메소드 체인을 활용하려는 비 서브 클래스 를 방지하는 방법을 다시 구현해야하는 방법입니다. - 사용자가 자신 을 활용할 수 있도록 모든 자체 복귀 기능을 구현 합니까? 이를 위해 인터페이스 골격을 인쇄하고 지금은 그대로 두어야 할 필요가있는 디자인을 생각해 냈습니다. 그것은 나를 위해 잘 작동했습니다 (이 디자인은 몇 년 동안 제작되었습니다 ... 가장 어려운 부분은 순환 종속성을 피하는 것입니다).

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}

답변:


21

Josh Bloch의 Builder Pattern에 비해 크게 개선 된 부분을 만들었습니다. 어떤 방식 으로든 "더 나은"것이 아니라, 매우 구체적인 상황에서 , 어떤 장점을 제공합니다. 가장 큰 장점은 빌더를 빌드 할 클래스에서 분리시키는 것입니다.

나는이 대안을 아래에 철저히 문서화했으며, 이것을 블라인드 빌더 패턴이라고 부릅니다.


디자인 패턴 : 블라인드 빌더

Joshua Bloch의 Builder Pattern (Effective Java의 항목 2, 2 판)에 대한 대안으로 Bloch Builder 의 많은 이점을 공유하는 "블라인드 빌더 패턴"을 작성했습니다. 정확히 같은 방식으로 사용됩니다. 블라인드 빌더는

  • 둘러싸는 클래스에서 빌더를 분리하여 순환 종속성을 제거합니다.
  • 둘러싸는 클래스 의 소스 코드 크기를 크게 줄입니다 ( 더 이상아닙니다 ).
  • 허용 ToBeBuilt클래스는 확장 할 의 빌더를 확장 할 필요없이 .

이 문서에서는 빌드중인 ToBeBuilt클래스를 " "클래스라고합니다.

Bloch Builder로 구현 된 클래스

Bloch Builder는 public static class빌드하는 클래스 내부 에 포함되어 있습니다. 예를 들면 :

공개 클래스 UserConfig {
   개인 최종 문자열 sName;
   개인 최종 정보;
   개인 최종 문자열 sFavColor;
   공개 UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //이전
         {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            새로운 NullPointerException을 던집니다 ( "uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // 여기서 모든 필드 확인
   }
   공개 문자열 toString () {
      "name ="+ sName + ", age ="+ iAge + ", sFavColor ="+ sFavColor를 반환합니다.
   }
   // 빌더 ... START
   공개 정적 클래스 Cfg {
      개인 문자열 sName;
      개인 정보;
      개인 문자열 sFavColor;
      공개 Cfg (문자열 s_name) {
         sName = s_name;
      }
      // 자기 귀환 세터 ... START
         공개 Cfg 연령 (int i_age) {
            iAge = i_age;
            이것을 돌려줍니다;
         }
         공개 Cfg favoriteColor (문자열 s_color) {
            sFavColor = s_color;
            이것을 돌려줍니다;
         }
      // 자기 귀환 세터 ... END
      공개 UserConfig build () {
         반환 (신규 UserConfig (this));
      }
   }
   // 빌더 ... END
}

Bloch Builder를 사용하여 클래스 인스턴스화

UserConfig uc = new UserConfig.Cfg ( "Kermit"). age (50) .favoriteColor ( "green"). build ();

블라인드 빌더로 구현 된 동일한 클래스

블라인드 빌더에는 세 가지 부분이 있으며 각 부분은 별도의 소스 코드 파일에 있습니다.

  1. ToBeBuilt(이 예 : 클래스 UserConfig)
  2. " Fieldable"인터페이스
  3. 빌더

1. 건설 될 수업

빌드 될 클래스는 Fieldable인터페이스를 유일한 생성자 매개 변수로 승인합니다 . 생성자는 모든 내부 필드를 설정하고 각 필드의 유효성을 검사 합니다. 가장 중요한 것은이 ToBeBuilt클래스는 빌더에 대한 지식이 없다는 것입니다.

공개 클래스 UserConfig {
   개인 최종 문자열 sName;
   개인 최종 정보;
   개인 최종 문자열 sFavColor;
    공개 UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //이전
         {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            새로운 NullPointerException을 던집니다 ( "uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // 여기서 모든 필드 확인
   }
   공개 문자열 toString () {
      "name ="+ sName + ", age ="+ iAge + ", sFavColor ="+ sFavColor를 반환합니다.
   }
}

한 스마트 주석 작성자 (명확하게 답변을 삭제 한 사람)가 언급했듯이 ToBeBuilt클래스가 클래스를 구현하는 Fieldable경우 하나의 유일한 생성자를 기본 복사 생성자 로 사용할 수 있습니다 (단, 필드는 항상 유효성이 검사된다는 단점이 있지만 원본의 필드 ToBeBuilt가 유효한 것으로 알려져 있습니다 ).

2. " Fieldable"인터페이스

필드 가능한 인터페이스는 ToBeBuilt클래스와 해당 빌더 사이의 "브리지" 이며 객체를 빌드하는 데 필요한 모든 필드를 정의합니다. 이 인터페이스는 ToBeBuilt클래스 생성자에 필요 하며 빌더에 의해 구현됩니다. 이 인터페이스는 빌더 이외의 클래스로 구현 될 수 있으므로 모든 클래스는 ToBeBuilt빌더를 사용하지 않고도 클래스를 쉽게 인스턴스화 할 수 있습니다 . 또한 ToBeBuilt빌더 확장이 바람직하지 않거나 필요하지 않을 때 클래스를 쉽게 확장 할 수 있습니다 .

아래 섹션에서 설명했듯이이 인터페이스의 기능을 전혀 문서화하지 않았습니다.

공용 인터페이스 UserConfig_Fieldable {
   문자열 getName ();
   int getAge ();
   문자열 getFavoriteColor ();
}

3. 빌더

빌더는 Fieldable클래스를 구현합니다 . 그것은 전혀 검증되지 않으며,이 사실을 강조하기 위해 모든 분야는 공개적이고 변경 가능합니다. 이 공개 접근성은 필수 사항은 아니지만, ToBeBuilt의 생성자가 호출 될 때까지 유효성 검사가 수행되지 않는다는 사실을 다시 강제하기 때문에이를 선호하고 권장합니다 . 이것은 다른 스레드가 빌더를 생성자에 전달하기 전에 빌더를 추가로 조작 할 수 있기 때문에 중요 ToBeBuilt합니다. 필드가 유효하다는 것을 보증하는 유일한 방법은 빌더가 어떻게 든 상태를 "잠글"수 없다고 가정하면 ToBeBuilt클래스가 최종 확인을 수행하는 것입니다.

마지막으로 Fieldable인터페이스 와 마찬가지로 getter도 문서화하지 않습니다.

공개 클래스 UserConfig_Cfg는 UserConfig_Fieldable {
   공개 문자열 sName;
   공개 정보;
    공개 문자열 sFavColor;
    공개 UserConfig_Cfg (문자열 s_name) {
       sName = s_name;
    }
    // 자기 귀환 세터 ... START
       공개 UserConfig_Cfg 연령 (int i_age) {
          iAge = i_age;
          이것을 돌려줍니다;
       }
       공개 UserConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          이것을 돌려줍니다;
       }
    // 자기 귀환 세터 ... END
    //getters...START
       공개 문자열 getName () {
          sName을 반환합니다.
       }
       공개 int getAge () {
          귀국;
       }
       공개 문자열 getFavoriteColor () {
          return sFavColor;
       }
    // 게터 ... END
    공개 UserConfig build () {
       반환 (신규 UserConfig (this));
    }
}

블라인드 빌더로 클래스 인스턴스화

UserConfig uc = new UserConfig_Cfg ( "Kermit"). age (50) .favoriteColor ( "green"). build ();

유일한 차이점은 " UserConfig_Cfg"대신 " UserConfig.Cfg"입니다

노트

단점 :

  • 블라인드 빌더는 해당 ToBeBuilt클래스의 개인 멤버에 액세스 할 수 없습니다 .
  • 게터가 이제 빌더와 인터페이스 모두에서 필요하므로 더 장황합니다.
  • 단일 수업에 대한 모든 것이 더 이상 한 곳에 있지 않습니다 .

블라인드 빌더 컴파일은 간단합니다 :

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Fieldable인터페이스는 전적으로 선택 사항입니다

A에 대한 ToBeBuilt몇 가지 필수 필드와 클래스 - 이와 같은 UserConfig예를 들어, 클래스, 생성자는 단순히 수

공개 UserConfig (문자열 s_name, int i_age, 문자열 s_favColor) {

그리고 빌더와 함께 호출

공개 UserConfig build () {
   반환 (새 UserConfig (getName (), getAge (), getFavoriteColor ()));
}

또는 (빌더에서) 게터를 모두 제거하여 :

   반환 (새로운 UserConfig (sName, iAge, sFavoriteColor));

필드를 직접 전달함으로써 ToBeBuilt클래스는 Fieldable인터페이스 와 마찬가지로 "맹인"(빌더를 알지 못함)합니다 . 그러나위한 ToBeBuilt의도하고 클래스 "확장 여러 번 서브 - 확장"(이 게시물의 제목에있는)에 대한 변경 어떤 필드를 필요로 변경 모든 서브 클래스, 모든 빌더와 ToBeBuilt생성자를. 필드 및 서브 클래스 수가 증가함에 따라 유지 관리가 비실용적이됩니다.

(실제로 필요한 필드가 거의 없기 때문에 빌더를 사용하는 것은 전혀 과잉 일 수 있습니다. 관심있는 사람들을 위해 내 개인 라이브러리의 더 큰 Fieldable 인터페이스 중 일부를 샘플링했습니다 .)

하위 패키지의 보조 클래스

나는 Fieldable모든 블라인드 빌더를 위해 모든 빌더와 클래스를 클래스의 하위 패키지로 선택했습니다 ToBeBuilt. 하위 패키지의 이름은 항상 " z"입니다. 이렇게하면이 보조 클래스가 JavaDoc 패키지 목록을 복잡하게 만들 수 없습니다. 예를 들어

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

검증 예

위에서 언급했듯이 모든 유효성 검사는 ToBeBuilt의 생성자 에서 발생합니다 . 다음은 유효성 검사 코드 예제가있는 생성자입니다.

공개 UserConfig (UserConfig_Fieldable uc_f) {
   //이전
      {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         새로운 NullPointerException을 던집니다 ( "uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // 유효성 확인 (실제로 패턴을 사전 컴파일해야합니다 ...)
      {
         if (! Pattern.compile ( "\\ w +"). matcher (sName) .matches ()) {
            새로운 IllegalArgumentException 던지기 ( "uc_f.getName () (\" "+ sName +"\ ")는 비워 둘 수 없으며 문자 숫자와 밑줄 만 포함해야합니다.");
         }
      } catch (NullPointerException rx) {
         새로운 NullPointerException을 던집니다 ( "uc_f.getName ()");
      }
      if (iAge <0) {
         새 IllegalArgumentException을 던집니다 ( "uc_f.getAge () ("+ iAge + ")가 0보다 작습니다.");
      }
      {
         if (! Pattern.compile ( "(? : red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
            새로운 IllegalArgumentException을 던집니다 ( "uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +"\ ")는 빨강, 파랑, 녹색 또는 핫 핑크가 아닙니다.");
         }
      } catch (NullPointerException rx) {
         새 NullPointerException을 던집니다 ( "uc_f.getFavoriteColor ()");
      }
}

문서 작성기

이 섹션은 Bloch Builder 및 Blind Builder 모두에 적용됩니다. 이 디자인에서 클래스를 문서화하는 방법을 보여줍니다. 세터 (빌더)와 게터 ( ToBeBuilt클래스)는 마우스 클릭 한 번으로 사용자가 어디를 알 필요없이 서로 직접 상호 참조하도록합니다. 이러한 기능은 실제로 존재하며 개발자가 중복으로 문서를 작성하지 않아도됩니다.

게터 : ToBeBuilt수업에서만

Getter는 ToBeBuilt수업 에서만 문서화됩니다 . _Fieldable_Cfg 클래스 에서 동등한 getter 는 무시됩니다. 나는 그것들을 전혀 문서화하지 않았다.

/ **
   <P> 사용자의 나이 </ P>
   @return 사용자의 나이를 나타내는 int입니다.
   @See UserConfig_Cfg # age (int)
   @ getName () 참조
 ** /
공개 int getAge () {
   귀국;
}

첫 번째 @see는 빌더 클래스에있는 해당 setter에 대한 링크입니다.

세터 : 빌더 클래스에서

세터는 문서화 가있는 것처럼 ToBeBuilt클래스 , 또한 것처럼 (실제로 수행되는 검증 수행 ToBeBuilt의 생성자를). 별표 ( " *")는 링크의 대상이 다른 클래스에 있음을 나타내는 시각적 단서입니다.

/ **
   <P> 사용자의 나이를 설정하십시오. </ P>
   @param i_age 0보다 작을 수 없습니다. {@code UserConfig # getName () getName ()} *으로 가져옵니다.
   @see #favoriteColor (문자열)
 ** /
공개 UserConfig_Cfg 연령 (int i_age) {
   iAge = i_age;
   이것을 돌려줍니다;
}

추가 정보

종합 : 전체 문서와 함께 Blind Builder 예제의 전체 소스

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> 사용자 정보-<I> [빌더 : UserConfig_Cfg] </ I> </ P>
   <P>이 클래스 생성자에서 모든 필드의 유효성 검사가 수행됩니다. 그러나 각 유효성 검증 요구 사항은 빌더의 세터 기능에만 문서화되어 있습니다. </ P>
   <P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
공개 클래스 UserConfig {
   공개 정적 최종 무효 main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ( "Kermit"). age (50) .favoriteColor ( "green"). build ();
      System.out.println (uc);
   }
   개인 최종 문자열 sName;
   개인 최종 정보;
   개인 최종 문자열 sFavColor;
   / **
      <P> 새 인스턴스를 만듭니다. 모든 필드를 설정하고 확인합니다. </ P>
      @param uc_f {@code null}이 아닐 수 있습니다.
    ** /
   공개 UserConfig (UserConfig_Fieldable uc_f) {
      //이전
         {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            새로운 NullPointerException을 던집니다 ( "uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // 확인
         {
            if (! Pattern.compile ( "\\ w +"). matcher (sName) .matches ()) {
               새로운 IllegalArgumentException 던지기 ( "uc_f.getName () (\" "+ sName +"\ ")는 비워 둘 수 없으며 문자 숫자와 밑줄 만 포함해야합니다.");
            }
         } catch (NullPointerException rx) {
            새로운 NullPointerException을 던집니다 ( "uc_f.getName ()");
         }
         if (iAge <0) {
            새 IllegalArgumentException을 던집니다 ( "uc_f.getAge () ("+ iAge + ")가 0보다 작습니다.");
         }
         {
            if (! Pattern.compile ( "(? : red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
               새로운 IllegalArgumentException을 던집니다 ( "uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +"\ ")는 빨강, 파랑, 녹색 또는 핫 핑크가 아닙니다.");
            }
         } catch (NullPointerException rx) {
            새 NullPointerException을 던집니다 ( "uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> 사용자 이름. </ P>
         @return {@code null}, 비어 있지 않은 문자열입니다.
         @See UserConfig_Cfg # UserConfig_Cfg (문자열)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      공개 문자열 getName () {
         sName을 반환합니다.
      }
      / **
         <P> 사용자의 나이 </ P>
         @return 0보다 크거나 같은 숫자입니다.
         @See UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      공개 int getAge () {
         귀국;
      }
      / **
         <P> 사용자가 좋아하는 색상. </ P>
         @return {@code null}, 비어 있지 않은 문자열입니다.
         @See UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      공개 문자열 getFavoriteColor () {
         return sFavColor;
      }
   // 게터 ... END
   공개 문자열 toString () {
      "getName () ="+ getName () + ", getAge () ="+ getAge () + ", getFavoriteColor () ="+ getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> {@ link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable) 생성자}에 필요합니다. </ P>
 ** /
공용 인터페이스 UserConfig_Fieldable {
   문자열 getName ();
   int getAge ();
   문자열 getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> {@ link UserConfig} 용 빌더. </ P>
   <P> 모든 필드의 유효성 검사는 <CODE> UserConfig </ CODE> 생성자에서 발생합니다. 그러나 각 유효성 검사 요구 사항은이 클래스 세터 함수에서만 문서화됩니다. </ P>
 ** /
공개 클래스 UserConfig_Cfg는 UserConfig_Fieldable {
   공개 문자열 sName;
   공개 정보;
   공개 문자열 sFavColor;
   / **
      <P> 사용자 이름으로 새 인스턴스를 만듭니다. </ P>
      @param s_name {@code null}이거나 비어있을 수 없으며 문자, 숫자 및 밑줄 만 포함해야합니다. {@code UserConfig # getName () getName ()} {@ code ()}로 얻으십시오 .
    ** /
   공개 UserConfig_Cfg (문자열 s_name) {
      sName = s_name;
   }
   // 자기 귀환 세터 ... START
      / **
         <P> 사용자의 나이를 설정하십시오. </ P>
         @param i_age 0보다 작을 수 없습니다. {@code UserConfig # getName () getName ()} {@ code ()}로 얻으십시오 .
         @see #favoriteColor (문자열)
       ** /
      공개 UserConfig_Cfg 연령 (int i_age) {
         iAge = i_age;
         이것을 돌려줍니다;
      }
      / **
         <P> 사용자가 선호하는 색상을 설정하십시오. </ P>
         @param s_color {@code "red"}, {@code "blue"}, {@code green} 또는 {@code "hot pink"} 여야합니다. {@code UserConfig # getName () getName ()} {@ code ()} *로 가져옵니다.
         @ 참조 # 나이 (int)
       ** /
      공개 UserConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         이것을 돌려줍니다;
      }
   // 자기 귀환 세터 ... END
   //getters...START
      공개 문자열 getName () {
         sName을 반환합니다.
      }
      공개 int getAge () {
         귀국;
      }
      공개 문자열 getFavoriteColor () {
         return sFavColor;
      }
   // 게터 ... END
   / **
      <P> 구성된대로 UserConfig를 빌드하십시오. </ P>
      @return <CODE> (신규 {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </ CODE>
    ** /
   공개 UserConfig build () {
      반환 (신규 UserConfig (this));
   }
}


1
확실히, 그것은 개선입니다. 여기에 구현 된 Bloch 's Builder는 두 가지 구체적인 클래스를 결합합니다 .이 클래스는 빌드 할 클래스 와 빌더입니다. 이것은 나쁜 디자인 그 자체 입니다. 블라인드 빌더는 빌드 할 클래스가 생성 종속성을 추상화 로 정의하여 다른 클래스가 분리 된 방식으로 구현할 수 있도록하여 결합을 구분합니다. 필수적인 객체 지향 디자인 지침을 크게 적용했습니다.
rucamzu

3
아직 알고리즘 디자인이 없다면 아직 어딘가에 블로그를 게시해야합니다! 나는 지금 그것을 공유하고 있습니다 :-).
Martijn Verburg

4
친절한 말 감사합니다. :이 지금 내 새 블로그의 첫 번째 게시물입니다 aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind

빌더와 빌드 된 오브젝트가 모두 Fieldable을 구현하는 경우, 패턴은 변경 가능 항목을 빌더의 "빌드"멤버로 만드는 방법이 아니라 ReadableFoo / MutableFoo / ImmutableFoo라고하는 것과 유사하게 시작합니다. 그것을 호출 asImmutable하고 ReadableFoo인터페이스에 포함 시키십시오 [그 철학을 사용하여 build불변의 객체를 호출 하면 단순히 동일한 객체에 대한 참조를 반환합니다].
supercat

1
@ThomasN *_Fieldable새로운 getter 를 확장 및 추가 하고을 확장 하고 *_Cfg새로운 setter를 추가해야하지만 기존 getter 및 setter를 재현 해야하는 이유는 모르겠습니다. 그들은 상속 받았으며, 다른 기능이 필요하지 않으면 다시 만들 필요가 없습니다.
aliteralmind

13

나는 여기서 질문이 그것을 증명하려고 시도하지 않고 처음부터 무언가를 가정한다고 생각합니다. 빌더 패턴은 본질적으로 좋습니다.

tl; dr 나는 빌더 패턴이 좋은 아이디어 라고 생각 하지 않습니다 .


빌더 패턴 목적

빌더 패턴의 목적은 클래스를보다 쉽게 ​​소비 할 수있는 두 가지 규칙을 유지하는 것입니다.

  1. 일관되지 않은 / 사용할 수없는 / 유효하지 않은 상태로 객체를 구성 할 수 없어야합니다.

    • 이 인스턴스에 대한 시나리오를 참조 Person객체가이 것없이 구성 될 수있다 Id개체 수 코드의 모든 조각이 그 사용하면서 채워 필요Id 와 단지에 제대로 일을 Person.
  2. 객체 생성자에 너무 많은 매개 변수가 필요하지 않아야합니다 .

따라서 빌더 패턴의 목적은 논란의 여지없이 좋습니다. 나는 그것의 욕구와 사용의 대부분이 기본적으로 지금까지 진행된 분석에 기초한다고 생각합니다. 우리는이 두 가지 규칙을 원합니다. 이것은이 두 가지 규칙을 제공합니다.


왜 다른 접근법을보고 귀찮게합니까?

나는 그 이유가이 질문 자체의 사실에 의해 잘 드러났다고 생각한다. 빌더 패턴을 적용 할 때 구조에 복잡성과 많은 의식이 추가됩니다. 이 질문은 복잡성이 자주 발생하는 것처럼 이상하게 (상속되는) 시나리오를 만들기 때문에 해당 복잡성 중 일부를 해결하는 방법을 묻습니다. 이러한 복잡성으로 인해 유지 관리 오버 헤드가 증가합니다 (속성 추가, 변경 또는 제거가 다른 방식보다 훨씬 복잡함).


다른 접근법

위의 규칙 1의 경우 어떤 접근 방식이 있습니까? 이 규칙이 언급하는 핵심은 시공시 객체가 올바르게 작동하는 데 필요한 모든 정보를 가지고 있다는 것입니다. 시공 후에는 정보를 외부에서 변경할 수 없으므로 불변 정보입니다.

생성시 오브젝트에 필요한 모든 정보를 제공하는 한 가지 방법은 생성자에 매개 변수를 추가하는 것입니다. 해당 정보가 생성자에 의해 요구되는 경우, 모든 정보없이이 오브젝트를 구성 할 수 없으므로 올바른 상태로 구성됩니다. 그러나 객체에 많은 정보가 필요하다면 어떻게해야합니까? 이런 경우라면, 이 접근법은 위의 규칙 # 2를 어길 것 입니다.

좋아, 또 뭐가있어? 글쎄, 객체가 일관된 상태를 유지하는 데 필요한 모든 정보를 가져 와서 구축 할 때 가져 오는 다른 객체로 묶을 수 있습니다. 그러면 빌더 패턴 대신 위의 코드는 다음과 같습니다.

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

이것은 약간 단순하지만 빌더 패턴과 크게 다르지 않으며 가장 중요한 것은 이제 규칙 # 1과 규칙 # 2를 만족시키는 것 입니다.

그렇다면 비트를 추가로 작성하여 빌더에서 완전하게 만드십시오. 단순히 불필요 합니다. 이 접근 방식에서 빌더 패턴의 두 가지 목적을 약간 더 단순하고 유지 관리하기 쉽고 재사용 가능한 것으로 만족 시켰습니다 . 마지막 비트가 핵심입니다.이 예제는 상상력이 뛰어나 실제 의미 론적 목적에 적합하지 않으므로이 접근법이 단일 목적 클래스가 아닌 재사용 가능한 DTO를 만드는 방법을 보여 드리겠습니다 .

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

따라서 이와 같은 응집력있는 DTO 를 구축 할 때 빌더 패턴의 목적을보다 간단하고 폭 넓은 가치 / 유용 도로 만족시킬 수 있습니다. 또한이 접근 방식은 빌더 패턴으로 인한 상속 복잡성을 해결합니다.

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

DTO가 항상 응집력이있는 것은 아니며 여러 DTO에 걸쳐 그룹화해야하는 속성 그룹을 응집성있게 만들 수 있습니다. 이것은 실제로 문제가되지 않습니다. 객체에 18 개의 속성이 필요하고 해당 속성으로 3 개의 응집력있는 DTO를 만들 수있는 경우 빌더의 목적에 맞는 간단한 구성을 얻을 수 있습니다. 응집성 그룹화를 할 수 없다면, 객체가 완전히 관련이없는 속성을 가진 경우 객체가 응집력이 없다는 신호 일 수 있습니다. 그러나 단순한 구현으로 인해 단일 비 응집성 DTO를 만드는 것이 더 좋습니다. 상속 문제 해결.


빌더 패턴을 개선하는 방법

좋아, 그래서 모든 럼블 럼블을 제쳐두고, 당신은 문제가 있고 그것을 해결하기위한 디자인 접근법을 찾고 있습니다. 내 제안 : 상속 클래스는 단순히 수퍼 클래스의 빌더 클래스에서 상속되는 중첩 클래스를 가질 수 있으므로 상속 클래스는 기본적으로 수퍼 클래스와 동일한 구조를 가지며 추가 함수와 정확히 동일하게 작동 해야하는 빌더 패턴을 갖습니다. 하위 클래스의 추가 속성


좋은 생각 일 때

제쳐두고 빌더 패턴에는 틈새가 있습니다. 우리는 모두 특정 시점에서이 특정 빌더를 배웠기 때문에 모두 알고 있습니다 StringBuilder.-여기서 문자열은 쉽게 구성하고 연결할 수 없기 때문에 단순한 구성이 아닙니다. 성능상의 이점이 있기 때문에 훌륭한 빌더입니다. .

따라서 성능상의 이점은 다음과 같습니다. 많은 객체가 있고 불변 유형이며 불변 유형의 한 객체로 축소해야합니다. 점진적으로 수행하면 여기에 많은 중간 개체가 만들어 지므로 한 번에 모두 수행하는 것이 훨씬 성능이 좋고 이상적입니다.

내가의 주요 생각 그래서 좋은 생각 일 때이 문제 도메인에있는 StringBuilder: 불변의 유형의 단일 인스턴스에 불변 유형의 여러 인스턴스를 설정하는 필요 .


나는 당신의 주어진 모범이 어느 규칙을 만족한다고 생각하지 않습니다. 잘못된 상태에서 Cfg를 만드는 것을 막을 수는 없으며 매개 변수가 ctor 밖으로 이동하는 동안 관용적이지 않고 더 장황한 곳으로 이동했습니다. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()foo를 빌드하기위한 간결한 API를 제공하며 빌더 자체에서 실제 오류 검사를 제공 할 수 있습니다. 빌더가 없으면 객체 자체가 입력을 확인해야하므로 예전보다 나아지지 않습니다.
Phoshi

DTO는 setter에서 주석을 사용하여 선언적으로 여러 가지 방법으로 속성의 유효성을 검사 할 수는 있지만 해결하려는 경우-유효성 검사는 별도의 문제이며 빌더 접근 방식에서 생성자에서 유효성 검사가 발생한다는 것을 보여 주므로 동일한 논리가 완벽하게 적합합니다. 내 접근 방식에서. 그러나 일반적으로 DTO를 사용하여 유효성을 검사하는 것이 좋습니다. 왜냐하면 DTO는 여러 유형을 구성하는 데 사용할 수 있으므로 유효성 검사를 수행하면 여러 유형의 유효성을 검사 할 수 있기 때문입니다. 빌더는 작성된 특정 유형에 대해서만 유효성을 검증합니다.
Jimmy Hoffa

가장 유연한 방법은 빌더에서 정적 유효성 검증 기능을 사용하는 것입니다.이 기능은 단일 Fieldable매개 변수 를 허용합니다 . 나는 로부터이 검증 함수를 호출 할 ToBeBuilt생성자를하지만, 그것은 어디서든, 무엇이든 호출 할 수 있습니다. 이를 통해 특정 구현을 강요하지 않고도 중복 코드의 가능성을 없앨 수 있습니다. ( Fieldable개념이 마음에 들지 않으면 개별 필드에서 유효성 검사 기능으로 전달하는 것을 막을 수있는 것은 없습니다. 그러나 이제는 필드 목록을 유지해야 할 곳이 적어도 곳이 있습니다.)
aliteralmind

+1 그리고 생성자에 너무 많은 의존성을 가진 클래스는 분명히 응집력이 없으며 더 작은 클래스로 리팩토링되어야합니다.
Basilevs

@JimmyHoffa : 아, 당신은 방금 그것을 생략했습니다. 나는 이것과 빌더의 차이점을 확신하지 못한다. 이것 외에는 어떤 빌더에서 .build를 호출하는 대신 구성 인스턴스를 ctor에 전달하고 빌더가 모든 것에 대한 정확성 검사를위한 더 확실한 경로를 가지고 있음을 확신하지 못한다. 자료. 각 개별 변수는 유효한 범위 내에 있지만 특정 순열에서는 유효하지 않습니다. .build는 이것을 확인할 수 있지만 항목을 ctor로 전달하면 객체 자체 내부에서 오류를 확인해야합니다.
Phoshi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.