jak skomplikowany powinien być konstruktor

18

Rozmawiam z moim współpracownikiem o tym, ile pracy może wykonać konstruktor. Mam klasę B, która wewnętrznie wymaga innego obiektu A. Obiekt A jest jednym z niewielu elementów, których klasa B potrzebuje do wykonania swojej pracy. Wszystkie jego publiczne metody zależą od wewnętrznego obiektu A. Informacje o obiekcie A są przechowywane w bazie danych, więc próbuję sprawdzić i uzyskać je, sprawdzając je w bazie danych w konstruktorze. Mój współpracownik zwrócił uwagę, że konstruktor nie powinien wykonywać dużo pracy poza przechwytywaniem parametrów konstruktora. Ponieważ wszystkie publiczne metody i tak zawiodłyby, gdyby obiekt A nie został znaleziony przy użyciu danych wejściowych do konstruktora, argumentowałem, że zamiast zezwalać na utworzenie instancji i późniejsze niepowodzenie, w rzeczywistości lepiej jest rzucić ją wcześniej w konstruktorze.

Co myślą inni? Używam C #, jeśli to robi jakąkolwiek różnicę.

Czytanie Czy jest jakiś powód, aby wykonywać całą pracę obiektu w konstruktorze? zastanawiam się, czy pobranie obiektu A przez przejście do bazy danych jest częścią „każdej innej inicjalizacji niezbędnej do przygotowania obiektu do użycia”, ponieważ jeśli użytkownik przekaże konstruktorowi nieprawidłowe wartości, nie byłbym w stanie użyć żadnej z jego publicznych metod.

Konstruktory powinny utworzyć instancję pól obiektu i wykonać dowolną inną inicjalizację niezbędną do przygotowania obiektu do użycia. Ogólnie oznacza to, że konstruktory są małe, ale istnieją scenariusze, w których byłoby to znaczną ilość pracy.

Taein Kim
źródło
8
Nie zgadzam się. Spójrz na zasadę inwersji zależności, byłoby lepiej, gdyby obiekt A został przekazany do obiektu B w poprawnym stanie w swoim konstruktorze.
Jimmy Hoffa
5
Pytasz, ile można zrobić? Ile należy zrobić? Lub ile konkretnej sytuacji należy zrobić? To trzy bardzo różne pytania.
Bobson
1
Zgadzam się z sugestią Jimmy'ego Hoffy, by spojrzeć na zastrzyk zależności (lub „odwrócenie zależności”). To była moja pierwsza myśl po przeczytaniu opisu, ponieważ brzmi to tak, jakbyś już był w połowie drogi; masz już funkcjonalność podzieloną na klasę A, z której korzysta klasa B. Nie mogę rozmawiać z twoim przypadkiem, ale ogólnie uważam, że kod refaktoryzacji w celu użycia wzorca wstrzykiwania zależności sprawia, że ​​klasy są prostsze / mniej powiązane.
Dr Wily's Apprentice
1
Bobson, zmieniłem tytuł na „powinien”. Proszę o najlepszą praktykę tutaj.
Taein Kim
1
@JimmyHoffa: może powinieneś rozwinąć swój komentarz w odpowiedź.
kevin cline

Odpowiedzi:

22

Twoje pytanie składa się z dwóch całkowicie oddzielnych części:

Czy powinienem zgłosić wyjątek od konstruktora, czy powinienem mieć metodę niepowodzenia?

Jest to wyraźne zastosowanie zasady szybkiego działania. Awarie konstruktora są znacznie łatwiejsze do debugowania w porównaniu z koniecznością znalezienia przyczyny niepowodzenia metody. Na przykład instancja może zostać już utworzona z innej części kodu i podczas wywoływania metod występują błędy. Czy to oczywiste, że obiekt został utworzony źle? Nie.

