Konstruktor zasadniczo nie powinien wywoływać metod

12

Opisałem koledze, dlaczego konstruktor wywołujący metodę może być antypatternem.

przykład (w moim zardzewiałym C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Chciałbym motywować ten fakt lepiej poprzez wasz dodatkowy wkład. Jeśli masz przykłady, odniesienia do książek, strony na blogu lub nazwy zasad, będą one bardzo mile widziane.

Edycja: Mówię ogólnie, ale kodujemy w Pythonie.

Stefano Borini
źródło
Czy jest to ogólna zasada czy specyficzna dla poszczególnych języków?
ChrisF
Jaki język? W C ++ to coś więcej niż anty-wzór: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, OP mówi o „metodach klasowych”, co - przynajmniej dla mnie - oznacza metody nieinstancyjne . Który dlatego nie może być wirtualny.
Péter Török
3
@Alb W Javie jest całkowicie w porządku. Nie powinieneś jednak wyraźnie przekazywać thisżadnej z metod wywoływanych z konstruktora.
biziclop
3
@Stefano Borini: Jeśli kodujesz w Pythonie, dlaczego nie pokazać przykładu w Pythonie zamiast zardzewiałego C ++? Wyjaśnij też, dlaczego jest to zła rzecz. Robimy to cały czas.
S.Lott

Odpowiedzi:

26

Nie określiłeś języka.

W C ++ konstruktor musi uważać podczas wywoływania funkcji wirtualnej, ponieważ rzeczywista funkcja, którą wywołuje, jest implementacją klasy. Jeśli jest to czysto wirtualna metoda bez implementacji, będzie to naruszenie zasad dostępu.

Konstruktor może wywoływać funkcje inne niż wirtualne.

Jeśli Twoim językiem jest Java, w którym funkcje są domyślnie wirtualne, warto zachować szczególną ostrożność.

Wydaje się, że C # radzi sobie z sytuacją tak, jak byś się spodziewał: możesz wywoływać metody wirtualne w konstruktorach i wywołuje najbardziej ostateczną wersję. Więc w C # nie jest anty-wzorem.

Częstym powodem wywoływania metod z konstruktorów jest to, że masz wielu konstruktorów, którzy chcą wywołać wspólną metodę „init”.

Zauważ, że destruktory będą miały ten sam problem z metodami wirtualnymi, dlatego nie możesz mieć wirtualnej metody „czyszczenia”, która znajduje się poza twoim destruktorem i oczekujesz, że zostanie wywołany przez destruktor klasy podstawowej.

Java i C # nie mają destruktorów, mają finalizatory. Nie znam zachowania w Javie.

Wygląda na to, że C # poprawnie obsługuje czyszczenie.

(Zauważ, że chociaż Java i C # mają funkcje odśmiecania pamięci, to tylko zarządza alokacją pamięci. Istnieje jeszcze inne czyszczenie, które musi wykonać Twój destruktor, ale nie zwalnia ono pamięci).

Dojną krową
źródło
13
Jest tu kilka drobnych błędów. Metody w języku C # nie są domyślnie wirtualne. C # ma inną semantykę niż C ++ podczas wywoływania metody wirtualnej w konstruktorze; zostanie wywołana metoda wirtualna na najbardziej pochodnym typie, a nie metoda wirtualna na części aktualnie budowanego typu. C # nazywa swoje metody finalizacji „destruktorami”, ale masz rację, że mają one semantykę finalizatorów. Metody wirtualne wywoływane w C # destruktorach działają w taki sam sposób, jak w konstruktorach; wywoływana jest metoda najbardziej pochodna.
Eric Lippert,
@ Péter: Zamierzałem metody instancji. przepraszam za zamieszanie.
Stefano Borini,
1
@Eric Lippert. Dziękuję za Twoją wiedzę na temat C #, odpowiednio zmodyfikowałem swoją odpowiedź. Nie znam tego języka, znam C ++ bardzo dobrze, a Java słabiej.
CashCow,
5
Nie ma za co. Zauważ, że wywołanie metody wirtualnej w konstruktorze klasy podstawowej w C # jest nadal dość złym pomysłem.
Eric Lippert
Jeśli wywołasz (wirtualną) metodę w Javie z konstruktora, zawsze wywoła ona najbardziej wyprowadzone zastąpienie. Jednak to, co nazywacie „tak, jak byście się tego spodziewali”, jest tym, co nazywam mylącym. Ponieważ podczas gdy Java wywołuje przesłonięcie najbardziej wyprowadzone, ta metoda będzie widziała tylko przetworzone inicjatory plików, ale nie konstruktor własnego uruchomienia klasy. Wywołanie metody na klasie, która nie ma jeszcze niezmiennika, może być niebezpieczne. Myślę więc, że C ++ dokonał tutaj lepszego wyboru.
5gon12eder
18

OK, teraz, że zamieszanie dotyczące metod klasy vs metody instancji jest wyjaśniona, mogę dać odpowiedź :-)

Problem nie polega na wywołaniu metod instancji w ogóle z konstruktora; dotyczy to wywoływania metod wirtualnych (bezpośrednio lub pośrednio). A głównym powodem jest to, że obiekt wewnątrz konstruktora nie jest jeszcze w pełni zbudowany . A zwłaszcza jego części podklasy wcale nie są konstruowane podczas działania konstruktora klasy podstawowej. Zatem jego stan wewnętrzny jest niespójny w sposób zależny od języka, co może powodować różne subtelne błędy w różnych językach.

C ++ i C # zostały już omówione przez innych. W Javie zostanie wywołana metoda wirtualna najbardziej pochodnego typu, jednak ten typ nie został jeszcze zainicjowany. Jeśli więc w tej metodzie są używane pola z typu pochodnego, pola te mogą jeszcze nie zostać poprawnie zainicjowane w tym momencie. Problem ten został szczegółowo omówiony w punkcie 17 wydajnej wersji Java 2. edycji : Projekt i dokument do dziedziczenia, w przeciwnym razie jest to zabronione .

Należy zauważyć, że jest to szczególny przypadek ogólnego problemu przedwczesnego publikowania odwołań do obiektów . Metody instancji mają thisparametr niejawny , ale thisjawne przekazanie do metody może powodować podobne problemy. Zwłaszcza w programach współbieżnych, w których jeśli odwołanie do obiektu jest przedwcześnie publikowane w innym wątku, wątek ten może już wywoływać metody przed zakończeniem konstruktora w pierwszym wątku.

Péter Török
źródło
3
(+1) „wewnątrz konstruktora obiekt nie jest jeszcze w pełni zbudowany.” To samo co „metody klasowe a instancja”. Niektóre języki programowania uważają, że jest on konstruowany podczas wchodzenia do konstruktora, tak jakby programista przypisywał wartości do konstruktora.
umlcat
7

Nie uważałbym, że wywołania metod tutaj są antypatternem, a raczej zapachem kodu. Jeśli klasa dostarcza resetmetodę, która przywraca obiekt do pierwotnego stanu, wówczas wywoływanie reset()konstruktora to DRY. (Nie wypowiadam się na temat metod resetowania).

Oto artykuł, który może pomóc w spełnieniu twojego apelu o autorytet: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Tak naprawdę nie chodzi o wywoływanie metod, ale o konstruktory, które robią za dużo. IMHO, wywoływanie metod w konstruktorze to zapach, który może wskazywać, że konstruktor jest zbyt ciężki.

Jest to związane z tym, jak łatwo przetestować kod. Powody obejmują:

  1. Testy jednostkowe obejmują wiele kreacji i zniszczeń - dlatego budowa powinna być szybka.

  2. W zależności od tego, co robią te metody, może to utrudnić testowanie dyskretnych jednostek kodu bez polegania na pewnych (potencjalnie niemożliwych do przetestowania) warunkach wstępnych ustawionych w konstruktorze (np. Uzyskaniu informacji z sieci).

Paul Butcher
źródło
3

Filozoficznie, celem konstruktora jest przekształcenie surowej części pamięci w instancję. Podczas wykonywania konstruktora obiekt jeszcze nie istnieje, dlatego wywoływanie jego metod jest złym pomysłem. W końcu możesz nie wiedzieć, co robią wewnętrznie, i mogą słusznie uznać, że obiekt przynajmniej istnieje (duh!), Kiedy zostaną wywołani.

Technicznie rzecz biorąc, nie może być w tym nic złego, w C ++, a zwłaszcza w Pythonie, od ciebie zależy, abyś był ostrożny.

Praktycznie powinieneś ograniczać wywołania tylko do takich metod, które inicjują członków klasy.


źródło
2

To nie jest problem ogólnego zastosowania. Jest to problem w C ++, szczególnie podczas korzystania z dziedziczenia i metod wirtualnych, ponieważ konstrukcja obiektu odbywa się wstecz, a wskaźniki vtable są resetowane z każdą warstwą konstruktora w hierarchii dziedziczenia, więc jeśli wywołujesz metodę wirtualną, możesz nie w końcu dostajesz taki, który faktycznie odpowiada klasie, którą próbujesz stworzyć, co pokonuje cały cel korzystania z metod wirtualnych.

W językach z rozsądną obsługą OOP, które od samego początku ustawiają wskaźnik vtable, ten problem nie istnieje.

Mason Wheeler
źródło
2

Istnieją dwa problemy z wywoływaniem metody:

  • wywoływanie metody wirtualnej, która może albo zrobić coś nieoczekiwanego (C ++), albo użyć części obiektów, które nie zostały jeszcze zainicjowane
  • wywołanie metody publicznej (która powinna wymusić niezmienniki klasy), ponieważ obiekt niekoniecznie jest jeszcze kompletny (a zatem jego niezmiennik może się nie utrzymywać)

Nie ma nic złego w wywoływaniu funkcji pomocnika, o ile nie mieści się ona w dwóch poprzednich przypadkach.

Matthieu M.
źródło
1

Nie kupuję tego. W systemie obiektowym wywołanie metody jest właściwie jedyną rzeczą, którą możesz zrobić. W rzeczywistości jest to mniej więcej definicja „obiektowa”. Jeśli więc konstruktor nie może wywołać żadnej metody, to co może zrobić?

Jörg W Mittag
źródło
Zainicjuj obiekt.
Stefano Borini
@Stefano Borini: Jak? W systemie obiektowym jedyne, co możesz zrobić, to metody wywoływania. Lub spojrzeć na to z przeciwnej strony: wszystko odbywa się przez wywołanie metod. I „wszystko” oczywiście obejmuje inicjalizację obiektu. Jeśli więc, aby zainicjować obiekt, musisz wywołać metody, ale konstruktory nie mogą wywoływać metod, to w jaki sposób konstruktor może zainicjować obiekt?
Jörg W Mittag
absolutnie nie jest prawdą, że jedyne, co możesz zrobić, to wywołać metody. Możesz po prostu zainicjować stan bez żadnego wywołania, bezpośrednio do wewnętrznych elementów twojego obiektu ... Konstruktorem jest stworzenie obiektu w spójnym stanie. Jeśli wywołasz inne metody, mogą wystąpić problemy z obsługą obiektu w stanie częściowym, chyba że są to metody specjalnie zaprojektowane do wywoływania z konstruktora (zwykle jako metody pomocnicze)
Stefano Borini
@Stefano Borini: „Możesz po prostu zainicjować stan bez żadnego wywołania, bezpośrednio do wewnętrznych elementów twojego obiektu.” Niestety, co dotyczy metody? Skopiować i wkleić kod?
S.Lott,
1
@ S.Lott: nie, nazywam to, ale staram się zachować funkcję modułu zamiast metody obiektowej i chcę, aby dostarczyła dane zwrotne, które mogę wprowadzić w stan obiektu w konstruktorze. Jeśli naprawdę muszę mieć metodę obiektową, ustawię ją jako prywatną i wyjaśnię, że jest to inicjalizacja, na przykład nadanie jej właściwej nazwy. Nigdy jednak nie wywołałbym metody publicznej, aby ustawić status obiektu od konstruktora.
Stefano Borini,
0

W teorii OOP nie powinno to mieć znaczenia, ale w praktyce każdy język programowania OOP obsługuje konstruktory inaczej . Nie używam często metod statycznych.

W C ++ i Delphi, gdybym musiał podać wartości początkowe niektórym właściwościom („elementom pola”), a kod jest bardzo rozszerzony, dodaję dodatkowe metody jako rozszerzenie konstruktorów.

I nie nazywaj innych metod, które robią bardziej złożone rzeczy.

Jeśli chodzi o metody „getters” i „setters”, zwykle używam zmiennych prywatnych / chronionych do przechowywania ich stanu, a także metod „getters” i „setters”.

W konstruktorze przypisuję „domyślne” wartości do pól stanu właściwości, BEZ wywoływania „akcesorów”.

umlcat
źródło