Czy metody init () pachną kodem?

20

Czy jest jakiś cel deklarowania init()metody dla typu?

Nie pytam, czy powinniśmy preferować init()konstruktor, czy jak uniknąć deklarowaniainit() .

Pytam, czy istnieje jakikolwiek uzasadnienie deklaracji init()metody (zobaczenie, jak często jest stosowana) lub czy jest to zapach kodu i należy tego unikać.


Ten init()idiom jest dość powszechny, ale jeszcze nie widziałem żadnych realnych korzyści.

Mówię o typach, które zachęcają do inicjalizacji za pomocą metody:

class Demo {
    public void init() {
        //...
    }
}

Kiedy będzie to kiedykolwiek przydatne w kodzie produkcyjnym?


Wydaje mi się, że może to być zapach kodu, ponieważ sugeruje, że konstruktor nie w pełni zainicjował obiekt, w wyniku czego powstał częściowo obiekt. Obiekt nie powinien istnieć, jeśli jego stan nie jest ustawiony.

To sprawia, że ​​wierzę, że może to być część techniki stosowanej do przyspieszenia produkcji w sensie aplikacji korporacyjnych. To jedyny logiczny powód, dla którego mogę wymyślić taki idiom, po prostu nie jestem pewien, czy byłoby to korzystne, gdyby tak było.

Vince Emigh
źródło
1
Aby „... zobaczyć, jak to jest powszechne ...”: czy to jest powszechne? Czy możesz podać jakieś przykłady? Być może masz do czynienia z frameworkiem, który wymaga oddzielenia inicjalizacji i konstrukcji.
PRZYJEDŹ
Czy metoda znajduje się w klasie bazowej, klasie pochodnej, czy w obu? (Lub: czy metoda znajduje się w klasie należącej do hierarchii dziedziczenia? Czy klasa podstawowa wywołuje klasę init()pochodną, ​​czy odwrotnie?) Jeśli tak, to przykład pozwalający klasie podstawowej wykonać „postkonstruktor” ”, które można wykonać tylko po zakończeniu budowy najbardziej pochodnej klasy. Jest to przykład inicjalizacji wielofazowej.
rwong
Jeśli nie chcesz inicjować w momencie tworzenia instancji, warto rozdzielić te dwa elementy.
JᴀʏMᴇᴇ
możesz być zainteresowany. is-a-start-run-or-execute-method-a-good-practice
Laiv

Odpowiedzi:

39

Tak, to zapach kodu. Zapach kodu nie jest czymś, co zawsze musi zostać usunięte. To coś, co sprawia, że ​​patrzysz po raz drugi.

Tutaj masz obiekt w dwóch zasadniczo różnych stanach: przedinicjalizacyjnym i poinicjalizacyjnym. Państwa te mają różne obowiązki, różne metody, które mogą być wywoływane, i różne zachowania. W rzeczywistości są to dwie różne klasy.

Jeśli fizycznie utworzysz z nich dwie osobne klasy, to statycznie usuniesz całą klasę potencjalnych błędów, kosztem być może sprawiając, że twój model nie będzie pasował do „prawdziwego modelu świata” tak blisko. Zazwyczaj nazwać pierwszy Configlub Setupczy coś takiego.

Więc następnym razem spróbuj przekształcić idiomy konstruktywno-inicjujące w modele dwuklasowe i zobacz, jak ci się to uda.

Karl Bielefeldt
źródło
6
Twoja sugestia wypróbowania modelu dwuklasowego jest dobra. Przydatne jest zaproponowanie konkretnego kroku w celu wyeliminowania zapachu kodu.
Ivan
1
To najlepsza jak dotąd odpowiedź. Jedną rzecz mnie jednak wkurza: „ Te stany mają różne obowiązki, różne metody, które mogą być wywoływane, i inne zachowanie ” - Nieoddzielenie tych obowiązków narusza SRP. Miałoby to sens, gdyby celem oprogramowania było odtworzenie scenariusza z prawdziwego świata w każdym aspekcie. Ale w produkcji deweloperzy piszą głównie kod, który zachęca do łatwego zarządzania, w razie potrzeby poprawiając model, aby lepiej pasował do środowiska programowego (ciąg dalszy w następnym komentarzu)
Vince Emigh,
1
Prawdziwy świat to inne środowisko. Programy próbujące go replikować wydają się być specyficzne dla domeny, ponieważ w większości projektów nie należy go / nie należy uwzględniać. Wiele rzeczy, które są nieprzyzwoite, są akceptowane, jeśli chodzi o projekty specyficzne dla domeny, dlatego staram się zachować to tak ogólne, jak to możliwe (na przykład, jak wywoływanie thiskonstruktora jest zapachem kodu i może powodować kod podatny na błędy, i unikanie go jest zalecane bez względu na domenę, do której należy Twój projekt).
Vince Emigh,
14