Jeśli chodzi o problem „zawiń połączenie w try / catch”. Wyjątki mają być wyjątkowe . Jeśli wiesz, że jakiś kod zgłosi wyjątek, nie pakujesz go w try / catch, ale sprawdzasz parametry tego kodu przed wykonaniem kodu, który może wygenerować wyjątek. Wyjątki mają na celu jedynie zapewnienie, że system nie przejdzie w stan nieprawidłowy. Jeśli wiesz, że niektóre parametry wejściowe mogą prowadzić do nieprawidłowego stanu, upewnij się, że parametry te nigdy się nie zdarzają. W ten sposób musisz spróbować try / catch tylko w miejscach, w których możesz logicznie obsłużyć wyjątek, który zwykle znajduje się na granicy systemu.

Czy mogę uzyskać dostęp do „innych części systemu, takich jak DB” z konstruktora.

Myślę, że jest to sprzeczne z zasadą najmniejszego zdziwienia . Niewiele osób oczekiwałoby od konstruktora dostępu do bazy danych. Więc nie, nie powinieneś tego robić.

Euforyk
źródło
2
Bardziej odpowiednim rozwiązaniem jest zazwyczaj dodanie metody typu „otwarty”, w której konstrukcja jest bezpłatna, ale nie można użyć przed „otwarciem” / „połączeniem” / itd., Gdzie następuje cała faktyczna inicjalizacja. Awarie konstruktorów mogą powodować różnego rodzaju problemy, gdy w przyszłości chcesz wykonać częściową konstrukcję obiektów, co jest przydatną umiejętnością.
Jimmy Hoffa
@ JimmyHoffa Nie widzę różnicy między metodą konstruktora a konkretną metodą budowy.
Euforyczny
4
Możesz nie, ale wielu tak. Zastanów się, kiedy tworzysz obiekt połączenia, czy konstruktor go łączy? Czy obiekt jest użyteczny, jeśli nie jest podłączony? W obu pytaniach odpowiedź brzmi „nie”, ponieważ pomocne jest częściowe zbudowanie obiektów połączeń, aby można było dostosować ustawienia i inne rzeczy przed otwarciem połączenia. To jest wspólne podejście do tego scenariusza. Nadal uważam, że wstrzyknięcie konstruktora jest właściwym podejściem w scenariuszach, w których obiekt A ma zależność od zasobów, ale otwarta metoda jest wciąż lepsza niż zmuszanie konstruktora do wykonywania niebezpiecznej pracy.
Jimmy Hoffa
@JimmyHoffa Ale to specyficzne wymaganie dotyczące klasy połączenia, a nie ogólna zasada projektowania OOP. Właściwie nazwałbym to wyjątkowym przypadkiem niż regułą. Mówisz w zasadzie o wzorze konstruktora.
Euforyczny
2
To nie jest wymóg, to była decyzja projektowa. Mogli zmusić konstruktora do wzięcia ciągu połączenia i połączenia natychmiast po zbudowaniu. Postanowili nie zbyt, ponieważ jest to pomocne, aby móc utworzyć obiekt, a następnie dowiedzieć się uszczypnąć ciągu połączenia lub innych bitów i bobbles i połączyć je później ,
Jimmy Hoffa
19

Ugh.

Konstruktor powinien robić jak najmniej - problem polega na tym, że zachowanie wyjątków w konstruktorach jest niezręczne. Bardzo niewielu programistów wie, jakie jest właściwe zachowanie (w tym scenariusze dziedziczenia), a zmuszanie użytkowników do próbowania / wychwytywania każdej pojedynczej instancji jest ... w najlepszym razie bolesne.

Są to dwie części: w konstruktorze i przy użyciu konstruktora. W konstruktorze jest to niewygodne, ponieważ nie można wiele zdziałać w przypadku wystąpienia wyjątku. Nie możesz zwrócić innego typu. Nie możesz zwrócić wartości null. Zasadniczo możesz powtórzyć wyjątek, zwrócić uszkodzony obiekt (zły konstruktor) lub (najlepiej) zastąpić część wewnętrzną rozsądnym ustawieniem domyślnym. Używanie konstruktora jest niewygodne, ponieważ wtedy twoje instancje (i instancje wszystkich typów pochodnych!) Mogą rzucać. Następnie nie można ich używać w inicjalizatorach członków. W końcu używasz wyjątków dla logiki, aby zobaczyć „czy moje dzieło się powiodło?”, Wykraczające poza czytelność (i kruchość) spowodowaną wszędzie przez try / catch.

