Kiedy użyjesz Wzorca konstruktora? [Zamknięte]


530

Jakie są wspólne , rzeczywiste przykłady świata z użyciem wzoru Builder? Co ci to kupuje? Dlaczego nie użyć tylko wzoru fabrycznego?


Stackoverflow.com/questions/35238292/... wymienić kilka API że stosowanie budowniczy
Alireza Fattahi

Odpowiedzi Aarona i Tethy są bardzo pouczające. Oto pełny artykuł związany z tymi odpowiedziami.
Diablo

Odpowiedzi:


262

Kluczowa różnica między konstruktorem a fabrycznym IMHO polega na tym, że konstruktor jest przydatny, gdy trzeba zrobić wiele rzeczy, aby zbudować obiekt. Na przykład wyobraź sobie DOM. Musisz utworzyć wiele węzłów i atrybutów, aby uzyskać swój ostateczny obiekt. Fabryka jest używana, gdy fabryka może łatwo utworzyć cały obiekt w ramach jednego wywołania metody.

Jednym z przykładów użycia konstruktora jest budowanie dokumentu XML. Użyłem tego modelu podczas budowania fragmentów HTML, na przykład mógłbym mieć Konstruktora do budowania określonego typu tabeli i może mieć następujące metody (parametry nie są pokazane) :

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

Ten konstruktor wyplułby dla mnie HTML. Jest to o wiele łatwiejsze do odczytania niż przechodzenie przez dużą metodę proceduralną.

Sprawdź Wzór konstruktora na Wikipedii .


1020

Poniżej znajduje się kilka powodów przemawiających za użyciem wzorca i przykładowego kodu w Javie, ale jest to implementacja Wzorca Konstruktora objętego Gangiem Czterech we Wzorcach Projektu . Powody, dla których chcesz go używać w Javie, dotyczą również innych języków programowania.

Jak stwierdza Joshua Bloch w Effective Java, wydanie drugie :

Wzorzec konstruktora jest dobrym wyborem przy projektowaniu klas, których konstruktory lub fabryki statyczne miałyby więcej niż garść parametrów.

Wszyscy kiedyś spotkaliśmy klasę z listą konstruktorów, w której każde dodanie dodaje nowy parametr opcji:

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

Nazywa się to Wzorem Konstruktora Teleskopowego. Problem z tym wzorcem polega na tym, że gdy konstruktory mają długość 4 lub 5 parametrów, trudno jest zapamiętać wymaganą kolejność parametrów, a także konkretny konstruktor, którego możesz chcieć w danej sytuacji.

Jedną z alternatyw dla Telescoping Constructor Pattern jest wzorzec JavaBean, w którym wywołujesz konstruktor z obowiązkowymi parametrami, a następnie wywołujesz dowolne ustawiacze po:

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

Problem polega na tym, że ponieważ obiekt jest tworzony przez kilka wywołań, może być w niespójnym stanie w trakcie jego budowy. Wymaga to również dodatkowego wysiłku, aby zapewnić bezpieczeństwo nici.

Lepszą alternatywą jest użycie Wzorca konstruktora.

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

Pamiętaj, że Pizza jest niezmienna, a wszystkie wartości parametrów znajdują się w jednym miejscu . Ponieważ metody ustawiające Buildera zwracają obiekt Builder, można je połączyć w łańcuch .

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

Powoduje to, że kod jest łatwy do napisania, bardzo łatwy do odczytania i zrozumienia. W tym przykładzie metodę kompilacji można zmodyfikować w celu sprawdzenia parametrów po skopiowaniu ich z konstruktora do obiektu Pizza i wygenerowania wyjątku IllegalStateException, jeśli podano niepoprawną wartość parametru. Ten wzór jest elastyczny i łatwo będzie dodać do niego więcej parametrów w przyszłości. Jest to naprawdę przydatne tylko wtedy, gdy masz więcej niż 4 lub 5 parametrów dla konstruktora. To powiedziawszy, może być opłacalne, jeśli podejrzewasz, że możesz dodać więcej parametrów w przyszłości.

Pożyczyłem dużo na ten temat z książki Effective Java, 2. wydanie Joshua Blocha. Aby dowiedzieć się więcej o tym wzorcu i innych skutecznych praktykach Java , bardzo go polecam.