To zależy.

initMetoda zapach kodu gdy nie jest to konieczne, aby inicjowanie przedmiot oddzielony od konstruktora. Czasami zdarza się, że warto oddzielić te kroki.

Szybkie wyszukiwanie w Google dało mi ten przykład. Mogę łatwo wyobrazić sobie więcej przypadków, w których kod wykonywany podczas przydzielania obiektów (konstruktor) mógłby być lepiej oddzielony od samej inicjalizacji. Być może masz system zrównany, a alokacja / konstrukcja odbywa się na poziomie X, ale inicjalizacja tylko na poziomie Y, ponieważ tylko Y może zapewnić niezbędne parametry. Być może „init” jest kosztowny i musi zostać uruchomiony tylko dla podzbioru przydzielonych obiektów, a określenie tego podzbioru można wykonać tylko na poziomie Y. Lub chcesz zastąpić (wirtualną) metodę „init” w pochodnej klasa, której nie można wykonać za pomocą konstruktora. Być może poziom X zapewnia przydzielone obiekty z drzewa dziedziczenia, ale poziom Y nie jest świadomy konkretnej pochodnej, tylko wspólnego interfejsu (gdzieinit może zdefiniowane).

Oczywiście, z mojego doświadczenia initwynika, że przypadki te stanowią jedynie niewielki procent standardowego przypadku, w którym wszystkie inicjowanie można wykonać bezpośrednio w konstruktorze, a ilekroć zobaczysz oddzielną metodę, dobrym pomysłem może być zakwestionowanie jej konieczności.

Doktor Brown
źródło
2
Odpowiedź w tym łączu mówi, że jest przydatny do zmniejszenia ilości pracy w konstruktorze, ale zachęca do tworzenia częściowo utworzonych obiektów. Mniejsze konstruktory można uzyskać poprzez dekompozycję, więc wydaje mi się, że odpowiedzi stwarzają nowy problem (możliwość zapomnienia o wywołaniu wszystkich wymaganych metod inicjalizacji, pozostawiając obiekt podatny na błędy) i należy do kategorii zapachu kodu
Vince Emigh
@VinceEmigh: ok, to był pierwszy przykład mogę tu na platfform SE, może nie najlepszy, ale nie uzasadnione przypadki użycia dla odrębnej initmetody. Jednak gdy tylko zobaczysz taką metodę, możesz zakwestionować jej konieczność.
Doc Brown
Kwestionuję / kwestionuję każdy przypadek użycia dla tego, ponieważ uważam, że nie ma sytuacji, w której byłby to koniecznością. Dla mnie jest to zły czas tworzenia obiektu i należy tego unikać, ponieważ jest to kandydat na błędy, których można uniknąć dzięki odpowiedniemu projektowaniu. Jeśli init()metoda byłaby właściwie wykorzystywana , jestem pewien, że skorzystałbym z poznania jej celu. Przepraszam za moją niewiedzę, jestem po prostu zaskoczony, jak trudno mi znaleźć dla niej pożytek, uniemożliwiając mi rozważenie tego, czego należy unikać
Vince Emigh,
1
@VinceEmigh: kiedy nie możesz pomyśleć o takiej sytuacji, musisz popracować nad wyobraźnią ;-). Lub przeczytaj ponownie moją odpowiedź, nie ograniczaj jej tylko do „alokacji”. Lub pracuj z większą liczbą platform różnych dostawców.
Doc Brown
1
@DocBrown Używanie wyobraźni zamiast sprawdzonych praktyk prowadzi do pewnego funky kodu, takiego jak inicjalizacja podwójnego nawiasu klamrowego: sprytna, ale nieefektywna i należy jej unikać. Wiem, że dostawcy go używają, ale to nie znaczy, że użycie jest uzasadnione. Jeśli czujesz, że tak jest, daj mi znać, ponieważ właśnie to próbowałem rozgryźć. Wydawało ci się, że jesteś całkowicie zdeterminowany, mając KOGO korzystny cel, ale brzmi to tak, jakbyś miał trudności z podaniem przykładu zachęcającego do dobrego projektu. Wiem, w jakich okolicznościach można go użyć, ale czy należy go stosować?
Vince Emigh,
5

