Когда бы вы использовали шаблон Builder? [закрыто]

531

Каковы некоторые распространенные , реальные примеры использования шаблона Builder? Что он тебе покупает? Почему бы просто не использовать фабричный шаблон?

Чарльз Грэм
источник
В stackoverflow.com/questions/35238292/… упоминаются некоторые API, которые используют шаблон компоновщика
Alireza Fattahi
Ответы Аарона и Теты действительно информативны. Вот полная статья, связанная с этими ответами.
Diablo

Ответы:

262

Ключевое различие между сборщиком и фабрикой IMHO заключается в том, что сборщик полезен, когда вам нужно много чего сделать, чтобы построить объект. Например, представьте DOM. Вы должны создать множество узлов и атрибутов, чтобы получить конечный объект. Фабрика используется, когда фабрика может легко создать весь объект за один вызов метода.

Одним из примеров использования компоновщика является создание XML-документа. Я использовал эту модель при построении фрагментов HTML, например, у меня может быть Builder для построения таблицы определенного типа, и он может иметь следующие методы (параметры не показаны). :

BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

Затем этот конструктор выложит мне HTML. Это гораздо легче прочитать, чем пройти через большой процедурный метод.

Проверьте Шаблон Строителя в Википедии .

JoshBerke
источник
1021

Ниже приведены некоторые причины, приводящие доводы в пользу использования шаблона и примера кода в Java, но это реализация шаблона Builder, охватываемого бандой четырех в шаблонах проектирования. . Причины, по которым вы будете использовать его в Java, также применимы и к другим языкам программирования.

Как утверждает Джошуа Блох в Effective Java, 2nd Edition :

Шаблон конструктора является хорошим выбором при разработке классов, чьи конструкторы или статические фабрики будут иметь больше, чем несколько параметров.

В какой-то момент мы все столкнулись с классом со списком конструкторов, где каждое добавление добавляет новый параметр option:

Pizza(int size) { ... }        
Pizza(int size, boolean cheese) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }

Это называется паттерном телескопического конструктора. Проблема с этим шаблоном состоит в том, что, если конструкторы имеют длину 4 или 5 параметров, становится трудно запомнить требуемый порядок параметров, а также какой конкретный конструктор вам может потребоваться в данной ситуации.

Одной альтернативой шаблону телескопического конструктора является шаблон JavaBean, в котором вы вызываете конструктор с обязательными параметрами, а затем вызываете любые дополнительные сеттеры после:

Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

Проблема здесь заключается в том, что, поскольку объект создается за несколько вызовов, он может находиться в несогласованном состоянии в процессе его создания.Это также требует много дополнительных усилий для обеспечения безопасности резьбы.

Лучшая альтернатива - использовать шаблон Builder.

public class Pizza {
  private int size;
  private boolean cheese;
  private boolean pepperoni;
  private boolean bacon;

  public static class Builder {
    //required
    private final int size;

    //optional
    private boolean cheese = false;
    private boolean pepperoni = false;
    private boolean bacon = false;

    public Builder(int size) {
      this.size = size;
    }

    public Builder cheese(boolean value) {
      cheese = value;
      return this;
    }

    public Builder pepperoni(boolean value) {
      pepperoni = value;
      return this;
    }

    public Builder bacon(boolean value) {
      bacon = value;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }

