Як покращити схему Builder від Bloch, зробити її більш доцільною для використання у високорозширюваних класах


34

На мене сильно вплинув Ефективна книга Java Джошуа Блоха (2-е видання), ймовірно, більше, ніж будь-яка книга програмування, яку я читав. Зокрема, найбільший ефект мав його зразок "Будівельник" (пункт 2).

Не дивлячись на те, що будівельник Блоха за два місяці мене значно відставав, ніж за останні десять років програмування, я все ще потрапляю в ту саму стіну: розширення занять методами ланцюгів, що повертаються, в кращому випадку відлякує, а в гіршому - кошмар. - особливо, коли дженерики вступають у гру, і особливо з самореференційними дженериками (такими як Comparable<T extends Comparable<T>>).

У мене є дві основні потреби, лише друга з яких я хотів би зупинитися на цьому питанні:

  1. Перша проблема полягає в тому, "як розділити ланцюги методів самоповернення, не потребуючи їх повторної реалізації в кожному ... одному ... класі?" Для тих, хто може бути цікавим, я звернувся до цієї частини внизу цього відповіді, але тут я не хочу зосередитися.

  2. Друга проблема, яку я прошу прокоментувати, - "як я можу впровадити конструктор у класи, які самі по собі мають бути розширені багатьма іншими класами?" Розширення класу з будівельником, природно, складніше, ніж розширення класу без. Розширення класу, який має конструктор, який також реалізує Needable, а отже, і пов'язані з ним значні дженерики , є непростим.

Отже, це моє питання: Як я можу вдосконалити (що я називаю) Bloch Builder, щоб я міг вільно приєднати будівельника до будь-якого класу - навіть коли цей клас мав на увазі "базовий клас", який може бути багаторазово розширений і подовжений - не відлякуючи свого майбутнього «я» або користувачів моєї бібліотеки , через додатковий багаж, який накладає їм будівельник (та його потенційні генеричні товари)?


Додаток
Моє запитання зосереджено на частині 2 вище, але я хотів трохи детальніше розглянути проблему, включаючи те, як я з нею впорався:

Перша проблема полягає в тому, "як розділити ланцюги методів самоповернення, не потребуючи їх повторної реалізації в кожному ... одному ... класі?" Це не перешкоджає розширенню класів від необхідності повторної реалізації цих ланцюгів, що, звичайно, вони повинні, скоріше, як запобігти необхідності повторного використання підкласів , які хочуть скористатися цими ланцюгами методів. -здійснювати кожну функцію, що повертається, для того, щоб їх користувачі могли ними скористатися? Для цього я придумав необхідний дизайн, який я надрукую тут для скелетів інтерфейсу, і покину це на цьому. Він добре працював для мене (ця конструкція була роками в процесі виготовлення ... Найважче було уникнути кругових залежностей):

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

Я створив те, що для мене - це велике вдосконалення в порівнянні з моделлю Builder Джоша Блоха. Ні в якому разі не сказати, що це "краще", тільки те, що в дуже специфічній ситуації це дає певні переваги - найбільша істота в тому, що він відокремлює будівельника від його класу, що будується.

Я детально задокументував цю альтернативу нижче, яку я називаю Шаблоном сліпого будівельника.


Шаблон дизайну: сліпий будівельник

Як альтернатива Шаблону будівельника Джошуа Блоха (пункт 2 в «Ефективна Java», 2-е видання), я створив те, що я називаю «Сліпий зразок будівельника», який розділяє багато переваг Bloch Builder і, окрім одного персонажа, використовується точно так само. Сліпі будівельники мають перевагу

  • роз'єднання будівельника від його класу, що оточує, усунення кругової залежності,
  • значно зменшує розмір вихідного коду (то , що більше не ) , що містить в клас, і
  • дозволяє ToBeBuiltрозширити клас без необхідності розширювати його конструктор .

У цій документації я буду називати створений клас як " ToBeBuilt" клас.

Клас, реалізований з Bloch Builder

Bloch Builder - це public static classміститься всередині класу, який він будує. Приклад:

