Почему тип связан с его строителем?

20

Недавно я удалил свой ответ на Code Review , который начался так:

private Person(PersonBuilder builder) {

Стоп. Красный флаг. PersonBuilder будет строить Person; это знает о Человеке. Класс Person не должен ничего знать о PersonBuilder - это просто неизменный тип. Вы создали круговую связь здесь, где A зависит от B, который зависит от A.

Человек должен просто принять его параметры; клиент, который хочет создать Человека, не создавая его, должен иметь возможность сделать это.

Я получил удар вниз и сказал, что (цитируя) Красный флаг, почему? Реализация здесь имеет ту же форму, которую продемонстрировал Джошуа Блох в своей книге «Эффективная Java» (пункт № 2).

Итак, похоже, что Единственно верный способ реализации Pattern Builder в Java - это сделать построитель вложенным типом (хотя это и не тот вопрос, о котором идет речь), а затем создать продукт (класс создаваемого объекта). ) возьмите зависимость от строителя , вот так:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

Одна и та же страница Википедии для того же шаблона Builder имеет совершенно другую (и гораздо более гибкую) реализацию для C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Как вы можете видеть, продукт здесь ничего не знает о Builderклассе, и, несмотря на все заботы, он может быть создан прямым вызовом конструктора, абстрактной фабрикой ... или конструктором - насколько я понимаю, продукту творческого паттерна никогда не нужно ничего знать о том, что его создает.

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

Так в чем же дело? Зачем придумывать изощренные решения проблемы, которой вообще не должно быть? Если мы на минуту снимаем Джошуа Блоха с пьедестала, можем ли мы придумать одну вескую и уважительную причину для объединения двух конкретных типов и назвать это лучшей практикой?

Это все пахнет грузо-культовым программированием для меня.

Матье Гиндон
источник
12
И этот «вопрос» пахнет просто быть напыщенным ...
Филипп Кендалл
2
@PhilipKendall возможно немного. Но я искренне заинтересован в понимании того, почему каждая реализация Java-компоновщика имеет такую ​​тесную связь.
Матье Гиндон
19
@PhilipKendall Это пахнет любопытство ко мне.
Мачта
8
@PhilipKendall Это читается как напыщенная речь, да, но в его основе есть действительный вопрос по теме. Может быть, напыщенная речь могла быть отредактирована?
Андрес Ф.
Я не впечатлен аргументом «слишком много аргументов конструктора». Но я еще меньше впечатлен обоими этими примерами для строителей. Возможно, потому, что примеры слишком просты, чтобы продемонстрировать нетривиальное поведение, но пример Java читается как извилистый беглый интерфейс, а пример на C # - просто обходной способ реализации свойств. Ни один из примеров не делает никакой проверки параметров; конструктор, по крайней мере, проверяет тип и количество предоставленных аргументов, что означает, что выигрывает конструктор со слишком большим количеством аргументов.
Роберт Харви

Ответы:

22

Я не согласен с вашим утверждением, что это красный флаг. Я также не согласен с вашим представлением контрапункта, что существует один правильный способ представления паттерна Builder.

Для меня есть один вопрос: является ли Builder необходимой частью API типа? Здесь есть PersonBuilder необходимая часть API Person?

Вполне разумно, что проверяемый на достоверность, неизменный и / или тесно инкапсулированный класс Person обязательно будет создан с помощью предоставляемого им построителя (независимо от того, является ли этот построитель вложенным или смежным). При этом Person может сохранять все свои поля приватными или package-private и final, а также оставлять класс открытым для модификации, если он находится в стадии разработки. (Это может в конечном итоге закрыто для модификации и открыты для расширения, но это не важно сейчас, и особенно спорный для неизменяемых объектов данных.)

Если это тот случай, когда Person обязательно создается с помощью поставляемого PersonBuilder как часть единого проекта API уровня пакета, тогда циклическое связывание просто отлично, и красный флаг здесь не гарантируется. Примечательно, что вы назвали это неопровержимым фактом, а не мнением или выбором дизайна API, так что это могло привести к неправильному ответу. Я бы не проголосовал против вас, но я не виню кого-либо за это, и оставлю оставшуюся часть обсуждения «что оправдывает понижение» справочному центру Code Review и мета . Случаются отрицательные голоса; это не "пощечина".

Конечно, если вы хотите оставить много способов открыть объект Person, то PersonBuilder может стать потребителем или утилитой.класса Person, от которого Person не должен напрямую зависеть. Этот выбор кажется более гибким - кому бы не хотелось больше вариантов создания объектов? - но для того, чтобы придерживаться гарантий (таких как неизменяемость или сериализуемость), внезапно Человек должен представить другой тип техники публичного создания, такой как очень длинный конструктор , Если конструктор предназначен для представления или замены конструктора множеством необязательных полей или конструктора, открытого для модификации, тогда длинный конструктор класса может быть деталью реализации, которую лучше оставить скрытой. (Также имейте в виду, что официальный и необходимый Строитель не запрещает вам писать свою собственную строительную утилиту, просто ваша строительная утилита может использовать Строителя как API, как в моем первоначальном вопросе выше.)