  private Pizza(Builder builder) {
    size = builder.size;
    cheese = builder.cheese;
    pepperoni = builder.pepperoni;
    bacon = builder.bacon;
  }
}

Обратите внимание, что Pizza является неизменным и все значения параметров находятся в одном месте . Поскольку методы сеттера Builder возвращают объект Builder, они могут быть связаны .

Pizza pizza = new Pizza.Builder(12)
                       .cheese(true)
                       .pepperoni(true)
                       .bacon(true)
                       .build();

В результате получается код, который легко написать, а также легко прочитать и понять. В этом примере метод сборки может быть изменен для проверки параметров после их копирования из компоновщика в объект Pizza и создания исключения IllegalStateException, если указано недопустимое значение параметра. Этот шаблон является гибким, и в будущем к нему легко добавить больше параметров. Это действительно полезно, только если у вас будет более 4 или 5 параметров для конструктора. Тем не менее, это может быть стоит в первую очередь если вы подозреваете, что, возможно, добавите больше параметров в будущем.

Я много позаимствовал на эту тему из книги Джошуа Блоха « Эффективная Java», 2-е издание . Чтобы больше узнать об этом шаблоне и других эффективных методах Java, я настоятельно рекомендую его.

Аарон
источник
24
Отличается от оригинального конструктора GOF, верно? Потому что нет директора класса. Для меня это выглядит почти как другая модель, но я согласен, что это очень полезно.
Лино Роза
194
Для этого конкретного примера, не было бы лучше , чтобы удалить логические параметры и быть в состоянии сказатьnew Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg
46
Это больше похоже на свободный интерфейс, чем на шаблон строителя .
Роберт Харви
21
@Fabian Steeg, я думаю, что люди слишком остро реагируют на более приятные на вид булевы сеттеры, имейте в виду, что такого рода сеттеры не допускают изменений во время выполнения: Pizza.Builder(12).cheese().pepperoni().bacon().build();вам нужно будет перекомпилировать свой код или иметь ненужную логику, если вам нужны только пепперони пицц. По крайней мере, вы также должны предоставить параметризованные версии, такие как первоначально предложенный @Kamikaze Mercenary. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();, Опять же, мы никогда не тестируем юнит, не так ли?
egallardo
23
@JasonC Правильно, а какая польза от неизменной пиццы?
Maarten Bodewes
325

Рассмотрим ресторан. Создание «сегодняшней еды» - это фабричный шаблон, потому что вы говорите кухне «принесите мне сегодняшнюю еду», а кухня (фабрика) решает, какой объект генерировать, основываясь на скрытых критериях.

Строитель появится, если вы закажете заказную пиццу. В этом случае официант говорит шеф-повару (строителю): «Мне нужна пицца; добавь в нее сыр, лук и бекон!» Таким образом, конструктор предоставляет атрибуты, которые должен иметь сгенерированный объект, но скрывает, как их устанавливать.

Tetha
источник
Нитин продлил аналогию с кухней в другом ответе на этот вопрос .
Pops
19

Класс .NET StringBuilder является отличным примером шаблона компоновщика. В основном это используется для создания строки в серии шагов. Конечный результат, который вы получаете при выполнении ToString (), всегда является строкой, но создание этой строки зависит от того, какие функции в классе StringBuilder были использованы. Подводя итог, основная идея состоит в том, чтобы создавать сложные объекты и скрывать детали реализации того, как он создается.


источник
9
Я не думаю, что это модель строителя. StringBuilder - это просто еще одна реализация класса символьного массива (то есть строки), но он учитывает производительность и управление памятью, поскольку строки являются неизменяемыми.
Чарльз Грэм
23
Это абсолютно шаблон сборки, как и класс StringBuilder в Java. Обратите внимание, как метод append () обоих этих классов возвращает сам StringBuilder, так что можно выполнить цепочку b.append(...).append(...)перед окончательным вызовом toString(). Образец цитирования: infoq.com/articles/internal-dsls-java
pohl
3
@pohl Да, я не думаю, что это действительно шаблон построения, я бы сказал, что это больше, чем просто свободный интерфейс.
Дидье А.
«Обратите внимание, как метод append () обоих этих классов возвращает сам StringBuilder», это не шаблон Builder, а простой интерфейс. Просто часто Строитель ТАКЖЕ использует свободный интерфейс. У Строителя не должно быть свободного интерфейса.
bytedev
Но обратите внимание, что StringBuilder по своей природе не синхронизируется, в отличие от StringBuffer, который синхронизируется.
Алан Дип
11

Для многопоточной задачи нам нужно было создать сложный объект для каждого потока. Объект представляет обрабатываемые данные и может меняться в зависимости от ввода пользователя.

Можем ли мы использовать фабрику вместо этого? да

Почему не мы? Строитель имеет больше смысла, я думаю.

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

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

Кэмерон МакФарланд
источник
9

Вы используете его, когда у вас есть много вариантов для решения. Думайте о вещах как jmock:

m.expects(once())
    .method("testMethod")
    .with(eq(1), eq(2))
    .returns("someResponse");

Это кажется намного более естественным и ... возможно.

Есть также сборка XML, сборка строк и многое другое. Представь, если java.util.Mapбы поставил как строитель. Вы можете сделать что-то вроде этого:

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);
Dustin
источник
3
Я забыл прочитать карту «если», реализовал шаблон строителя и был удивлен, увидев там конструкцию .. :)
sethu
3
:) Прости за это. Во многих языках принято возвращать себя вместо пустоты. Вы можете сделать это в Java, но это не очень распространено.
Дастин
7
Пример карты - просто пример объединения методов.
nogridbag
@nogridbag Это было бы ближе к каскадированию методов. Хотя он использует цепочку таким образом, что имитирует каскадирование, он, очевидно, является цепочкой, но семантически он ведет себя как каскадирование.
Дидье А.
9

