Коли ви користуєтеся шаблоном Builder? [зачинено]


530

Назвіть декілька загальних , реальних прикладів використання моделі Builder? Що це купує у вас? Чому б просто не використовувати заводський візерунок?


Stackoverflow.com/questions/35238292 / ... згадав деякі API , що використання будівельник шаблон
Реза Фаттаха

Відповіді Аарона та Тетхи справді інформативні. Ось повна стаття, пов’язана з цими відповідями.
Діабло

Відповіді:


262

Ключова відмінність будівельника від фабричного ІМХО полягає в тому, що будівельник корисний, коли потрібно зробити багато речей для створення об'єкта. Наприклад, уявіть собі DOM. Вам потрібно створити безліч вузлів і атрибутів, щоб отримати ваш кінцевий об'єкт. Фабрика використовується, коли фабрика може легко створити весь об'єкт в межах одного виклику методу.

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

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

Потім цей конструктор виплюнув би мені HTML. Це читати набагато простіше, ніж ходити через великий процедурний метод.

Ознайомтеся з шаблоном Builder у Вікіпедії .


1020

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

Як заявляє Джошуа Блох в Ефективній Java, 2-е видання :

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

Ми всі в якийсь момент стикалися з класом зі списком конструкторів, де кожне додавання додає новий параметр параметра:

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


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

Розгляньте ресторан. Створення «сьогоднішньої страви» - це заводський зразок, оскільки ви кажете на кухні «дістаньте мені сьогоднішню страву», а кухня (фабрика) вирішує, який об’єкт створити, грунтуючись на прихованих критеріях.

Будівельник з'являється, якщо ви замовляєте власну піцу. У цьому випадку офіціант каже шеф-кухарю (будівельнику) "мені потрібна піца; додайте в неї сир, цибулю та бекон!" Таким чином, будівельник розкриває атрибути, які повинен мати створений об'єкт, але приховує, як їх встановити.


Нітін поширив кухонну аналогію в іншій відповіді на це питання .
Попс

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, це лише вільний інтерфейс. Просто, що часто Будівельник ТАКОЖ використовує вільний інтерфейс. Builder не повинен мати вільний інтерфейс.
bytedev

Але зауважте, що StringBuilder від природи не синхронізований, на відміну від StringBuffer, який синхронізований.
Алан Глибокий

11

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

Чи могли б ми замість цього використати фабрику? Так

Чому ми не стали? Думаю, Builder має більше сенсу.

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

Будівельники будують один і той же тип об’єктів знову і знову, але конструкція динамічна, тому її можна змінювати під час виконання.


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);

3
Я забув прочитати "якщо" карта реалізувала модель для будівельників і здивувалась, щоб побачити конструкцію там. :)
sethu

3
:) Вибач за те. У багатьох мовах прийнято повертати себе замість нікчемного. Ви можете це робити в java, але це не дуже часто.
Дастін

7
Приклад карти - це просто приклад ланцюгового методу.
нігеріг

@nogridbag Це насправді було б ближче до методу каскадування. Хоча це використовує ланцюжок таким чином, що імітує каскад, тому воно, очевидно, є ланцюговим, але семантично воно поводиться як каскадне.
Дідьє А.

9

Під час перегляду фреймворку Microsoft MVC я подумав про модель розробника. Я натрапив на схему в класі ControllerBuilder. Цей клас повинен повернути заводський клас контролера, який потім використовується для побудови конкретного контролера.

Перевага, яку я бачу при використанні шаблону для побудови, полягає в тому, що ви можете створити власну фабрику і підключити її до рамки.

@Tetha, там може бути ресторан (Framework), яким керує італійський хлопець, який обслуговує піцу. Для того, щоб приготувати піцу італійський хлопець (Object Builder) використовує Оуена (Фабрика) з основою для піци (базовий клас).

Зараз індійський хлопець переймає ресторан від італійського хлопця. Індійський ресторан (Framework) серверів dosa замість піци. Для підготовки доси індійський хлопець (будівельник об'єктів) використовує Сковороду (Фабрика) з Майдою (базовий клас)

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

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. По-перше, як я знаю, що клас "Комплекс" здатний в першу чергу доставити XMLable продукт і як би я змінив його для створення об'єкта JSONable? Швидка відповідь: я не можу, тому що мені потрібно використовувати будівельники. І ми приходимо повним колом ...
Девід Баркер

1
total bs, Builder призначений для створення непорушних об'єктів і з можливістю змінити спосіб будівництва в майбутньому, не торкаючись класу продуктів
Міккі Тін

6
хммм? Ви читали десь у відповіді, в якій говорили, для чого призначений Builder? Це альтернативна точка зору до питання у верхній частині "Коли ви використовуєте шаблон для будівельників?", Яке базується на досвіді незліченного зловживання шаблоном, де щось набагато простіше робить роботу краще. Усі шаблони корисні, якщо ви знаєте, коли і як ними користуватися - ось у чому вся суть документального оформлення шаблонів! Якщо шаблоном надмірно використовувати або ще гірше - неправомірно використовувати, він стає антидіаграмою в контексті вашого коду. На жаль ...
Павло Лечев

6

Ще одна перевага будівельника полягає в тому, що якщо у вас є Завод, у вас ще є якась муфта, оскільки для роботи Фабрики він повинен знати всі об'єкти, які він може створити . Якщо ви додасте інший об’єкт, який можна було б створити, вам доведеться змінити заводський клас, щоб включити його. Це трапляється і на абстрактній фабриці.

З будівельника, з іншого боку, вам просто потрібно створити нового бетонного будівельника для цього нового класу. Клас режисера залишиться тим самим, оскільки він отримує будівельника в конструкторі.

Також є багато ароматів будівельника. 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

Спираючись на попередні відповіді (призначені каламбури), відмінним реальним прикладом є побудований Groovy для підтримки Builders.

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


3

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



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 structure, а також для маніпулювання зрушеннями часового поясу при необхідності.

Якщо ви коли-небудь бачили код, який створює та використовує 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);
    }

sut Builder:

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 , який майже те саме, але для сутів.
Рафаель Міцелі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.