24
Różni się od oryginalnego konstruktora GOF, prawda? Ponieważ nie ma klasy reżyserskiej. Wydaje mi się, że to kolejny wzór, ale zgadzam się, że jest bardzo przydatny.
Lino Rosa

194
W tym konkretnym przykładzie nie byłoby lepiej usunąć parametry boolowskie i móc powiedziećnew Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg

46
To bardziej przypomina płynny interfejs niż wzorzec konstruktora .
Robert Harvey

21
@Fabian Steeg, myślę, że ludzie przesadnie reagują na ładniej ustawiające elementy logiczne, pamiętaj, że takie zestawy nie zezwalają na zmiany w środowisku wykonawczym: Pizza.Builder(12).cheese().pepperoni().bacon().build();musisz ponownie skompilować kod lub mieć niepotrzebną logikę, jeśli potrzebujesz tylko trochę pepperoni pizze. Przynajmniej powinieneś dostarczyć sparametryzowane wersje, takie jak początkowo sugerowany @Kamikaze Mercenary. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();. Z drugiej strony, nigdy nie przeprowadzamy testów jednostkowych, prawda?
egallardo

23
@JasonC Zgadza się, a jaki użytek stanowi niezmienna pizza?
Maarten Bodewes,

325

Rozważ restaurację. Stworzenie „dzisiejszego posiłku” jest wzorcem fabrycznym, ponieważ mówisz kuchni „przynieś mi dzisiejszy posiłek”, a kuchnia (fabryka) decyduje o tym, który obiekt wygenerować, na podstawie ukrytych kryteriów.

Kreator pojawi się, jeśli zamówisz niestandardową pizzę. W tym przypadku kelner mówi szefowi kuchni (budowniczy) „Potrzebuję pizzy; dodaj do niej ser, cebulę i bekon!” W ten sposób konstruktor ujawnia atrybuty, które powinien mieć wygenerowany obiekt, ale ukrywa, jak je ustawić.


Nitin rozszerzył analogię kuchni w innej odpowiedzi na to pytanie .
Wyskakuje

19

Klasa .NET StringBuilder jest doskonałym przykładem wzorca budowniczego. Najczęściej jest używany do tworzenia ciągu znaków w szeregu kroków. Ostateczny wynik uzyskiwania przy wykonywaniu ToString () jest zawsze ciągiem, ale utworzenie tego ciągu zależy od funkcji użytych w klasie StringBuilder. Podsumowując, podstawową ideą jest budowanie złożonych obiektów i ukrywanie szczegółów implementacyjnych dotyczących tego, jak jest budowany.


9
Nie sądzę, że to wzorzec konstruktora. StringBuilder to kolejna implementacja klasy tablicy znaków (tj. String), ale uwzględnia zarządzanie wydajnością i pamięcią, ponieważ ciągi są niezmienne.
Charles Graham,

23
Jest to absolutnie wzorzec konstruktora, podobnie jak klasa StringBuilder w Javie. Zwróć uwagę, w jaki sposób metoda append () obu tych klas zwraca samą StringBuilder, aby można było połączyć łańcuch b.append(...).append(...)przed ostatecznym wywołaniem toString(). Cytowanie: infoq.com/articles/internal-dsls-java
pohl

3
@pohl Ya Nie sądzę, że to naprawdę wzorzec konstruktora, powiedziałbym, że to bardziej płynny interfejs.
Didier A.,

„Zauważ, że metoda append () obu tych klas zwraca sam StringBuilder”, to nie jest wzorzec Buildera, tylko płynny interfejs. Po prostu często Konstruktor użyje również płynnego interfejsu. Konstruktor nie musi mieć płynnego interfejsu.
bytedev

Należy jednak pamiętać, że StringBuilder z natury nie jest synchronizowany, w przeciwieństwie do StringBuffer, który jest synchronizowany.
Alan Deep

11

W przypadku problemu wielowątkowego potrzebowaliśmy zbudować złożony obiekt dla każdego wątku. Obiekt reprezentował przetwarzane dane i może się zmieniać w zależności od danych wejściowych użytkownika.

Czy zamiast tego moglibyśmy skorzystać z fabryki? tak

Dlaczego nie Budowniczy ma chyba więcej sensu.

Fabryki są używane do tworzenia różnych typów obiektów, które są tego samego typu podstawowego (implementują ten sam interfejs lub klasę podstawową).