Moje doświadczenie dzieli się na dwie grupy:

  1. Kod, w którym faktycznie wymagana jest funkcja init (). Może się to zdarzyć, gdy nadklasa lub struktura uniemożliwia konstruktorowi klasy uzyskanie wszystkich jego zależności podczas budowy.
  2. Kod, w którym używana jest funkcja init (), której można było uniknąć.

Z mojego osobistego doświadczenia widziałem tylko kilka przykładów (1), ale wiele innych przykładów (2). W związku z tym zwykle zakładam, że init () to zapach kodu, ale nie zawsze tak jest. Czasami po prostu nie da się tego obejść.

Odkryłem, że użycie Wzorca konstruktora często pomaga usunąć potrzebę / chęć posiadania init ().

Ivan
źródło
1
Jeśli nadklasa lub struktura nie pozwala typowi uzyskać zależności, których potrzebuje za pomocą konstruktora, jak dodanie init()metody go rozwiązałoby? Ta init()metoda wymagałaby parametrów do zaakceptowania zależności lub musiałaby zostać utworzona instancja zależności w ramach init()metody, co można również zrobić z konstruktorem. Czy możesz podać przykład?
Vince Emigh,
1
@VinceEmigh: init () może czasem być użyte do załadowania pliku konfiguracyjnego z zewnętrznego źródła, otwarcia połączenia z bazą danych lub czegoś podobnego. W ten sposób używana jest metoda DoFn.initialize () (z frameworku Apache Crunch). Można go również użyć do załadowania wewnętrznych pól, które nie mogą być serializowane (DoFns musi być możliwy do serializacji). Dwa problemy tutaj to, że (1) coś musi zapewnić, że wywoływana jest metoda inicjalizacji, i (2) obiekt musi wiedzieć, gdzie zamierza uzyskać (lub jak zbudować) te zasoby.
Ivan
1

Typowy scenariusz, w którym przydaje się metoda Init, ma plik konfiguracyjny, który chcesz zmienić, i uwzględnić tę zmianę bez ponownego uruchamiania aplikacji. To oczywiście nie oznacza, że ​​metodę Init należy wywoływać niezależnie od konstruktora. Możesz wywołać metodę Init z konstruktora, a następnie wywołać ją później, gdy / jeśli zmienią się parametry konfiguracyjne.

Podsumowując: tak jak w przypadku większości dylematów, czy jest to zapach kodu, czy nie, zależy od sytuacji i okoliczności.

Vladimir Stokic
źródło
Jeśli aktualizacje konfiguracji, a to wymaga obiekt zresetować / zmienić jego stan na podstawie konfiguracji, nie sądzisz, że byłoby lepiej mieć akt obiekt jako obserwator kierunku Config?
Vince Emigh,
@Vince Emigh Not necesarilly. Obserwator działałby, gdybym wiedział dokładnie, kiedy konfiguracja się zmienia. Jeśli jednak dane konfiguracyjne są przechowywane w pliku, który można zmienić poza aplikacją, nie ma tak naprawdę eleganckiego podejścia. Na przykład, jeśli mam program, który analizuje niektóre pliki i przekształca dane w jakiś model wewnętrzny, a osobny plik konfiguracyjny zawiera wartości domyślne brakujących danych, jeśli zmienię wartości domyślne, przeczytam je ponownie przy następnym uruchomieniu parsowanie. Posiadanie metody Init w mojej aplikacji byłoby w tym przypadku całkiem przydatne.
Vladimir Stokic
Jeśli plik konfiguracyjny zostanie zmodyfikowany zewnętrznie w czasie wykonywania, nie ma sposobu na ponowne załadowanie tych zmiennych bez jakiegoś powiadomienia powiadamiającego twoją aplikację, że musi wywołać init ( update/ reloadprawdopodobnie byłby bardziej opisowy dla tego rodzaju zachowania), aby faktycznie zarejestrować te zmiany . W takim przypadku to powiadomienie spowoduje wewnętrzną zmianę wartości konfiguracji w aplikacji, co, jak sądzę, można zaobserwować, obserwując konfigurację, powiadamiając obserwatorów, kiedy konfiguracja ma zmienić jedną z jej wartości. A może źle rozumiem twój przykład?
Vince Emigh,
Aplikacja pobiera niektóre pliki zewnętrzne (eksportowane z niektórych GIS lub innego systemu), analizuje te pliki i przekształca je w model wewnętrzny, z którego korzystają systemy, z których korzysta aplikacja. Podczas tego procesu można znaleźć pewne luki w danych i te luki w danych można wypełnić, ustawiając wartości domyślne. Te wartości domyślne można zapisać w pliku konfiguracyjnym, który można odczytać na początku procesu transformacji, który jest wywoływany za każdym razem, gdy pojawiają się nowe pliki do transformacji. Tutaj może się przydać metoda Init.
Vladimir Stokic
1