публічний клас UserConfig {
   приватний фінал String sName;
   приватний заключний int iAge;
   приватний фінал String sFavColor;
   загальнодоступний UserConfig (UserConfig.Cfg uc_c) {// КОНСТРУКТОР
      // передача
         спробуйте {
            sName = uc_c.sName;
         } улов (NullPointerException rx) {
            кинути новий NullPointerException ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // ОЦІНЮЙТЕ ВСІ ПОЛИ ТУТ
   }
   public String toString () {
      повернути "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   // будівельник ...START
   публічний статичний клас Cfg {
      приватне ім'я String sName;
      private int iAge;
      приватний String sFavColor;
      загальнодоступний Cfg (ім'я рядка) {
         sName = s_name;
      }
      // Автомати, що повертаються ... СТАРТ
         загальнодоступний вік Cfg (int i_age) {
            iAge = i_age;
            повернути це;
         }
         public Cfg favoriteColor (String s_color) {
            sFavColor = s_color;
            повернути це;
         }
      // Самостійно повертаються сетери ... END
      public UserConfig build () {
         повернення (новий UserConfig (це));
      }
   }
   // будівельник ...END
}

Миттєвий пошук класу з Bloch Builder

UserConfig uc = новий UserConfig.Cfg ("Kermit"). Вік (50) .favoriteColor ("зелений"). Build ();

Той самий клас, реалізований як Blind Builder

У Blind Builder є три частини, кожна з яких знаходиться в окремому файлі вихідного коду:

  1. ToBeBuiltКлас (в даному прикладі: UserConfig)
  2. Його " Fieldable" інтерфейс
  3. Будівельник

1. Побудований клас

Клас, що будується, приймає свій Fieldableінтерфейс як єдиний конструкторський параметр. Конструктор встановлює з нього всі внутрішні поля та перевіряє кожне. Найголовніше, що цей ToBeBuiltклас не знає свого будівельника.

публічний клас UserConfig {
   приватний фінал String sName;
   приватний заключний int iAge;
   приватний фінал String sFavColor;
    загальнодоступний UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      // передача
         спробуйте {
            sName = uc_f.getName ();
         } улов (NullPointerException rx) {
            кинути новий NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // ОЦІНЮЙТЕ ВСІ ПОЛИ ТУТ
   }
   public String 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конструктор «S. Єдиний спосіб гарантувати, що поля є дійсними - припускаючи, що будівельник не може якось "заблокувати" свій стан, - це ToBeBuiltклас зробити остаточну перевірку.

Нарешті, як і в Fieldableінтерфейсі, я не документую жодного з його пристроїв.

публічний клас UserConfig_Cfg реалізує UserConfig_Fieldable {
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (ім'я рядка) {
       sName = s_name;
    }
    // Автомати, що повертаються ... СТАРТ
       public UserConfig_Cfg вік (int i_age) {
          iAge = i_age;
          повернути це;
       }
       public UserConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          повернути це;
       }
    // Самостійно повертаються сетери ... END
    //getters...START
       public String getName () {
          повернути sName;
       }
       public int getAge () {
          повернути iAge;
       }
       public String getFavoriteColor () {
          повернути sFavColor;
       }
    //getters...END
    public UserConfig build () {
       повернення (новий UserConfig (це));
    }
}

Миттєвий пошук класу з сліпим будівельником

UserConfig uc = новий UserConfig_Cfg ("Kermit"). Вік (50) .favoriteColor ("зелений"). Build ();

Єдина відмінність - " UserConfig_Cfg" замість " UserConfig.Cfg"

Примітки

Недоліки:

  • Сліпі будівельники не можуть отримати доступ до приватних членів свого ToBeBuiltкласу,
  • Вони більш багатослівні, оскільки в даний час необхідні геттери як в програмі, так і в інтерфейсі.
  • Все для одного класу вже не в одному місці .

Складання сліпого Builder прямо:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

FieldableІнтерфейс абсолютно НЕ обов'язково

Для ToBeBuiltкласу з кількома необхідними полями - наприклад, цей UserConfigклас прикладу, конструктор може бути просто

public UserConfig (рядок s_name, int i_age, String s_favColor) {

І подзвонив у будівельник с

public UserConfig build () {
   return (новий UserConfig (getName (), getAge (), getFavoriteColor ()));
}

Або взагалі усунувши геттери (у будівельнику):

   повернення (новий UserConfig (sName, iAge, sFavoriteColor));

Передаючи поля безпосередньо, ToBeBuiltклас настільки ж «сліпий» (не знаючи про його конструктора), як і з Fieldableінтерфейсом. Однак, для ToBeBuiltкласів, які мають бути "розширені та подовжені багато разів" (що є в заголовку цієї посади), будь-які зміни в будь-якому полі потребують змін у кожному підкласі, у кожному будівельнику та ToBeBuiltконструкторі. Зі збільшенням кількості полів і підкласів це підтримувати недоцільно.

(Дійсно, за допомогою кількох необхідних полів використання конструктора взагалі може бути надмірним. Для тих, хто цікавиться, ось вибірка деяких більших інтерфейсів Fielvable у моїй персональній бібліотеці.)

Середні класи в підпакеті

Я вибираю, щоб усі будівельники та Fieldableкласи для всіх сліпих будівельників були в підпакеті їхнього ToBeBuiltкласу. Підпакет завжди називається " z". Це не дозволяє цим середнім класам захаращувати список пакетів JavaDoc. Наприклад

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

Приклад перевірки

Як було сказано вище, вся перевірка відбувається в ToBeBuiltконструкторі s. Ось конструктор знову з прикладом перевірки коду:

загальнодоступний UserConfig (UserConfig_Fieldable uc_f) {
   // передача
      спробуйте {
         sName = uc_f.getName ();
      } улов (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 +" \ ") може бути порожнім і повинен містити лише цифри цифр і підкреслення.");
         }
      } улов (NullPointerException rx) {
         кинути новий NullPointerException ("uc_f.getName ()");
      }
      якщо (iAge <0) {
         кинути новий IllegalArgumentException ("uc_f.getAge () (" + iAge + ") менше нуля.");
      }
      спробуйте {
         if (! Pattern.compile ("(?: червоний | синій | зелений | | | гаряче рожевий)"). matcher (sFavColor) .matches ()) {
            киньте новий IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") не червоний, синій, зелений або гарячий рожевий.");
         }
      } улов (NullPointerException rx) {
         кинути новий NullPointerException ("uc_f.getFavoriteColor ()");
      }
}