Jeśli twój konstruktor robi rzeczy nietrywialne lub rzeczy, których można się spodziewać, że zawiedzie, rozważ Createmetodę statyczną (z konstruktorem niepublicznym). Jest o wiele bardziej użyteczny, łatwiejszy do debugowania, a mimo to zapewnia w pełni skonstruowaną instancję, gdy odniesie sukces.

Telastyn
źródło
2
Skomplikowane scenariusze budowy są dokładnie tym, dlaczego istnieją wzorce kreacji, o których tu wspominasz. To dobre podejście do tych problematycznych konstrukcji. Dano mi do myślenia o tym, jak w .NET Connectionobiekt może CreateCommanddla ciebie, abyś wiedział, że polecenie jest odpowiednio połączone.
Jimmy Hoffa
2
Dodam do tego, że Baza danych jest silną zależnością zewnętrzną, przypuśćmy, że chcesz w przyszłości zmienić bazy danych (lub wyśmiać obiekt do testowania), przenosząc ten rodzaj kodu do klasy Factory (lub coś w rodzaju usługi IService) dostawca) wydaje się czystszym podejściem
Jason Sperske
4
czy możesz rozwinąć kwestię „zachowanie wyjątków w konstruktorach jest niezręczne”? Gdybyś użył Utwórz, czy nie musiałbyś zawijać go w try catch, tak jak zawijasz wywołanie do konstruktora? Wydaje mi się, że Create to po prostu inna nazwa dla konstruktora. Może nie otrzymuję twojej właściwej odpowiedzi?
Taein Kim
1
Musiałem spróbować wyłapać wyjątki od konstruktorów, +1 do argumentów przeciwko temu. Pracując z ludźmi, którzy mówią „Brak pracy w konstruktorze”, może to również powodować dziwne wzorce. Widziałem kod, w którym każdy obiekt ma nie tylko konstruktor, ale także jakąś formę Initialize () gdzie, zgadnij co? Obiekt nie jest tak naprawdę ważny, dopóki nie zostanie wywołany. A potem masz dwie rzeczy do wywołania: ctor i Initialize (). Problem wykonywania pracy w celu ustawienia obiektu nie znika - C # może dać inne rozwiązania niż powiedzmy C ++. Fabryki są dobre!
J Trana
1
@jtrana - tak, inicjalizacja nie jest dobra. Konstruktor inicjuje pewne rozsądne ustawienie domyślne lub fabryka lub metoda create go buduje i zwraca użyteczną instancję.
Telastyn
1

Konstruktor - równowaga złożoności metody jest rzeczywiście kwestią dyskusji na temat dobrego stylu. Dość często Class() {}używany jest pusty konstruktor , z metodą Init(..) {..}bardziej skomplikowanych rzeczy. Wśród argumentów, które należy wziąć pod uwagę:

  • Możliwość serializacji klas
  • Dzięki metodzie separacji Init od konstruktora często trzeba wielokrotnie powtarzać tę samą sekwencję Class x = new Class(); x.Init(..);, częściowo w testach jednostkowych. Jednak nie tak dobre do czytania. Czasami po prostu umieszczam je w jednym wierszu kodu.
  • Gdy celem testu jednostkowego jest po prostu test inicjalizacji klasy, lepiej mieć własny Init, aby wykonywać bardziej skomplikowane czynności wykonywane jeden po drugim, z pośrednimi Asertami.
Pavel Khrapkin
źródło
moim zdaniem zaletą tej initmetody jest to, że obsługa awarii jest o wiele łatwiejsza. W szczególności zwracana wartość initmetody może wskazywać, czy inicjalizacja się powiodła, a metoda może zapewnić, że obiekt jest zawsze w dobrym stanie.
pqnet