Wdrażając Wzorzec Konstruktora, często mylę się, kiedy pozwolić, aby budowanie się nie powiodło, a nawet co kilka dni potrafię zająć różne stanowisko w tej sprawie.
Najpierw jakieś wyjaśnienie:
- Z wczesnym niepowodzeniem mam na myśli, że budowanie obiektu powinno zakończyć się niepowodzeniem, gdy tylko zostanie przekazany nieprawidłowy parametr. Więc wewnątrz
SomeObjectBuilder
. - Z porażką późną mam na myśli to, że budowanie obiektu może zakończyć się niepowodzeniem tylko w przypadku
build()
wywołania, które domyślnie wywołuje konstruktora obiektu do zbudowania.
Następnie kilka argumentów:
- Za opóźnieniem: klasa konstruktora nie powinna być niczym więcej niż klasą, która po prostu przechowuje wartości. Co więcej, prowadzi to do mniejszego duplikowania kodu.
- Na korzyść wczesnego niepowodzenia: Ogólne podejście w programowaniu oprogramowania polega na tym, że chcesz wykrywać problemy tak wcześnie, jak to możliwe, a zatem najbardziej logicznym miejscem do sprawdzenia byłoby w klasie konstruktora „konstruktor”, „ustawiacze” i ostatecznie w metodzie kompilacji.
Jaki jest w tym ogólny konsensus?
java
design-patterns
skiwi
źródło
źródło
null
obiekt, gdy wystąpi problembuild()
.Odpowiedzi:
Spójrzmy na opcje, w których możemy umieścić kod weryfikacyjny:
build()
metody.build()
metodą podczas tworzenia encji.Opcja 1 pozwala nam wcześniej wykryć problemy, ale mogą wystąpić skomplikowane przypadki, w których możemy zweryfikować dane wejściowe tylko w pełnym kontekście, wykonując co najmniej część sprawdzania poprawności
build()
metodą. Zatem wybranie opcji 1 doprowadzi do niespójnego kodu z częścią sprawdzania poprawności w jednym miejscu, a drugą częścią w innym miejscu.Opcja 2 nie jest znacznie gorsza niż opcja 1, ponieważ zwykle setery w programie budującym są wywoływane tuż przed
build()
, szczególnie w płynnych interfejsach. Dlatego w większości przypadków nadal można wykryć problem wystarczająco wcześnie. Jeśli jednak konstruktor nie jest jedynym sposobem na utworzenie obiektu, doprowadzi to do duplikacji kodu sprawdzania poprawności, ponieważ będziesz musiał mieć go wszędzie tam, gdzie tworzysz obiekt. Najbardziej logicznym rozwiązaniem w tym przypadku będzie umieszczenie walidacji tak blisko stworzonego obiektu, jak to możliwe, czyli wewnątrz niego. I to jest opcja 3 .Z punktu widzenia SOLID, sprawdzanie poprawności w kreatorze również narusza SRP: klasa konstruktora ma już obowiązek agregowania danych w celu skonstruowania obiektu. Sprawdzanie poprawności polega na ustanawianiu umów dotyczących własnego stanu wewnętrznego, nową odpowiedzialnością jest sprawdzanie stanu innego obiektu.
Tak więc, z mojego punktu widzenia, nie tylko lepiej jest zawieść późno z perspektywy projektowania, ale także lepiej zawieść wewnątrz zbudowanej jednostki, niż w samym budowniczym.
UPD: ten komentarz przypomniał mi o jeszcze jednej możliwości, gdy sprawdzanie poprawności wewnątrz konstruktora (opcja 1 lub 2) ma sens. Ma to sens, jeśli budowniczy ma własne kontrakty na obiekty, które tworzy. Załóżmy na przykład, że mamy konstruktora, który konstruuje ciąg o określonej treści, powiedzmy, listę zakresów liczb
1-2,3-4,5-6
. Ten konstruktor może mieć taką metodęaddRange(int min, int max)
. Wynikowy ciąg nie wie nic o tych liczbach, ani nie powinien wiedzieć. Sam konstruktor definiuje format ciągu i ograniczenia liczb. Dlatego metodaaddRange(int,int)
musi sprawdzić poprawność liczb wejściowych i zgłosić wyjątek, jeśli max jest mniejsze niż min.To powiedziawszy, ogólną zasadą będzie sprawdzanie poprawności tylko umów określonych przez samego konstruktora.
źródło
Biorąc pod uwagę, że używasz Javy, rozważ autorytatywne i szczegółowe wskazówki dostarczone przez Joshua Blocha w artykule Tworzenie i niszczenie obiektów Java (pogrubiona czcionka w poniższym cytacie jest moja):
Uwaga: zgodnie z objaśnieniem redaktora tego artykułu, „elementy” w powyższym cytacie odnoszą się do reguł przedstawionych w Effective Java, wydanie drugie .
W artykule nie zagłębiono się w wyjaśnienie, dlaczego jest to zalecane, ale jeśli o tym pomyślisz, przyczyny są dość oczywiste. Ogólna wskazówka na temat zrozumienia tego znajduje się w tym artykule, w wyjaśnieniu, w jaki sposób koncepcja konstruktora jest połączona z koncepcją konstruktora - i oczekuje się, że niezmienniki klas będą sprawdzane w konstruktorze, a nie w żadnym innym kodzie, który może poprzedzać / przygotowywać jego wywołanie.
Aby bardziej konkretnie zrozumieć, dlaczego sprawdzanie niezmienników przed wywołaniem kompilacji byłoby błędne, rozważ popularny przykład CarBuilder . Metody konstruktora można wywoływać w dowolnej kolejności, w wyniku czego do momentu kompilacji nie można naprawdę wiedzieć, czy dany parametr jest prawidłowy.
Weź pod uwagę, że samochód sportowy nie może mieć więcej niż 2 miejsca. Skąd można wiedzieć, czy
setSeats(4)
wszystko jest w porządku? Dopiero w kompilacji można się z pewnością dowiedzieć, czysetSportsCar()
został wywołany, czy nie, co oznacza, czy rzucić,TooManySeatsException
czy nie.źródło
Nieprawidłowe wartości, które są nieprawidłowe, ponieważ nie są tolerowane, powinny moim zdaniem zostać natychmiast ujawnione. Innymi słowy, jeśli akceptujesz tylko liczby dodatnie i przekazywana jest liczba ujemna, nie trzeba czekać na
build()
wywołanie. Nie wziąłbym pod uwagę tego rodzaju problemów, których „można się spodziewać”, ponieważ jest to warunek wstępny wywołania metody na początek. Innymi słowy, prawdopodobnie nie będziesz zależał od niepowodzenia ustawienia niektórych parametrów. Bardziej prawdopodobne jest, że założysz, że parametry są poprawne lub sam wykonasz kontrolę.Jednak w przypadku bardziej skomplikowanych problemów, które nie są tak łatwo sprawdzane, lepiej poinformować o tym podczas rozmowy telefonicznej
build()
. Dobrym przykładem może być podanie podanych informacji o połączeniu w celu nawiązania połączenia z bazą danych. W takim przypadku, chociaż technicznie można sprawdzić takie warunki, nie jest już intuicyjny i tylko komplikuje kod. Moim zdaniem są to również rodzaje problemów, które mogą się zdarzyć i których nie można przewidzieć, dopóki nie spróbujesz. Jest to swego rodzaju różnica między dopasowaniem łańcucha do wyrażenia regularnego, aby sprawdzić, czy można go przeanalizować jako int, a po prostu próbą jego przetworzenia, obsługując wszelkie potencjalne wyjątki, które mogą wystąpić w konsekwencji.Ogólnie nie lubię zgłaszać wyjątków podczas ustawiania parametrów, ponieważ oznacza to konieczność wychwycenia zgłoszonego wyjątku, więc zazwyczaj preferuję sprawdzanie poprawności w
build()
. Z tego powodu wolę używać wyjątku RuntimeException, ponieważ ponownie błędy w przekazywanych parametrach zwykle nie powinny się zdarzyć.Jest to jednak najlepsza praktyka niż cokolwiek innego. Mam nadzieję, że to odpowiada na twoje pytanie.
źródło
O ile mi wiadomo, ogólną praktyką (nie jestem pewien, czy istnieje konsensus) jest nieudana, gdy tylko możliwe jest wykrycie błędu. Utrudnia to również niezamierzone niewłaściwe użycie interfejsu API.
Jeśli jest to trywialny atrybut, który można sprawdzić na wejściu, taki jak pojemność lub długość, która powinna być nieujemna, najlepiej od razu zawieść. Zatrzymanie błędu zwiększa odległość między błędem a sprzężeniem zwrotnym, co utrudnia znalezienie źródła problemu.
Jeśli masz nieszczęście być w sytuacji, gdy ważność atrybutu zależy od innych, masz dwie możliwości:
build()
tak się nazywa.Jak w przypadku większości rzeczy, jest to decyzja podejmowana w kontekście. Jeśli kontekst powoduje, że wczesne niepowodzenia jest niewygodne lub skomplikowane, można dokonać kompromisu, aby odłożyć kontrole na później, ale domyślnie powinno być szybkie przełączanie.
źródło
unsigned
,@NonNull
itp.X
na wartość, która jest nieprawidłowa, biorąc pod uwagę obecną wartośćY
, ale przed wywołaniembuild()
ustawY
wartość, która sprawi, że będzieX
poprawna.Shape
a konstruktor maWithLeft
i maWithRight
właściwości, a chcesz dostosować budowniczego do budowy obiektu w innym miejscu, wymaganie, abyWithRight
wywołać go najpierw podczas przesuwania obiektu w prawo, aWithLeft
podczas przesuwania go w lewo, spowodowałoby niepotrzebną złożoność w porównaniu zWithLeft
ustawieniem lewej krawędzi po prawej stronie starej prawej krawędzi, pod warunkiem, żeWithRight
naprawia prawą krawędź przedbuild
wywołaniem.Podstawową zasadą jest „wczesne niepowodzenie”.
Nieco bardziej zaawansowaną zasadą jest „zawodzić jak najwcześniej”.
Jeśli właściwość jest z natury nieważna ...
... wtedy natychmiast go odrzucasz.
Inne przypadki mogą wymagać kombinacji wartości i mogą być lepiej umieszczone w metodzie build ():
źródło