Dlaczego typ miałby być sprzężony z jego konstruktorem?

20

Niedawno moją odpowiedź w Code Review , która zaczęła się tak:

private Person(PersonBuilder builder) {

Zatrzymać. Czerwona flaga. Osoba budująca osobę zbuduje osobę; wie o Osobie. Klasa Person nie powinna wiedzieć nic o PersonBuilder - to tylko niezmienny typ. Utworzyłeś tutaj sprzęgło kołowe, w którym A zależy od B, który zależy od A.

Osoba powinna po prostu przyjąć swoje parametry; klient, który chce stworzyć Osobę bez budowania, powinien móc to zrobić.

Zostałem uderzony w głos głosowania i powiedziałem (cytując) czerwoną flagę, dlaczego? Implementacja ma ten sam kształt, co Joshua Bloch zademonstrował w swojej książce „Effective Java” (punkt # 2).

Wygląda więc na to, że jedynym słusznym sposobem implementacji wzorca konstruktora w Javie jest uczynienie konstruktora typem zagnieżdżonym (nie o to chodzi w tym pytaniu), a następnie uczynienie produktu (klasa budowanego obiektu ) weź zależność od konstruktora , na przykład:

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

Ta sama strona Wikipedii dla tego samego wzorca Konstruktora ma zupełnie inną (i znacznie bardziej elastyczną) implementację dla C #:

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

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Jak widać, produkt tutaj nie wie nic o Builderklasie, a wszystko, co go obchodzi, może zostać utworzony przez bezpośrednie wywołanie konstruktora, fabrykę abstrakcyjną ... lub konstruktora - o ile rozumiem, produkt o twórczym wzorze nigdy nie musi wiedzieć nic o tym, co go tworzy.

Dostałem kontrargument (który najwyraźniej jest wyraźnie broniony w książce Blocha), że wzorzec konstruktora mógłby zostać użyty do przerobienia typu, który miałby rozdęty konstruktor z dziesiątkami opcjonalnych argumentów. Zamiast więc trzymać się tego, co myślałem , że trochę przestudiowałem na tej stronie, i odkryłem, że jak podejrzewałem, ten argument nie dotyczy wody .

Więc o co chodzi? Po co wymyślać zbyt skomplikowane rozwiązania problemu, który w ogóle nie powinien istnieć? Jeśli na chwilę zdejmiemy Joshua Blocha z piedestału, czy możemy wymyślić jeden dobry, ważny powód, aby połączyć dwa konkretne typy i nazwać to najlepszą praktyką?

To wszystko cuchnie programowaniu kultu .

Mathieu Guindon
źródło
12
A to „pytanie” polega na byciu po prostu rantem…
Philip Kendall
2
@PhilipKendall może trochę. Ale naprawdę jestem zainteresowany zrozumieniem, dlaczego każda implementacja konstruktora Java ma tak ścisłe powiązanie.
Mathieu Guindon
19
@PhilipKendall Czuje do mnie ciekawość.
Maszt
8
@PhilipKendall To brzmi jak rant, tak, ale w jego rdzeniu jest ważne pytanie na temat. Może rant mógłby zostać zredagowany?
Andres F.
Nie jestem pod wrażeniem argumentu „zbyt wiele argumentów konstruktora”. Ale oba te przykłady są jeszcze mniej pod wrażeniem. Być może dlatego, że przykłady są zbyt proste, aby zademonstrować nietrywialne zachowanie, ale przykład Java brzmi jak zawiły płynny interfejs, a przykład w C # jest tylko okrągłym sposobem implementacji właściwości. Żaden przykład nie zawiera żadnej formy sprawdzania poprawności parametrów; konstruktor przynajmniej sprawdziłby rodzaj i liczbę podanych argumentów, co oznacza, że ​​konstruktor ze zbyt dużą liczbą argumentów wygrywa.
Robert Harvey

Odpowiedzi:

22

Nie zgadzam się z twoim twierdzeniem, że jest to czerwona flaga. Nie zgadzam się również z twoją reprezentacją kontrapunktu, że istnieje jeden właściwy sposób reprezentowania wzorca budowniczego.

Dla mnie jest jedno pytanie: Czy Konstruktor jest niezbędną częścią interfejsu API tego typu? Czy personBuilder jest niezbędną częścią interfejsu API osoby?

Jest całkowicie uzasadnione, że sprawdzona pod kątem ważności, niezmienna i / lub ściśle zamknięta klasa Person musi zostać utworzona za pośrednictwem dostarczonego przez nią Konstruktora (niezależnie od tego, czy ten Konstruktor jest zagnieżdżony czy sąsiadujący). W ten sposób Osoba może zachować wszystkie swoje pola prywatne lub pakiet-prywatne i końcowe, a także pozostawić klasę otwartą na modyfikację, jeśli jest w toku. (To może skończyć się zamknięte dla modyfikacji i otwarte na rozbudowę, ale to nie jest ważne teraz, i jest szczególnie kontrowersyjna dla niezmiennych obiektów danych).

Jeśli jest tak, że Osoba jest koniecznie tworzona za pomocą dostarczonego PersonBuilder jako część pojedynczego projektu interfejsu API na poziomie pakietu, wówczas okrągłe połączenie jest w porządku i nie jest tu wymagana czerwona flaga. W szczególności podałeś to jako niezaprzeczalny fakt, a nie opinię lub wybór projektu API, co mogło przyczynić się do złej odpowiedzi. Nie oddałbym ci głosu, ale nie obwiniam za to kogoś innego, a resztę dyskusji na temat „tego, co uzasadnia zdanie negatywne”, pozostawiam w Centrum pomocy Code Review i meta . Zdarzenia negatywne się zdarzają; to nie jest „policzek”.

Oczywiście, jeśli chcesz pozostawić wiele możliwości do zbudowania obiektu Person, wówczas PersonBuilder może stać się konsumentem lub narzędziemklasy Person, od której osoba nie powinna bezpośrednio zależeć. Ten wybór wydaje się bardziej elastyczny - kto nie chciałby więcej opcji tworzenia obiektów? - ale aby zachować gwarancje (takie jak niezmienność lub szeregowalność) nagle Osoba musi ujawnić inny rodzaj publicznej techniki tworzenia, na przykład bardzo długiego konstruktora . Jeśli Konstruktor ma reprezentować lub zamieniać konstruktor na wiele opcjonalnych pól lub konstruktor otwarty na modyfikacje, to długi konstruktor klasy może być szczegółem implementacji, który lepiej pozostawić ukryty. (Należy również pamiętać, że oficjalny i niezbędny Konstruktor nie wyklucza pisania własnego narzędzia budowlanego, tylko to, że narzędzie konstrukcyjne może korzystać z Konstruktora jako API, jak w moim pierwotnym pytaniu powyżej).


ps: Zauważam, że podana przez ciebie próbka recenzji kodu ma niezmienną Osobę, ale kontrprzykład z Wikipedii wymienia zmienny Samochód z pobieraczami i ustawiaczami. Łatwiej będzie zobaczyć pominięcie niezbędnych samochodów w samochodzie, jeśli zachowasz spójność języka i niezmienników.

Jeff Bowman wspiera Monikę
źródło
Myślę, że rozumiem twój punkt widzenia traktowania konstruktora jako szczegółów implementacyjnych. Po prostu nie wygląda na to, że Effective Java poprawnie przekazuje tę wiadomość (nie przeczytałem / nie przeczytałem tej książki), pozostawiając mnóstwo młodszych (?) Programistów Java, zakładając, że „tak to się robi”, niezależnie od zdrowego rozsądku . Zgadzam się, że przykłady z Wikipedii są złe do porównania .. ale hej, to Wikipedia .. i FWIW Tak naprawdę nie dbam o opinię; usunięta odpowiedź i tak ma pozytywny wynik netto (i tam jest moja szansa, aby w końcu zdobyć [odznakę: presja rówieśników]).
Mathieu Guindon
1
Nie mogę jednak odrzucić myśli, że typ ze zbyt wieloma argumentami konstruktora jest problemem projektowym (pachnie łamaniem SRP), że wzór konstruktora szybko wsuwa się pod dywan.
Mathieu Guindon
3
@ Mat'sMug Mój post powyżej przyjmuje, że klasa potrzebuje tylu argumentów konstruktora . Podobnie traktuję to jako zapach projektowy, jeśli mówimy o dowolnym elemencie logiki biznesowej, ale w przypadku obiektu danych nie jest kwestią typu, który ma dziesięć lub dwadzieścia bezpośrednich atrybutów. Na przykład nie narusza SRP dla obiektu, który reprezentuje pojedynczy mocno wpisany rekord w dzienniku .
Jeff Bowman wspiera Monikę
1
Słusznie. Nadal nie rozumiem String(StringBuilder)konstruktora, który wydaje się być zgodny z tą „zasadą”, w której typ jest zależny od swojego konstruktora. Byłem oszołomiony, gdy zobaczyłem przeciążenie tego konstruktora. To nie jest tak, że Stringma 42 parametry konstruktora ...
Mathieu Guindon
3
@Luaan Odd. Dosłownie nigdy nie widziałem, aby był używany i nie wiedziałem, że istnieje; toString()jest uniwersalny.
Chrylis
7

Rozumiem, jak denerwujące może być, gdy w debacie używane jest „odwołanie się do władzy”. Argumenty powinny opierać się na własnym IMO i chociaż nie jest błędem wskazywanie rad tak szanowanej osoby, to tak naprawdę nie można uznać za pełny argument sam w sobie, albowiem wiemy, że Słońce podróżuje po Ziemi, ponieważ Arystoteles tak powiedział .

Powiedziawszy to, myślę, że przesłanka twojego argumentu jest taka sama. Nigdy nie powinniśmy łączyć dwóch konkretnych rodzajów i nazywać to najlepszą praktyką, ponieważ ... Ktoś tak powiedział?

Aby argument, że sprzężenie jest problematyczne, był poprawny, muszą istnieć określone problemy, które stwarza. Jeśli takie podejście nie podlega tym problemom, nie można logicznie argumentować, że ta forma łączenia jest problematyczna. Więc jeśli chcesz tutaj powiedzieć, myślę, że musisz pokazać, w jaki sposób takie podejście podlega tym problemom i / lub w jaki sposób oddzielenie konstruktora poprawi projekt.

Od razu staram się zobaczyć, w jaki sposób połączone podejście spowoduje problemy. Klasy są całkowicie sprzężone, na pewno, ale konstruktor istnieje tylko w celu tworzenia instancji klasy. Innym sposobem spojrzenia na to byłoby rozszerzenie konstruktora.

JimmyJames
źródło
Więc ... wyższe sprzężenie, niższa spójność => szczęśliwe zakończenie? hmm ... Rozumiem, że konstruktor „rozszerza konstruktor”, ale przywołując inny przykład, nie widzę, co Stringdzieje się z przeciążeniem konstruktora przyjmującym StringBuilderparametr (chociaż można dyskutować, czy a StringBuilderjest konstruktorem , ale to inna historia).
Mathieu Guindon
4
@ Mat'sMug Aby być jasnym, tak naprawdę nie mam silnego przeczucia co do obu stron. Nie używam wzorca konstruktora, ponieważ tak naprawdę nie tworzę klas, które mają dużo danych wejściowych stanu. Generalnie rozłożyłbym taką klasę z powodów, o których wspominasz gdzie indziej. Mówię tylko, że jeśli potrafisz pokazać, w jaki sposób takie podejście prowadzi do problemu, nie będzie miało znaczenia, czy Bóg zejdzie i przekaże ten wzór budowniczego Mojżeszowi na kamiennej tabliczce. Wygrałeś'. Z drugiej strony, jeśli nie możesz ...
JimmyJames
Jednym z uzasadnionych zastosowań wzorca konstruktora, który mam we własnej bazie kodu, jest wyśmiewanie VBIDE API; w zasadzie podajesz treść ciągu modułu kodu, a konstruktor daje ci VBEpróbę, wraz z oknami, aktywnym okienkiem kodu, projektem i komponentem z modułem, który zawiera podany ciąg. Bez tego nie wiem, jak mógłbym napisać 80% testów w Rubberduck , przynajmniej bez dźgania mnie okiem za każdym razem, gdy na nie patrzę.
Mathieu Guindon
@ Mat'sMug Może to być naprawdę przydatne do rozbierania danych na niezmienne obiekty. Tego rodzaju przedmioty są zwykle torbami o właściwościach, tzn. Nie są tak naprawdę OO. Ta cała przestrzeń problemu to taka PITA, że wszystko, co pomaga zrobić, przyzwyczai się bez względu na to, czy przestrzegają dobrych praktyk, czy nie.
JimmyJames
4

Aby odpowiedzieć na pytanie, dlaczego typ byłby łączony z konstruktorem, konieczne jest zrozumienie, dlaczego ktokolwiek miałby używać konstruktora w pierwszej kolejności. W szczególności: Bloch zaleca używanie konstruktora, gdy masz dużą liczbę parametrów konstruktora. To nie jest jedyny powód, aby używać konstruktora, ale spekuluję, że jest to najczęściej. W skrócie, konstruktor zastępuje te parametry konstruktora i często konstruktor jest deklarowany w tej samej klasie, którą buduje. Zatem klasa zamykająca ma już wiedzę o konstruktorze i przekazanie jej jako argumentu konstruktora tego nie zmienia. Właśnie dlatego typ byłby sprzężony z jego konstruktorem.

Dlaczego posiadanie dużej liczby parametrów oznacza, że ​​powinieneś używać konstruktora? Cóż, posiadanie dużej liczby parametrów nie jest niczym niezwykłym w prawdziwym świecie, ani nie narusza zasady pojedynczej odpowiedzialności. Jest do bani, gdy masz dziesięć parametrów String i próbujesz zapamiętać, które z nich są, i czy jest to piąty czy szósty String, który powinien być zerowy. Konstruktor ułatwia zarządzanie parametrami, a także może robić inne fajne rzeczy, o których powinieneś przeczytać książkę, aby dowiedzieć się więcej.

Kiedy używasz konstruktora, który nie jest sprzężony z jego typem? Zasadniczo, gdy komponenty obiektu nie są natychmiast dostępne, a obiekty, które mają te elementy, nie muszą wiedzieć o typie, który próbujesz zbudować, lub dodajesz konstruktor dla klasy, nad którą nie masz kontroli .

Stary Gruby Ned
źródło
Dziwne, jedyne razy potrzebowałem konstruktora, gdy miałem typ, który nie ma ostatecznego kształtu, taki jak ten MockVbeBuilder , który buduje próbkę interfejsu API IDE; jeden test może wymagać tylko prostego modułu kodu, inny test może wymagać dwóch formularzy i modułu klasy - IMO , po to jest wzorzec konstruktora GoF. To powiedziawszy, mogę zacząć używać go do innej klasy z szalonym konstruktorem ... ale sprzężenie po prostu nie wydaje się właściwe.
Mathieu Guindon
1
@ Mat's Mug Przedstawiona przez ciebie klasa wydaje się bardziej reprezentatywna dla wzorca projektowego Builder (od Design Patterns Gamma, i in.), Niekoniecznie prosta klasa budowniczego. Obaj budują różne rzeczy, ale nie są tym samym. Ponadto nigdy nie potrzebujesz „konstruktora”, to tylko ułatwia inne rzeczy.
Old Fat Ned
1

produkt twórczego wzoru nigdy nie musi wiedzieć nic o tym, co go tworzy.

PersonKlasa nie wiem, co się jej tworzenia. Ma tylko konstruktor z parametrem, więc co? Nazwa typu parametru kończy się na ... Builder, który tak naprawdę niczego nie wymusza. W końcu to tylko nazwa.

Konstruktor jest uwielbionym obiektem konfiguracji 1 . A obiekt konfiguracyjny to tak naprawdę grupowanie właściwości, które do siebie należą. Jeśli należą do siebie tylko w celu przekazania konstruktorowi klasy, niech tak będzie! Zarówno origini destinationna StreetMapprzykład są typu Point. Czy możliwe byłoby przekazanie poszczególnych współrzędnych każdego punktu do mapy? Jasne, ale właściwości Pointnależą do siebie, a ponadto możesz mieć na nich wszelkiego rodzaju metody, które pomagają w ich budowie:

1 Różnica polega na tym, że konstruktor nie jest zwykłym obiektem danych, ale umożliwia te powiązane wywołania settera. Chodzi Buildergłównie o to, jak się budować.

Dodajmy teraz trochę wzorca poleceń do miksu, ponieważ jeśli przyjrzysz się uważnie, budowniczy jest tak naprawdę poleceniem:

  1. Zawiera on akcję: wywołanie metody innej klasy
  2. Zawiera niezbędne informacje do wykonania tej akcji: wartości parametrów metody

Specjalną częścią dla konstruktora jest to, że:

  1. Ta metoda, która ma zostać wywołana, jest tak naprawdę konstruktorem klasy. Lepiej nazwij to teraz wzorem twórczym .
  2. Wartością tego parametru jest sam konstruktor

To, co jest przekazywane Personkonstruktorowi, może je zbudować, ale niekoniecznie. Może to być zwykły stary obiekt konfiguracyjny lub konstruktor.

zero
źródło
0

Nie, są całkowicie w błędzie i masz absolutną rację. Ten model konstruktora jest po prostu głupi. To nie jest sprawa osoby, z której pochodzą argumenty konstruktora. Konstruktor to tylko wygoda dla użytkowników i nic więcej.

DeadMG
źródło
4
Dzięki ... Jestem pewien, że ta odpowiedź zebrałaby więcej głosów, gdyby rozszerzyła się nieco, wygląda na niedokończoną odpowiedź =)
Mathieu Guindon
Tak, dałbym +1, alternatywne podejście do konstruktora, które zapewnia te same zabezpieczenia i gwarancje bez połączenia
matowy freake
3
Aby zobaczyć kontrapunkt do tego, zobacz przyjętą odpowiedź , która wymienia przypadki, w których PersonBuilderfaktycznie jest istotną częścią PersonAPI. W obecnej formie ta odpowiedź nie uzasadnia tego przypadku.
Andres F.