PS: Я отмечаю, что в приведенном вами примере проверки кода есть неизменный Персона, но в контрпримере из Википедии перечислены изменяемые машины с геттерами и сеттерами. Может быть легче увидеть необходимые машины, опущенные в автомобиле, если вы сохраняете язык и инварианты согласованными между ними.

Джефф Боуман поддерживает Монику
источник
Я думаю, что вижу вашу точку зрения на конструктор как на деталь реализации. Это просто не похоже на то, что Effective Java должным образом передает это сообщение тогда (я не читал / не читал эту книгу), оставляя тонну младших (?) Разработчиков Java, предполагающих, что «это так», независимо от здравого смысла , Я согласен с тем, что примеры из Википедии плохи для сравнения ... но, эй, это Википедия ... и FWIW, на самом деле мне плевать на понижение; удаленный ответ в любом случае имеет положительный результат (и у меня есть шанс окончательно набрать [значок: давление со стороны сверстников]).
Матье Гиндон
1
Тем не менее, я не могу отбросить мысль о том, что тип с слишком большим количеством аргументов конструктора является проблемой проектирования (пахнет нарушением SRP), что шаблон компоновщика быстро засовывается под ковер.
Матье Гиндон
3
@ Mat'sMug Мой пост выше воспринимает как должное, что классу нужно столько аргументов конструктора . Я бы также отнесся к этому как к запаху дизайна, если бы речь шла о произвольном компоненте бизнес-логики, но для объекта данных не имеет значения, имеет ли тип десять или двадцать прямых атрибутов. Например , не является нарушением SRP для объекта, представляющего одну строго типизированную запись в журнале .
Джефф Боуман поддерживает Монику
1
Справедливо. Я до сих пор не понимаю String(StringBuilder)конструктор, который, похоже, подчиняется тому «принципу», когда для типа нормально иметь зависимость от своего конструктора. Я был ошеломлен, когда увидел перегрузку конструктора. Это не так, как Stringимеет 42 параметров конструктора ...
Матье Гиндон
3
@ Луаан Странно. Я буквально никогда не видел его использованным и не знал, что он существует; toString()универсален.
Хрилис - на забастовку -
7

Я понимаю, как это может раздражать, когда в дискуссии используется «обращение к власти». Аргументы должны быть основаны на их собственной ИМО, и хотя нет ничего неправильного в том, чтобы указывать на советы такого уважаемого человека, это не может считаться полным аргументом сам по себе. Мы знаем, что Солнце путешествует по земле, потому что Аристотель сказал так ,

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

Чтобы аргумент о том, что связывание проблематично, является действительным, должны быть определенные проблемы, которые он создает. Если этот подход не подпадает под эти проблемы, то вы не можете логически утверждать, что эта форма связи является проблематичной. Поэтому, если вы хотите высказать свою точку зрения, я думаю, вам нужно показать, как этот подход зависит от этих проблем и / или как разделение компоновщика улучшит дизайн.

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

JimmyJames
источник
Итак ... более высокая связь, более низкая сплоченность => счастливый конец? Хмм ... Я вижу пункт о строитель «простирающийся конструктор», но привести еще один пример, я не понимаю , что Stringделает с перегрузкой конструктор , принимающий StringBuilderпараметр (хотя это спорно ли StringBuilderэто строитель , но это другая история).
Матье Гиндон
4
@ Mat'sMug Чтобы было ясно, у меня нет сильного чувства об этом в любом случае. Я не склонен использовать шаблон построителя, потому что на самом деле я не создаю классы с большим количеством входных данных. Я бы вообще разложил такой класс по причинам, на которые вы ссылаетесь в другом месте. Все, что я говорю, это то, что если вы сможете показать, как этот подход приводит к проблеме, не будет иметь значения, если Бог сошёл и передал этот образец строителя Моисею на каменной табличке. Ты победил'. С другой стороны, если вы не можете ...
JimmyJames
Одно из законных применений шаблона компоновщика, которое у меня есть в моей собственной базе кода, - это насмешка над API VBIDE; в основном вы предоставляете строковое содержимое модуля кода, а сборщик предоставляет вам VBEмакет с окнами, активной панелью кода, проектом и компонентом с модулем, который содержит предоставленную вами строку. Без этого я не знаю, как я смогу написать 80% тестов в Rubberduck , по крайней мере, не ударяя себя в глаза каждый раз, когда я смотрю на них.
Матье Гиндон
@ Mat'sMug Это может быть очень полезно для разбивки данных на неизменяемые объекты. Эти виды объектов, как правило, являются мешками свойств, то есть не совсем ОО. Вся эта проблемная область - это такая PITA, что все, что помогает сделать, будет использовано, следуют ли они передовой практике или нет.
JimmyJames
4