Проходя через фреймворк Microsoft MVC, я подумал о шаблоне компоновщика. Я наткнулся на шаблон в классе ControllerBuilder. Этот класс должен возвращать фабричный класс контроллера, который затем используется для создания конкретного контроллера.

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

@Tetha, может быть ресторан (Framework), управляемый итальянским парнем, который подает пиццу. Для приготовления пиццы итальянский парень (Object Builder) использует Owen (Factory) с основанием для пиццы (базовый класс).

Теперь индийский парень принимает ресторан у итальянского парня. Индийский ресторан (Framework) на серверах доши вместо пиццы. Для приготовления досы индийский парень (строитель объектов) использует сковороду (фабрика) с майдой (базовый класс)

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

class RestaurantObjectBuilder
{
   IFactory _factory = new DefaultFoodFactory();

   //This can be used when you want to plugin the 
   public void SetFoodFactory(IFactory customFactory)
   {
        _factory = customFactory;
   }

   public IFactory GetFoodFactory()
   {
      return _factory;
   }
}
Нитин
источник
7

Мне всегда не нравился шаблон Builder как нечто громоздкое, навязчивое и очень часто злоупотребляемое менее опытными программистами. Это шаблон, который имеет смысл только в том случае, если вам нужно собрать объект из некоторых данных, которые требуют шага после инициализации (т.е. после того, как все данные собраны - сделайте что-нибудь с этим). Вместо этого в 99% случаев строители просто используются для инициализации членов класса.

В таких случаях гораздо лучше просто объявить withXyz(...)установщики типов внутри класса и заставить их возвращать ссылку на себя.

Учти это:

public class Complex {

    private String first;
    private String second;
    private String third;

    public String getFirst(){
       return first; 
    }

    public void setFirst(String first){
       this.first=first; 
    }

    ... 

    public Complex withFirst(String first){
       this.first=first;
       return this; 
    }

    public Complex withSecond(String second){
       this.second=second;
       return this; 
    }

    public Complex withThird(String third){
       this.third=third;
       return this; 
    }

}


Complex complex = new Complex()
     .withFirst("first value")
     .withSecond("second value")
     .withThird("third value");

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

Павел Лечев
источник
Я просто решил, что хочу построить свой сложный XML-документ как JSON. Во-первых, как я узнаю, что класс Complex в первую очередь способен предоставлять XMLable-продукт и как я могу изменить его для создания JSONable-объекта? Быстрый ответ: я не могу, потому что мне нужно использовать строителей. И мы прошли полный круг ...
Дэвид Баркер
1
Всего бс, Builder предназначен для создания неизменных объектов и с возможностью изменять способ строительства в будущем, не затрагивая класс продукта
Микки Тин
6
хммм? Вы читали где-то в ответе мне, говоря, для чего предназначен Builder? Это альтернативная точка зрения на вопрос «Когда бы вы использовали шаблон Builder?», Который основан на опыте многочисленных злоупотреблений шаблоном, когда что-то намного более простое делает работу лучше. Все шаблоны полезны, если вы знаете, когда и как их использовать - в этом весь смысл документирования шаблонов! Когда шаблон чрезмерно используется или, что еще хуже, неправильно используется, он становится анти-шаблоном в контексте вашего кода. Увы ...
Павел Лечев
6

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

С другой стороны, для строителя вам просто нужно создать нового бетоностроителя для этого нового класса. Класс Director останется прежним, потому что он получает конструктор в конструкторе.