Konstruktorzy ciągle budują ten sam typ obiektu, ale konstrukcja jest dynamiczna, więc można ją zmieniać w czasie wykonywania.


9

Używasz go, gdy masz wiele opcji do czynienia. Pomyśl o takich rzeczach jak jmock:

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

Wydaje się bardziej naturalny i ... jest możliwy.

Istnieje również budowanie XML, budowanie ciągów i wiele innych rzeczy. Wyobraź sobie, java.util.Mapże umieścił jako budowniczy. Możesz zrobić takie rzeczy:

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);

3
Zapomniałem przeczytać mapę „if” zaimplementowaną wzorzec konstruktora i byłem zaskoczony, widząc tam konstrukcję .. :)
sethu

3
:) Przepraszam za to. W wielu językach powszechne jest zwracanie siebie zamiast pustki. Możesz to zrobić w Javie, ale nie jest to zbyt powszechne.
Dustin

7
Przykład mapy jest po prostu przykładem łączenia metod.
nogridbag

@nogridbag W rzeczywistości byłoby to bliżej kaskadowania metod. Chociaż wykorzystuje łańcuchy w sposób symulujący kaskadowanie, więc oczywiście jest to łańcuchowanie, ale semantycznie zachowuje się jak kaskadowanie.
Didier A.,

9

Przeglądając środowisko Microsoft MVC, zastanowiłem się nad wzorcem konstruktora. Natknąłem się na wzór w klasie ControllerBuilder. Ta klasa ma zwrócić klasę fabryczną kontrolera, która jest następnie używana do budowy konkretnego kontrolera.

Zaletą, jaką widzę przy użyciu wzorca budowniczego, jest to, że możesz stworzyć własną fabrykę i podłączyć ją do frameworka.

@Tetha, może istnieć restauracja (Framework) prowadzona przez Włocha, która serwuje Pizza. W celu przygotowania pizzy Włoch (Object Builder) używa Owena (Factory) z podstawą do pizzy (klasa podstawowa).

Teraz Indianin przejmuje restaurację od Włocha. Serwery indyjskiej restauracji (Framework) dosa zamiast pizzy. Aby przygotować dosa Indianina (budowniczego obiektów) używa Patelni (fabryka) z Maidą (klasa podstawowa)

Jeśli spojrzysz na scenariusz, jedzenie jest inne, sposób przygotowania jedzenia jest inny, ale w tej samej restauracji (w tych samych ramach). Restauracja powinna być zbudowana w taki sposób, aby obsługiwała kuchnię chińską, meksykańską lub dowolną. Konstruktor obiektów wewnątrz frameworka ułatwia podłączanie żądanej kuchni. na przykład

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

Zawsze nie lubiłem wzorca Buildera jako czegoś nieporęcznego, natrętnego i bardzo często wykorzystywanego przez mniej doświadczonych programistów. Jest to wzorzec, który ma sens tylko wtedy, gdy trzeba złożyć obiekt z niektórych danych, co wymaga kroku po inicjalizacji (tj. Po zebraniu wszystkich danych - zrób coś z tym). Zamiast tego w 99% przypadków konstruktory są po prostu używane do inicjowania członków klasy.

W takich przypadkach zdecydowanie lepiej jest po prostu zadeklarować withXyz(...)ustawiające typy wewnątrz klasy i sprawić, aby zwróciły odwołanie do siebie.

Rozważ to:

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

Teraz mamy schludną pojedynczą klasę, która zarządza własną inicjalizacją i wykonuje prawie taką samą pracę jak konstruktor, z wyjątkiem tego, że jest znacznie bardziej elegancka.


Właśnie zdecydowałem, że chcę zbudować swój złożony dokument XML jako JSON. Po pierwsze, skąd mam wiedzieć, że klasa „Complex” jest w stanie przede wszystkim dostarczyć produkt XMLable i jak mam go zmienić, aby utworzyć obiekt JSONable? Szybka odpowiedź: nie mogę, bo muszę używać konstruktorów. I zataczamy pełne koło ...
David Barker

1
total bs, Builder jest zaprojektowany do budowania niezmiennych obiektów i ze zdolnością zmienia sposób budowania w przyszłości bez dotykania klasy produktu
Mickey Tin