Документування будівельників

Цей розділ застосований як для будівельників Bloch, так і для сліпих будівельників. Це демонструє, як я документую класи в цій конструкції, роблячи сеттери (у конструкторі) та їх гетьтери (у ToBeBuiltкласі) безпосередньо перехресними посиланнями один на одного - одним клацанням миші та без потреби користувача знати, де ці функції фактично знаходяться - і без того, щоб розробник мав нічого зайвого документувати.

Геттери: Тільки на ToBeBuiltзаняттях

Геттери задокументовані лише на ToBeBuiltуроці. Еквівалентні отримувачі як у, так _Fieldableі в_Cfg класах ігноруються. Я їх взагалі не документую.

/ **
   <P> Вік користувача. </P>
   @return Інт, що відображає вік користувача.
   @see UserConfig_Cfg # age (int)
   @ see getName ()
 ** /
public int getAge () {
   повернути iAge;
}

Перший @see- посилання на його сетер, який знаходиться в класі будівельників.

Розкладники: У класі будівельника

Сеттер задокументовано так, ніби він знаходиться в ToBeBuiltкласі , а також як би він робить валідацію (що дійсно робиться ToBeBuiltконструктором 's). Зірочка (" *") - це візуальна підказка, що вказує, що ціль посилання знаходиться в іншому класі.

/ **
   <P> Встановити вік користувача. </P>
   @param i_age Не може бути менше нуля. Отримайте {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (Рядок)
 ** /
public UserConfig_Cfg вік (int i_age) {
   iAge = i_age;
   повернути це;
}

Додаткова інформація

Зведення все це разом: Повне джерело прикладу «Сліпий будівельник» з повною документацією

UserConfig.java

імпорт java.util.regex.Pattern;
/ **
   <P> Інформація про користувача - <I> [builder: UserConfig_Cfg] </I> </P>
   <P> У цьому конструкторі класів відбувається перевірка всіх полів. Однак кожна вимога перевірки є документом лише у функціях встановлення будівельника. </P>
   <P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
 ** /
публічний клас UserConfig {
   public static final void main (String [] ігнорується) {
      UserConfig uc = новий UserConfig_Cfg ("Kermit"). Вік (50) .favoriteColor ("зелений"). Build ();
      System.out.println (uc);
   }
   приватний фінал String sName;
   приватний заключний int iAge;
   приватний фінал String sFavColor;
   / **
      <P> Створіть новий екземпляр. Це встановлює та підтверджує всі поля. </P>
      @param uc_f Не може бути {@code null}.
    ** /
   загальнодоступний UserConfig (UserConfig_Fieldable uc_f) {
      // передача
         спробуйте {
            sName = uc_f.getName ();
         } улов (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 +" \ ") може бути порожнім і повинен містити лише цифри цифр і підкреслення.");
            }
         } улов (NullPointerException rx) {
            кинути новий NullPointerException ("uc_f.getName ()");
         }
         якщо (iAge <0) {
            кинути новий IllegalArgumentException ("uc_f.getAge () (" + iAge + ") менше нуля.");
         }
         спробуйте {
            if (! Pattern.compile ("(?: червоний | синій | зелений | | | гаряче рожевий)"). matcher (sFavColor) .matches ()) {
               киньте новий IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") не червоний, синій, зелений або гарячий рожевий.");
            }
         } улов (NullPointerException rx) {
            кинути новий NullPointerException ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Ім'я користувача. </P>
         @return Не - {@ код null}, не порожній рядок.
         @see UserConfig_Cfg # UserConfig_Cfg (Рядок)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      public String getName () {
         повернути sName;
      }
      / **
         <P> Вік користувача. </P>
         @return Число, яке більше, ніж або рівно нулю.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public int getAge () {
         повернути iAge;
      }
      / **
         <P> Улюблений колір користувача. </P>
         @return Не - {@ код null}, не порожній рядок.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public String getFavoriteColor () {
         повернути sFavColor;
      }
   //getters...END
   public String 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

імпорт java.util.regex.Pattern;
/ **
   <P> Конструктор для {@link UserConfig}. </P>
   <P> Перевірка всіх полів відбувається в конструкторі <CODE> UserConfig </CODE>. Однак кожна вимога перевірки є документом лише у цих функціях встановлення класів. </P>
 ** /
публічний клас UserConfig_Cfg реалізує UserConfig_Fieldable {
   public String sName;
   public int iAge;
   public String sFavColor;
   / **
      <P> Створіть новий екземпляр із іменем користувача. </P>
      @param s_name Не може бути {@code null} або порожнім, а він повинен містити лише літери, цифри та підкреслення. Отримайте {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   public UserConfig_Cfg (ім'я рядка) {
      sName = s_name;
   }
   // Автомати, що повертаються ... СТАРТ
      / **
         <P> Встановити вік користувача. </P>
         @param i_age Не може бути менше нуля. Отримайте {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (Рядок)
       ** /
      public 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 ()} *.
         @see #age (int)
       ** /
      public UserConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         повернути це;
      }
   // Самостійно повертаються сетери ... END
   //getters...START
      public String getName () {
         повернути sName;
      }
      public int getAge () {
         повернути iAge;
      }
      public String getFavoriteColor () {
         повернути sFavColor;
      }
   //getters...END
   / **
      <P> Створіть UserConfig, як налаштовано. </P>
      @return <CODE> (новий {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (це) </CODE>
    ** /
   public UserConfig build () {
      повернення (новий UserConfig (це));
   }
}


1
Однозначно, це вдосконалення. Як реалізовано тут, Bloch's Builder з'єднує два конкретні класи, це будується та її будівельник. Це поганий дизайн сам по собі . Blind Builder ви описуєте розбиття цього з’єднання, будуючи побудований клас, визначають його конструктивну залежність як абстракцію , яку інші класи можуть реалізовувати нероздільно. Ви чудово застосували основні об'єктно-орієнтовані настанови дизайну.
rucamzu

3
Вам слід десь вести щоденник про це, якщо ви ще цього не зробили, приємний фрагмент дизайну алгоритму! Я зараз ділюсь цим :-).
Martijn Verburg

4
Дякую за добрі слова. Це перша публікація у моєму новому блозі: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind

Якщо і конструктор, і об'єкти, що вбудовуються, реалізовують Fielvable, модель починає нагадувати ту, яку я назвав ReadableFoo / MutableFoo / ImmutableFoo, хоча замість того, щоб мати спосіб зробити предмет, що змінюється, бути членом "build" будівельника, я називати його asImmutableі включати його в ReadableFooінтерфейс [використовуючи цю філософію, виклик buildнезмінного об'єкта просто поверне посилання на той самий об'єкт].
supercat

1
@ThomasN Вам потрібно розширити *_Fieldableі додати до нього нові геттери, розширити *_Cfgі додати нові сетери, але я не розумію, чому вам потрібно було б відтворити існуючі геттери та сетери. Вони передаються у спадок, і якщо їм не потрібна інша функціональність, відтворювати їх не потрібно.
aliteralmind

13

Я думаю, що питання тут передбачає щось із самого початку, не намагаючись довести це, що модель будівельника по суті хороша.

tl; dr Я думаю, що модель для будівельників дуже рідко, якщо коли-небудь хороша ідея.


Мета шаблону будівельника

Мета схеми для розробників - підтримувати два правила, які полегшать споживання вашого класу:

  1. Об'єкти не повинні бути побудовані в непостійних / непридатних / недійсних станах.

    • Це відноситься до сценаріїв , де, наприклад, Personоб'єкт може бути побудований без він Idзаповнюється, в той час як всі шматки коду , які використовують цей об'єкт може знадобитися в Idтільки для правильної роботи з Person.
  2. Конструктори об'єктів не повинні вимагати занадто багато параметрів .

Таким чином, призначення структури будівельника не є суперечливо хорошим. Я думаю, що багато бажання та використання цього базується на аналізі, який в основному зайшов так далеко: ми хочемо, щоб ці два правила були даними, але це дає ці два правила - хоча я думаю, що варто вивчити інші способи їх виконання.


Чому набридає дивитися на інші підходи?

Я думаю, що причину добре показує сам факт цього питання; Структура при застосуванні шаблону будівельника до структур додається багато. Це питання задає питання про те, як вирішити частину цієї складності, оскільки, як і складність, це робить сценарій, який веде себе дивно (успадковуючи). Ця складність також збільшує накладні витрати на обслуговування (додавання, зміна або видалення властивостей набагато складніше, ніж інакше).


Інші підходи

Отже, для правила номер один вище, які підходи існують? Основне, на яке посилається це правило, полягає в тому, що при будівництві об'єкт має всю інформацію, необхідну для його належного функціонування - і після побудови цю інформацію не можна змінювати зовні (тому це незмінна інформація).

Один із способів надати всю необхідну інформацію об’єкту при будівництві - це просто додати параметри до конструктора. Якщо конструктор вимагає цієї інформації, ви не зможете сконструювати цей об'єкт без усієї цієї інформації, тому він буде побудований у дійсному стані. Але що робити, якщо об’єкт вимагає багато інформації, щоб бути дійсною? О даге, якщо це так, цей підхід порушить правило №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()пропонує стислий API для створення foos та може запропонувати фактичну перевірку помилок у самому будівельнику. Без будівельника сам об'єкт повинен перевірити свої входи, а це означає, що ми не краще, ніж раніше.
Фоши

DTO можуть їх властивості численно підтверджені декларативно з анотаціями на сетері, однак, ви хочете піти про це - перевірка - це окрема проблема, і в його підході до конструктора він показує, що в конструкторі відбувається перевірка, та сама логіка цілком відповідає в моєму підході. Однак, як правило, було б краще використовувати DTO для його підтвердження, тому що, як я показую, DTO може використовуватися для побудови декількох типів, і тому, маючи перевірку, він би піддавався валідації декількох типів. Забудовник підтверджує лише той тип, для якого він створений.
Джиммі Хоффа

Мабуть, найбільш гнучким способом було б мати функцію статичної валідації в конструкторі, який приймає єдиний Fieldableпараметр. Я б назвав цю функцію перевірки від ToBeBuiltконструктора, але її можна було викликати будь-чим, з будь-якого місця. Це виключає можливість надмірного коду, не примушуючи до конкретної реалізації. (І ніщо не заважає вам переходити в окремих полях до функції перевірки, якщо вам Fieldableконцепція не подобається - але тепер було б щонайменше три місця, в яких слід було б підтримувати список полів.)
aliteralmind

+1 І клас, який має занадто багато залежностей у своєму конструкторі, очевидно, недостатньо згуртований, і його слід переробити на менші класи.
Василевс

@JimmyHoffa: Ах, бачу, ти це просто пропустив. Я не впевнений, що я бачу різницю між цим і будівельником, то, крім цього, передається екземпляр конфігурації в ctor замість виклику .build у якогось будівельника, і що у будівельника є більш очевидний шлях для перевірки правильності всіх дані. Кожна окрема змінна може бути в межах її дійсних діапазонів, але недійсна в цій конкретній перестановці. .build може перевірити це, але передача елемента в ctor вимагає перевірки помилок всередині самого об’єкта - прискіпливо!
Фоші
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.