Также есть много ароматов застройщика. Kamikaze Mercenary`s дает еще один.

Лино Роза
источник
6
/// <summary>
/// Builder
/// </summary>
public interface IWebRequestBuilder
{
    IWebRequestBuilder BuildHost(string host);

    IWebRequestBuilder BuildPort(int port);

    IWebRequestBuilder BuildPath(string path);

    IWebRequestBuilder BuildQuery(string query);

    IWebRequestBuilder BuildScheme(string scheme);

    IWebRequestBuilder BuildTimeout(int timeout);

    WebRequest Build();
}

/// <summary>
/// ConcreteBuilder #1
/// </summary>
public class HttpWebRequestBuilder : IWebRequestBuilder
{
    private string _host;

    private string _path = string.Empty;

    private string _query = string.Empty;

    private string _scheme = "http";

    private int _port = 80;

    private int _timeout = -1;

    public IWebRequestBuilder BuildHost(string host)
    {
        _host = host;
        return this;
    }

    public IWebRequestBuilder BuildPort(int port)
    {
        _port = port;
        return this;
    }

    public IWebRequestBuilder BuildPath(string path)
    {
        _path = path;
        return this;
    }

    public IWebRequestBuilder BuildQuery(string query)
    {
        _query = query;
        return this;
    }

    public IWebRequestBuilder BuildScheme(string scheme)
    {
        _scheme = scheme;
        return this;
    }

    public IWebRequestBuilder BuildTimeout(int timeout)
    {
        _timeout = timeout;
        return this;
    }

    protected virtual void BeforeBuild(HttpWebRequest httpWebRequest) {
    }

    public WebRequest Build()
    {
        var uri = _scheme + "://" + _host + ":" + _port + "/" + _path + "?" + _query;

        var httpWebRequest = WebRequest.CreateHttp(uri);

        httpWebRequest.Timeout = _timeout;

        BeforeBuild(httpWebRequest);

        return httpWebRequest;
    }
}

/// <summary>
/// ConcreteBuilder #2
/// </summary>
public class ProxyHttpWebRequestBuilder : HttpWebRequestBuilder
{
    private string _proxy = null;

    public ProxyHttpWebRequestBuilder(string proxy)
    {
        _proxy = proxy;
    }

    protected override void BeforeBuild(HttpWebRequest httpWebRequest)
    {
        httpWebRequest.Proxy = new WebProxy(_proxy);
    }
}

/// <summary>
/// Director
/// </summary>
public class SearchRequest
{

    private IWebRequestBuilder _requestBuilder;

    public SearchRequest(IWebRequestBuilder requestBuilder)
    {
        _requestBuilder = requestBuilder;
    }

    public WebRequest Construct(string searchQuery)
    {
        return _requestBuilder
        .BuildHost("ajax.googleapis.com")
        .BuildPort(80)
        .BuildPath("ajax/services/search/web")
        .BuildQuery("v=1.0&q=" + HttpUtility.UrlEncode(searchQuery))
        .BuildScheme("http")
        .BuildTimeout(-1)
        .Build();
    }

    public string GetResults(string searchQuery) {
        var request = Construct(searchQuery);
        var resp = request.GetResponse();

        using (StreamReader stream = new StreamReader(resp.GetResponseStream()))
        {
            return stream.ReadToEnd();
        }
    }
}

class Program
{
    /// <summary>
    /// Inside both requests the same SearchRequest.Construct(string) method is used.
    /// But finally different HttpWebRequest objects are built.
    /// </summary>
    static void Main(string[] args)
    {
        var request1 = new SearchRequest(new HttpWebRequestBuilder());
        var results1 = request1.GetResults("IBM");
        Console.WriteLine(results1);

        var request2 = new SearchRequest(new ProxyHttpWebRequestBuilder("localhost:80"));
        var results2 = request2.GetResults("IBM");
        Console.WriteLine(results2);
    }
}
Раман Жилич
источник
1
Вы можете улучшить свои ответы двумя способами: 1) Сделать это SSCCE. 2) Объясните, как это отвечает на вопрос.
james.garriss
6

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

См. Builders в Groovy документации

Кен Нежный
источник
3

Я использовал builder в домашней библиотеке сообщений. Ядро библиотеки получало данные из сети, собирая их с помощью экземпляра Builder, а затем, как только Builder решил, что у него есть все необходимое для создания экземпляра Message, Builder.GetMessage () создавал экземпляр сообщения с использованием данных, собранных из провод.

wasker
источник
3

Посмотрите InnerBuilder, плагин IntelliJ IDEA, который добавляет действие «Строитель» в меню «Создать» (Alt + Insert), которое генерирует внутренний класс построителя, как описано в разделе «Эффективная Java».

https://github.com/analytically/innerbuilder

аналитически
источник
2

Когда я захотел использовать стандартный XMLGregorianCalendar для своего XML для сортировки объекта DateTime в Java, я услышал много комментариев о том, насколько тяжелым и громоздким было его использование. Я пытался контролировать поля XML в структурах xs: datetime для управления часовым поясом, миллисекундами и т. Д.

Поэтому я разработал утилиту для создания XMLGregorian календаря из GregorianCalendar или java.util.Date.

Из-за того, где я работаю, мне не разрешается делиться этим в сети без юридического, но вот пример того, как клиент использует это. Он абстрагирует детали и фильтрует некоторые реализации XMLGregorianCalendar, которые менее используются для xs: datetime.

XMLGregorianCalendarBuilder builder = XMLGregorianCalendarBuilder.newInstance(jdkDate);
XMLGregorianCalendar xmlCalendar = builder.excludeMillis().excludeOffset().build();

Конечно, этот шаблон является скорее фильтром, так как он устанавливает поля в xmlCalendar как неопределенные, поэтому они исключаются, он все равно «строит» его. Я легко добавил другие параметры в конструктор для создания структуры xs: date и xs: time, а также для управления смещениями часового пояса при необходимости.

Если вы когда-либо видели код, который создает и использует XMLGregorianCalendar, вы бы увидели, как это облегчает манипулирование.

Джон Браун
источник
0

Отличным примером из реальной жизни является использование при модульном тестировании ваших классов. Вы используете сборщики sut (System Under Test).

Пример:

Учебный класс:

public class CustomAuthenticationService
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationService(ICloudService cloudService, IDatabaseService databaseService)
    {
        _cloudService = cloudService;
        _databaseService = databaseService;
    }

    public bool IsAuthorized(User user)
    {            
        //Implementation Details
        return true;

}

Тестовое задание:

    [Test]
    public void Given_a_User_With_Permission_When_Verifying_If_Authorized_Then_Authorize_It_Returning_True()
    {
        CustomAuthenticationService sut = new CustomAuthenticationServiceBuilder();
        User userWithAuthorization = null;

        var result = sut.IsAuthorized(userWithAuthorization);

        Assert.That(result, Is.True);
    }

Сул Строитель:

public class CustomAuthenticationServiceBuilder
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationServiceBuilder()
    {
        _cloudService = new AwsService();
        _databaseService = new SqlServerService();
    }

    public CustomAuthenticationServiceBuilder WithAzureService(AzureService azureService)
    {
        _cloudService = azureService;

        return this;
    }

    public CustomAuthenticationServiceBuilder WithOracleService(OracleService oracleService)
    {
        _databaseService = oracleService;

        return this;
    }

    public CustomAuthenticationService Build()
    {
        return new CustomAuthenticationService(_cloudService, _databaseService);
    }

    public static implicit operator CustomAuthenticationService (CustomAuthenticationServiceBuilder builder)
    {
        return builder.Build();
    }
}
Рафаэль Мицели
источник
Зачем вам нужен строитель в этом случае, а не просто добавлять сеттеры в ваш CustomAuthenticationServiceкласс?
Лаур Иван
Это хороший вопрос @LaurIvan! Возможно, мой пример был немного плохим, но представьте, что вы не можете изменить класс CustomAuthenticationService, сборщик был бы привлекательным способом улучшить чтение ваших модульных тестов. И я думаю, что создание геттеров и сеттеров будут выставлять ваши поля, которые будут использоваться только для ваших тестов. Если вы хотите больше узнать о Sut Builder, вы можете прочитать о Test Data Builder , который почти такой же, но для suts.
Рафаэль Мицели