Как улучшить шаблон Bloch's Builder Pattern, чтобы сделать его более подходящим для использования в расширяемых классах


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

Я создал то, что для меня является большим улучшением по сравнению с «Образцом строителя» Джоша Блоха. Ни в коем случае не сказать, что он «лучше», просто в очень специфической ситуации он дает некоторые преимущества - самое большое, что он отделяет строителя от класса, который будет построен.

Я подробно задокументировал эту альтернативу ниже, которую я называю «Слепой шаблон».


Шаблон дизайна: слепой строитель

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

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

В этой документации я буду ссылаться на создаваемый класс как " ToBeBuilt" класс.

Класс, реализованный с помощью Bloch Builder

Блох Строитель является public static class содержимое внутри создаваемого им класса. Пример:

открытый класс UserConfig {
   приватный финал String sName;
   закрытый финал int iAge;
   приватный финал String sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// КОНСТРУКТОР
      //перечислить
         пытаться {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            бросить новое NullPointerException ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // ПРОВЕРКА ВСЕХ ПОЛЕЙ ЗДЕСЬ
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   открытый статический класс Cfg {
      private String sName;
      private int iAge;
      приватная строка sFavColor;
      public Cfg (String s_name) {
         sName = s_name;
      }
      // самовозвратные сеттеры ... START
         public cfg age (int i_age) {
            iAge = i_age;
            верни это;
         }
         public Cfg favouriteColor (String s_color) {
            sFavColor = s_color;
            верни это;
         }
      // самовозвратные сеттеры ... END
      public UserConfig build () {
         return (новый UserConfig (this));
      }
   }
   //builder...END
}

Создание класса с помощью Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();

Тот же класс, реализованный как Blind Builder

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

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

1. Строящийся класс

Класс, который будет построен, принимает свой Fieldableинтерфейс как единственный параметр конструктора. Конструктор устанавливает все внутренние поля и проверяет каждое из них. Самое главное, этот ToBeBuiltкласс не знает своего строителя.

открытый класс UserConfig {
   приватный финал String sName;
   закрытый финал int iAge;
   приватный финал String sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// КОНСТРУКТОР
      //перечислить
         пытаться {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            бросить новое NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // ПРОВЕРКА ВСЕХ ПОЛЕЙ ЗДЕСЬ
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Как заметил один умный комментатор (который необъяснимым образом удалил свой ответ), если ToBeBuiltкласс также реализует его Fieldable, его единственный конструктор может использоваться как в качестве основного, так и копирующего конструктора (недостатком является то, что поля всегда проверяются, даже если известно, что поля в оригинале ToBeBuiltдействительны).

2. Интерфейс " Fieldable"

Полевой интерфейс - это «мост» между ToBeBuiltклассом и его создателем, определяющий все поля, необходимые для построения объекта. Этот интерфейс требуется ToBeBuiltконструктором классов и реализуется сборщиком. Поскольку этот интерфейс может быть реализован другими классами, кроме компоновщика, любой класс может легко создать экземпляр ToBeBuiltкласса без необходимости использовать его компоновщик. Это также облегчает расширение ToBeBuiltкласса, когда расширение его компоновщика нежелательно или необходимо.

Как описано в следующем разделе, я не документирую функции этого интерфейса вообще.

открытый интерфейс UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

3. Строитель

Строитель реализует Fieldableкласс. Он вообще не проверяет, и, чтобы подчеркнуть этот факт, все его поля являются открытыми и изменчивыми. Хотя эта общедоступная доступность не является обязательной, я предпочитаю и рекомендую ее, поскольку она усиливает тот факт, что проверка не происходит до тех пор, пока не ToBeBuiltбудет вызван конструктор. Это важно, потому что это возможно для другого потока манипулировать построитель дальше, прежде чем он будет передан в ToBeBuiltконструктор «S. Единственный способ гарантировать, что поля действительны - при условии, что сборщик не может каким-либо образом «заблокировать» свое состояние - это сделать для ToBeBuiltкласса окончательную проверку.

Наконец, как и с Fieldableинтерфейсом, я не документирую ни одного из его получателей.

открытый класс UserConfig_Cfg реализует UserConfig_Fieldable {
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (String s_name) {
       sName = s_name;
    }
    // самовозвратные сеттеры ... START
       public UserConfig_Cfg age (int i_age) {
          iAge = i_age;
          верни это;
       }
       public UserConfig_Cfg favouriteColor (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 () {
       return (новый UserConfig (this));
    }
}

Создание класса с помощью слепого строителя

UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();

Единственная разница - UserConfig_Cfg«вместо UserConfig.Cfg»

Заметки

Недостатки:

  • Слепые строители не могут получить доступ к закрытым членам своего ToBeBuiltкласса,
  • Они более многословны, поскольку геттеры теперь требуются как в сборщике, так и в интерфейсе.
  • Все для одного класса больше не в одном месте .

Компилировать Blind Builder просто:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

FieldableИнтерфейс совершенно не обязательно

Для ToBeBuiltкласса с несколькими обязательными полями - такого как этот UserConfigпример класса, конструктор может быть просто

public UserConfig (String s_name, int i_age, String s_favColor) {

И позвонил в строителя с

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

Или даже исключив геттеры (в сборщике):

   return (новый 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конструкторе. Вот снова конструктор с примером кода проверки:

public 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 ()) {
            throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") не может быть пустым и должен содержать только буквы, цифры и подчеркивания.");
         }
      } catch (NullPointerException rx) {
         бросить новое исключение NullPointerException ("uc_f.getName ()");
      }
      if (iAge <0) {
         бросить новое IllegalArgumentException ("uc_f.getAge () (" + iAge + ") меньше нуля.");
      }
      пытаться {
         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 Builders, так и к Blind Builders. Он демонстрирует, как я документирую классы в этом проекте, делая сеттеры (в компоновщике) и их геттеры (в ToBeBuiltклассе), имеющие прямые ссылки друг на друга - одним щелчком мыши, и пользователю не нужно знать, где эти функции на самом деле находятся - и без необходимости разработчика ничего документировать.

Добытчики: ToBeBuiltтолько в классах

Геттеры документированы только в ToBeBuiltклассе. Эквивалентные добытчики как в условиях _Fieldableи_Cfg классов игнорируются. Я не документирую их вообще.

/ **
   <P> Возраст пользователя. </ P>
   @return Инт, представляющий возраст пользователя.
   @see UserConfig_Cfg # age (int)
   @ смотри getName ()
 ** /
public int getAge () {
   вернуть iAge;
}

Первая @see- это ссылка на его установщик, который находится в классе построителя.

Сеттеры: в классе строителей

Сеттер задокументирован так, как если бы он находился в ToBeBuiltклассе , а также как если бы он выполнял валидацию (что на самом деле выполняется ToBeBuiltконструктором). Звездочка (" *") - это визуальная подсказка, указывающая, что цель ссылки находится в другом классе.

/ **
   <P> Установите возраст пользователя. </ P>
   @param i_age Не может быть меньше нуля. Получить с {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (String)
 ** /
public UserConfig_Cfg age (int i_age) {
   iAge = i_age;
   верни это;
}

Дальнейшая информация

Собираем все вместе: полный исходный код примера Blind Builder с полной документацией

UserConfig.java

import 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 [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();
      System.out.println (ЯК);
   }
   приватный финал String sName;
   закрытый финал int iAge;
   приватный финал String sFavColor;
   / **
      <P> Создайте новый экземпляр. Это устанавливает и проверяет все поля. </ P>
      @param uc_f Не может быть {@code null}.
    ** /
   public 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 ()) {
               throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") не может быть пустым и должен содержать только буквы, цифры и подчеркивания.");
            }
         } catch (NullPointerException rx) {
            бросить новое исключение NullPointerException ("uc_f.getName ()");
         }
         if (iAge <0) {
            бросить новое IllegalArgumentException ("uc_f.getAge () (" + iAge + ") меньше нуля.");
         }
         пытаться {
            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 (String)
         @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 Не - {@ code null}, непустая строка.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public String getFavoriteColor () {
         вернуть sFavColor;
      }
   //getters...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Требуется конструктором {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </ P>
 ** /
открытый интерфейс UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

UserConfig_Cfg.java

import 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 (String s_name) {
      sName = s_name;
   }
   // самовозвратные сеттеры ... START
      / **
         <P> Установите возраст пользователя. </ P>
         @param i_age Не может быть меньше нуля. Получить с {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (String)
       ** /
      public UserConfig_Cfg age (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 favouriteColor (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 () {
      return (новый UserConfig (this));
   }
}


1
Определенно, это улучшение. Строитель Блоха, как он реализован здесь, объединяет два конкретных класса: это строящийся класс и его строитель. Это плохой дизайн как таковой . Blind Builder, который вы описываете, нарушает эту связь, имея класс , который будет построен, определяет его конструкционную зависимость как абстракцию , которую другие классы могут реализовать в отсоединенном виде. Вы в значительной степени применили то, что является основным руководящим принципом объектно-ориентированного дизайна.
rucamzu

3
Вы должны где-нибудь написать об этом в блоге, если вы еще этого не сделали, хороший пример дизайна алгоритма! Я сейчас делюсь этим :-).
Мартейн Вербург

4
Спасибо за добрые слова. Теперь это первое сообщение в моем новом блоге: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind

Если и конструктор, и встроенные объекты реализуют Fieldable, шаблон начинает напоминать шаблон, который я назвал ReadableFoo / MutableFoo / ImmutableFoo, хотя вместо того, чтобы иметь способ сделать изменяемую вещь элементом «build» компоновщика, я вызовите его asImmutableи включите в ReadableFooинтерфейс [используя эту философию, вызов buildнеизменяемого объекта просто вернет ссылку на тот же объект].
суперкат

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

13

Я думаю, что вопрос здесь предполагает что-то с самого начала, не пытаясь доказать это, что модель строителя по своей сути хороша.

Я думаю, что модель строителя редко, если вообще, хорошая идея.


Назначение шаблона Builder

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

  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 И класс, который имеет слишком много зависимостей в своем конструкторе, явно недостаточно сплочен и должен быть преобразован в меньшие классы.
Basilevs

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