6
hmmm? Czy czytałeś gdzieś w odpowiedzi, mówiąc, do czego przeznaczony jest Builder? Jest to alternatywny punkt widzenia na pytanie u góry „Kiedy użyjesz Wzorca konstruktora?”, Który opiera się na doświadczeniu niezliczonego nadużywania wzorca, w którym coś znacznie prostszego robi lepiej. Wszystkie wzory są przydatne, jeśli wiesz, kiedy i jak ich używać - na tym właśnie polega przede wszystkim dokumentowanie wzorów! Kiedy wzorzec jest nadużywany lub gorzej - niewłaściwie używany - staje się anty-wzorcem w kontekście twojego kodu. Niestety ...
Pavel Lechev

6

Kolejną zaletą konstruktora jest to, że jeśli masz Fabrykę, w twoim kodzie nadal jest trochę sprzężenia, ponieważ aby Fabryka działała, musi znać wszystkie obiekty, które może stworzyć . Jeśli dodasz inny obiekt, który mógłby zostać utworzony, będziesz musiał zmodyfikować klasę fabryczną, aby go uwzględnić. Dzieje się tak również w fabryce abstrakcyjnej.

Z drugiej strony za pomocą konstruktora musisz po prostu stworzyć nowego konstruktora betonu dla tej nowej klasy. Klasa dyrektora pozostanie taka sama, ponieważ otrzymuje konstruktor w konstruktorze.

Ponadto istnieje wiele smaków konstruktora. Kamikaze Mercenary`s daje kolejny.


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
Możesz poprawić swoje odpowiedzi na dwa sposoby: 1) Zrób to SSCCE. 2) Wyjaśnij, jak to odpowiada na pytanie.
james.garriss


3

Użyłem konstruktora w domowej bibliotece wiadomości. Rdzeń biblioteki odbierał dane z drutu, zbierając je za pomocą instancji Builder, a następnie, gdy Builder zdecydował, że ma wszystko, czego potrzeba do utworzenia instancji Message, Builder.GetMessage () konstruował instancję komunikatu na podstawie danych zebranych z drut.



2

Kiedy chciałem użyć standardowego XMLGregorianCalendar dla mojego XML, aby sprzeciwić się zestawieniu DateTime w Javie, usłyszałem wiele komentarzy na temat jego ciężaru i uciążliwości. Próbowałem kontrolować pola XML w strukturach xs: datetime, aby zarządzać strefą czasową, milisekundami itp.

Zaprojektowałem więc narzędzie do budowy kalendarza XMLGregorian z GregorianCalendar lub java.util.Date.

Ze względu na to, gdzie pracuję, nie mogę udostępniać go online bez legalnego, ale oto przykład, w jaki sposób klient go używa. Wyodrębnia szczegóły i filtruje niektóre implementacje XMLGregorianCalendar, które są rzadziej używane dla xs: datetime.

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

Przyznaję, że ten wzorzec jest raczej filtrem, ponieważ ustawia pola w xmlCalendar jako niezdefiniowane, więc są one wykluczane, nadal go „buduje”. Z łatwością dodałem inne opcje do konstruktora, aby utworzyć strukturę xs: date i xs: time, a także w razie potrzeby manipulować przesunięciami strefy czasowej.

Jeśli kiedykolwiek widziałeś kod, który tworzy i używa XMLGregorianCalendar, zobaczysz, jak to znacznie ułatwiło manipulowanie.


0

Doskonałym przykładem w świecie rzeczywistym jest testowanie lekcji podczas zajęć. Używasz konstruktorów sut (System Under Test).

Przykład:

Klasa:

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:

    [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();
    }
}

Dlaczego miałbyś potrzebować konstruktora w tym przypadku zamiast dodawania seterów do swojej CustomAuthenticationServiceklasy?
Laur Ivan

To dobre pytanie @LaurIvan! Może mój przykład był trochę kiepski, ale wyobraź sobie, że nie możesz zmienić klasy CustomAuthenticationService, konstruktor byłby atrakcyjnym sposobem na poprawienie odczytu testów jednostkowych. I myślę, że tworzenie programów pobierających i ustawiających odsłoni twoje pola, które zostaną użyte tylko do twoich testów. Jeśli chcesz dowiedzieć się więcej o narzędziu Sut Builder, przeczytaj o narzędziu Test Data Builder , które jest prawie takie samo, ale niestety.
Rafael Miceli
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.