Zależy od tego, jak z nich korzystasz.

Używam tego wzorca w językach odśmiecanych, takich jak Java / C #, gdy nie chcę ciągle przenosić obiektu na stos (np. Kiedy robię grę wideo i muszę utrzymać wysoką wydajność, śmieciarze zabijają wydajność). Używam konstruktora do dokonywania innych przydziałów sterty, których potrzebuje, orazinit do tworzenia podstawowego przydatnego stanu tuż za każdym razem, gdy chcę go ponownie użyć. Jest to związane z koncepcją pul obiektów.

Jest to również przydatne, jeśli masz kilka konstruktorów, które mają wspólny podzbiór instrukcji inicjalizacji, ale w takim przypadku initbędą prywatne. W ten sposób mogę zminimalizować każdy konstruktor tak bardzo, jak to możliwe, więc każdy zawiera tylko swoje unikalne instrukcje i pojedyncze wywołanieinit do wykonania reszty.

Ogólnie jest to jednak zapach kodu.

Cody
źródło
Czy reset()metoda nie byłaby bardziej opisowa dla twojego pierwszego stwierdzenia? Jeśli chodzi o drugi (wiele konstruktorów), posiadanie wielu konstruktorów to zapach kodu. Zakłada, że ​​obiekt ma wiele celów / obowiązków, co sugeruje naruszenie zasad SRP. Obiekt powinien mieć jedną odpowiedzialność, a konstruktor powinien zdefiniować wymagane zależności dla tej jednej odpowiedzialności. Jeśli masz wiele konstruktorów z powodu wartości opcjonalnych, powinni oni teleskopować (który jest również zapachem kodu, zamiast tego należy użyć konstruktora).
Vince Emigh,
@VinceEmigh, możesz użyć init lub zresetować, w końcu to tylko nazwa. Inicjacja ma większy sens w kontekście, w którym zwykle go używam, ponieważ nie ma sensu resetowanie obiektu, który nigdy nie był ustawiany po raz pierwszy. Jeśli chodzi o problem z konstruktorem, staram się unikać wielu konstruktorów, ale czasami jest to pomocne. Spójrz na stringlistę konstruktorów dowolnego języka , mnóstwo opcji. Dla mnie zwykle jest to maksymalnie 3 konstruktorów, ale wspólny podzbiór instrukcji podczas inicjalizacji ma sens, gdy współużytkują dowolny kod, ale różnią się w jakikolwiek sposób.
Cody
Nazwy są niezwykle ważne. Opisują zachowanie, które jest wykonywane bez zmuszania klienta do przeczytania umowy. Złe nazewnictwo może prowadzić do fałszywych założeń. Jeśli chodzi o wiele konstruktorów String, można to rozwiązać, oddzielając tworzenie ciągów. W końcu a Stringjest a String, a jego konstruktor powinien akceptować tylko to, co jest potrzebne, aby działało zgodnie z potrzebami. Większość z tych konstruktorów jest narażona na cele konwersji, co jest niewłaściwym użyciem konstruktorów. Konstruktory nie powinny wykonywać logiki lub ryzykują nieudaną inicjalizację, pozostawiając bezużyteczny obiekt.
Vince Emigh,
JDK jest pełen okropnych projektów, mógłbym wymienić około 10 z góry mojej głowy. Projektowanie oprogramowania ewoluowało, odkąd podstawowe aspekty wielu języków zostały publicznie ujawnione i trwają z powodu możliwości złamania kodu, jeśli miałby zostać przeprojektowany na współczesne czasy.
Vince Emigh,
1

init()metody mogą mieć pewien sens, gdy masz obiekty, które wymagają zasobów zewnętrznych (takich jak na przykład połączenie sieciowe), które są jednocześnie używane przez inne obiekty. Być może nie chcesz / nie musisz zawieszać zasobu przez cały okres istnienia obiektu. W takich sytuacjach możesz nie chcieć alokować zasobu w konstruktorze, gdy alokacja zasobów może się nie powieść.