Чтобы ответить, почему тип связан с компоновщиком, необходимо понять, почему кто-либо вообще использует компоновщик. В частности: Bloch рекомендует использовать конструктор, когда у вас есть большое количество параметров конструктора. Это не единственная причина для использования строителя, но я полагаю, что это наиболее распространенный вариант. Короче говоря, компоновщик является заменой этих параметров конструктора, и часто компоновщик объявляется в том же классе, что и компоновка. Таким образом, включающий класс уже знает о сборщике, и передача его в качестве аргумента конструктора не меняет этого. Вот почему тип будет связан с его строителем.

Почему наличие большого количества параметров означает, что вы должны использовать конструктор? Что ж, наличие большого количества параметров не является чем-то необычным в реальном мире и не нарушает принцип единой ответственности. Это также отстой, когда у вас есть десять параметров String и вы пытаетесь вспомнить, какие из них являются какими, и является ли это пятой или шестой строкой, которая должна быть нулевой. Строитель облегчает управление параметрами, а также может делать другие интересные вещи, о которых вы должны прочитать в книге, чтобы узнать о них.

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

Старый Толстый Нед
источник
Странно, единственный раз, когда мне нужен был конструктор, был когда у меня был тип, который не имеет определенной формы, как этот MockVbeBuilder , который создает макет API IDE; для одного теста может потребоваться простой модуль кода, для другого теста могут потребоваться две формы и модуль класса - IMO , для чего предназначен шаблон построителя GoF. Тем не менее, я мог бы начать использовать это для другого класса с сумасшедшим конструктором ... но связь просто не чувствует себя должным вообще.
Матье Гиндон
1
@ Mat's Mug Класс, который вы представили, кажется более представительным для шаблона проектирования Builder (из Design Patterns by Gamma, et al.), А не обязательно для простого класса Builder. Они оба строят вещи, но они не одно и то же. Кроме того, вам никогда не «нужен» строитель, он просто делает другие вещи проще.
Старый Толстый Нед
1

продукту творческого паттерна никогда не нужно ничего знать о том, что его создает.

PersonКласс не знает , что его создание. Он имеет только конструктор с параметром, и что? Имя типа параметра заканчивается на ... Builder, который на самом деле ничего не форсирует. В конце концов, это просто имя.

Строитель прославил 1 объект конфигурации. И объект конфигурации на самом деле просто группирует свойства, которые принадлежат друг другу. Если они принадлежат друг другу только с целью передачи конструктору класса, пусть будет так! Оба originи destinationиз StreetMapпримера имеют тип Point. Можно ли было передать отдельные координаты каждой точки на карту? Конечно, но свойства Pointпринадлежат друг другу, плюс вы можете иметь на них всевозможные методы, которые помогают в их построении:

1 Разница заключается в том, что компоновщик - это не просто объект простых данных, он допускает эти цепные вызовы сеттера. В Builderосновном это касается того, как строить себя.

Теперь давайте добавим немного комбинации команд в микс, потому что если вы посмотрите внимательно, то на самом деле строитель - это команда:

  1. Он инкапсулирует действие: вызывает метод другого класса
  2. Он содержит необходимую информацию для выполнения этого действия: значения параметров метода

Особая часть для строителя заключается в том, что:

  1. Этот вызываемый метод на самом деле является конструктором класса. Лучше называйте это творческим паттерном сейчас.
  2. Значением параметра является сам строитель

То, что передается Personконструктору, может его построить, но это не обязательно. Это может быть простой старый объект конфигурации или компоновщик.

значение NULL
источник
0

Нет, они совершенно не правы, а вы абсолютно правы. Эта модель строителя просто глупая. Это не дело Человека, откуда берутся аргументы конструктора. Конструктор - это просто удобство для пользователей и не более того.

DeadMG
источник
4
Спасибо ... Я уверен, что этот ответ собрал бы больше голосов, если бы он немного расширился, он выглядит как незаконченный ответ =)
Матье Гиндон
Да, я бы +1, альтернативный подход к сборщику, который обеспечивает те же гарантии и гарантии без сцепления
Мэтт Фрейк
3
Для контрапункта к этому, смотрите принятый ответ , который упоминает случаи, когда PersonBuilderфактически является существенной частью PersonAPI. В таком виде, этот ответ не доказывает свою правоту.
Андрес Ф.