Zwłaszcza w programowaniu wbudowanym chcesz mieć deterministyczny ślad pamięci, więc powszechną (dobrą?) Praktyką jest wczesne wywoływanie konstruktorów, może nawet statyczne, i inicjowanie dopiero później, gdy zostaną spełnione określone warunki.

Poza tymi przypadkami myślę, że wszystko powinno przejść do konstruktora.

tofro
źródło
Jeśli typ wymaga połączenia, należy użyć DI. Jeśli istnieje problem z utworzeniem połączenia, nie powinieneś tworzyć obiektu, który tego wymaga. Jeśli włączysz tworzenie połączenia w klasie, utworzysz obiekt, który utworzy instancję swoich zależności (połączenie). Jeśli wystąpienie instancji zależności nie powiedzie się, kończy się na obiekcie, którego nie można użyć, marnując w ten sposób zasoby.
Vince Emigh,
Niekoniecznie. Otrzymujesz obiekt, którego czasowo nie możesz użyć do wszystkich celów. W takich przypadkach obiekt może po prostu działać jako kolejka lub serwer proxy, dopóki zasób nie będzie dostępny. Całkowicie potępiające initmetody są zbyt ograniczające, IMHO.
tofro
0

Ogólnie wolę konstruktor, który odbiera wszystkie argumenty wymagane dla instancji funkcjonalnej. To wyjaśnia wszystkie zależności tego obiektu.

Z drugiej strony używam prostego frameworku konfiguracyjnego, który wymaga konstruktora publicznego bez parametrów i interfejsów do wstrzykiwania zależności i wartości konfiguracyjnych. Po tym, struktura konfiguracji wywołuje initmetodę obiektu: teraz otrzymałeś wszystkie rzeczy, które mam dla ciebie, wykonaj ostatnie kroki, aby przygotować się do pracy. Ale uwaga: to struktura konfiguracji, która automatycznie wywołuje metodę init, więc nie zapomnisz jej wywołać.

Bernhard Hiller
źródło
0

Nie ma zapachu kodu, jeśli metoda init () jest semantycznie osadzona w cyklu życia obiektu.

Jeśli potrzebujesz wywołać init (), aby ustawić obiekt w spójnym stanie, jest to zapach kodu.

Istnieje kilka technicznych przyczyn istnienia takiej struktury:

  1. hak do ramy
  2. resetowanie obiektu do stanu początkowego (unikaj redundancji)
  3. możliwość pominięcia podczas testów
oopexpert
źródło
1
Ale dlaczego inżynier oprogramowania miałby to włączyć w cykl życia? Czy jest jakikolwiek cel, który jest uzasadniony (biorąc pod uwagę, że zachęca do częściowo zbudowanych obiektów) i nie mógłby zostać zestrzelony przez bardziej wydajną alternatywę? Wydaje mi się, że osadzenie go w cyklu życia byłoby zapachem kodu i że należy go zastąpić lepszym czasem tworzenia obiektu (Po co przydzielać pamięć dla obiektu, jeśli nie planuje się go używać w tym czasie? Po co tworzyć obiekt, który jest częściowo skonstruowany, kiedy możesz po prostu poczekać, aż rzeczywiście go potrzebujesz, zanim go
utworzysz
Chodzi o to, że obiekt musi być użyteczny PRZED wywołaniem metody init. Może w inny sposób niż po jego wywołaniu. Zobacz wzór stanu. W sensie częściowej budowy obiektu jest to zapach kodowy.
oopexpert
-4

Nazwa init może czasami być nieprzejrzysta. Weźmy samochód i silnik. Aby uruchomić samochód (tylko włączyć zasilanie, aby słuchać radia), chcesz sprawdzić, czy wszystkie systemy są gotowe do pracy.

Więc budujesz silnik, drzwi, koło itp. Twój ekran pokazuje silnik = wyłączony.

Nie ma potrzeby rozpoczynania monitorowania silnika itp., Ponieważ wszystkie są drogie. Następnie, gdy przekręcisz kluczyk w celu zapłonu, wołasz silnik-> start. Zaczyna działać wszystkie drogie procesy.

Teraz widzisz silnik = włączony. I rozpoczyna się proces zapłonu.

Samochód nie uruchomi się bez dostępnego silnika.

Możesz zastąpić silnik złożonymi obliczeniami. Jak komórka Excela. Nie wszystkie komórki muszą być aktywne przez cały czas ze wszystkimi modułami obsługi zdarzeń. Kiedy skupisz się na komórce, możesz ją uruchomić. W ten sposób zwiększa się wydajność.

Luc Franken
źródło