2010-03-08 20 views
27

Powiel możliwe:
How much work should be done in a constructor?Czy konstruktor C++ powinien naprawdę działać?

mam strugging z jakąś poradę mam w głębi mojego umysłu, ale dla których nie mogę zapamiętać rozumowania.

Pamiętam, że w pewnym momencie przeczytałem kilka porad (nie pamiętam źródła), że konstruktory C++ nie powinny wykonywać prawdziwej pracy. Zamiast tego powinny one inicjować tylko zmienne. Porada wyjaśniła, że ​​prawdziwa praca powinna być wykonywana w jakiejś formie init(), którą należy wywołać osobno po utworzeniu instancji.

Sytuacja polega na tym, że mam klasę reprezentującą urządzenie sprzętowe. Logiczne jest dla mnie, aby konstruktor wywoływał procedury, które wysyłają zapytania do urządzenia, aby zbudować zmienne instancji opisujące urządzenie. Innymi słowy, po utworzeniu nowego obiektu, programista otrzymuje obiekt, który jest gotowy do użycia, nie wymaga osobnego wywołania object-> init().

Czy istnieje dobry powód, dla którego konstruktorzy nie powinni wykonywać prawdziwej pracy? Oczywiście może to spowolnić czas przydziału, ale nie byłoby inaczej, gdyby wywołanie osobnej metody nastąpiło natychmiast po przydzieleniu.

Po prostu próbuję rozgryźć, co nie sądzisz, co mogłoby doprowadzić do takiej porady.

+3

Możliwe do scalenia. – dmckee

Odpowiedz

26

Pamiętam, że Scott Meyers w bardziej efektywnym C++ zaleca, aby nie mieć zbędnego domyślnego konstruktora. W tym artykule dotknął również metodami podobnymi do Init(), aby "tworzyć" obiekty. Zasadniczo wprowadziłeś dodatkowy krok, który nakłada odpowiedzialność na klienta klasy.Ponadto, jeśli chcesz utworzyć tablicę tych obiektów, każdy z nich musiałby ręcznie wywołać Init(). Możesz mieć funkcję Init, którą konstruktor może wywołać wewnątrz, aby zachować porządek w kodzie, lub aby obiekt mógł zadzwonić, jeśli zaimplementujesz Reset(), ale z doświadczenia lepiej jest usunąć obiekt i odtworzyć go zamiast próbować resetować jego wartości domyślne, chyba że obiekty są tworzone i niszczone wielokrotnie w czasie rzeczywistym (np. efekty cząsteczkowe).

Należy również pamiętać, że konstruktorzy mogą wykonywać listy inicjalizacji, których normalne funkcje nie mogą.

Jednym z powodów, dla których można ostrzec przed użyciem konstruktorów do ciężkiej alokacji zasobów, jest fakt, że trudno jest wychwycić wyjątki w konstruktorach. Istnieją jednak różne sposoby. Inaczej myślę, że konstruktorzy mają robić to, co powinni - przygotować obiekt do jego początkowego stanu wykonania (ważne dla tworzenia obiektu jest alokacja zasobów).

+24

To byłby Scott Meyers - Sid Meier jest facetem Cywilizacji :) –

+0

Ups. Dokonałem edycji. – Extrakun

9

Zależy od tego, co rozumiesz przez prawdziwą pracę. Konstruktor powinien umieścić obiekt w stanie nadającym się do użytku, nawet jeśli ten stan jest flagą, co oznacza, że ​​nie został jeszcze zainicjowany :-)

Jedynym powodem, z jakim kiedykolwiek spotkałem się, aby nie wykonywać prawdziwej pracy, byłaby fakt, że jedynym sposobem, w jaki konstruktor może zawieść, jest wyjątek (a destruktor nie zostanie wywołany w tym przypadku). Nie ma możliwości zwrotu ładnego kodu błędu.

Pytanie trzeba zadać sobie pytanie:

Czy przedmiot użyteczny bez wywoływania metody init?

Jeśli odpowiedź brzmi "Nie", wykonywałbym całą tę pracę w konstruktorze. W przeciwnym razie będziesz musiał złapać sytuację, gdy użytkownik uruchomi instancję, ale jeszcze jej nie zainicjował i zwróci błąd.

Oczywiście, jeśli można ponownego zainicjowania urządzenie, należy podać jakąś init metody, ale w tym przypadku, chciałbym nadal wywołanie tej metody z konstruktora, jeśli warunek powyższy jest spełniony.

18

Jednym z powodów, dla których nie należy wykonywać "pracy" w konstruktorze, jest to, że jeśli zostanie zgłoszony wyjątek, destruktor klasy nie zostanie wywołany. Ale jeśli korzystasz z zasad RAII i nie polegasz na swoim destruktorze, aby wykonać prace porządkowe, to uważam, że lepiej nie wprowadzać metody, która nie jest wymagana.

+8

Dodałbym, że nawet jeśli konstruktor wyrzuci każdą w pełni zbudowaną zmienną składową, zostanie wywołany jej derstruktor (a więc jeśli jesteś w bloku kodu konstruktora, wszyscy członkowie zostaną poprawnie zniszczeni). –

+0

Jak zrobić porządek w konstruktorze po wygenerowaniu wyjątku? (Powiedzmy, że przydzieliłeś na przykład 'FILE *') ** nie polegaj na swoim destruktorze **, ale wszystko co możesz zrobić, to pozwolić stosowi oderwać się ... –

+0

Jednym z członków twojej klasy byłby obiekt typu RAII która zamknie plik * w jego destruktorze. Coś jak klasa FileCloser z tego: http://tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/ – Warpin

3

Jedynym prawdziwym powodem jest Testability. Jeśli twoi konstruktorzy są pełni "prawdziwej pracy", oznacza to zazwyczaj, że obiekty mogą być tworzone tylko w całkowicie zainicjowanej, uruchomionej aplikacji. To znak, że obiekt/klasa potrzebuje dalszego rozkładu.

+0

Argument testowalności jest dobry. Ale prawdopodobnie może być obsługiwany przez fałszywe obiekty w większości przypadków. – neuro

+1

Jeśli konstruktor tworzy własne obiekty, nie można skonfigurować obiektu z próbnymi obiektami, chyba że napiszesz nowy konstruktor. Oddzielenie odpowiedzialności za tworzenie złożonych obiektów od ustawiania zmiennych składowych zapewnia wymagany szew. – mabraham

6

Oprócz innych sugestii dotyczących obsługi wyjątków, jednym z elementów, które należy wziąć pod uwagę podczas łączenia z urządzeniem sprzętowym, jest sposób, w jaki klasa będzie obsługiwać sytuację, w której nie ma urządzenia lub komunikacja nie powiedzie się.

W sytuacji, w której nie można nawiązać komunikacji z urządzeniem, może być konieczne podanie pewnych metod w klasie, aby mimo to przeprowadzić późniejszą inicjalizację. W takim przypadku może być bardziej sensowne tworzenie instancji obiektu, a następnie uruchamianie wywołania inicjalizacyjnego. Jeśli inicjalizacja się nie powiedzie, możesz po prostu zatrzymać obiekt i spróbować ponownie zainicjować komunikację w późniejszym czasie. Lub może zajść potrzeba rozwiązania sytuacji, w której komunikacja jest tracona po inicjalizacji. W obu przypadkach prawdopodobnie będziesz chciał pomyśleć o tym, jak zaprojektujesz klasę, aby poradzić sobie z ogólnymi problemami z komunikacją i która może ci pomóc w podjęciu decyzji, co chcesz zrobić w konstruktorze, a co w metodzie inicjowania.

Po zaimplementowaniu klas, które komunikują się ze sprzętem zewnętrznym, łatwiej jest utworzyć instancję "odłączonego" obiektu i udostępnić metody łączenia i konfigurowania statusu początkowego. Zasadniczo zapewnia to większą elastyczność łączenia/rozłączania/ponownego łączenia z urządzeniem.

+0

Tak! Większość ludzi nie wydaje się tego rozumieć - umieszczenie pracy w konstruktorze nieodwracalnie wiąże sukces z czasem istnienia obiektu, co nie działa dobrze w przypadku obiektów rozłącznych (takich jak "stały" klient TCP). – Tom

+0

Zgadzam się. Używam interfejsu/paradygmatu open/close/isOpen do oznaczania/obsługi tego rodzaju obiektów. – neuro

3

Podczas korzystania z konstruktora i metody Init() masz źródło błędu. W moim doświadczeniu napotkasz sytuację, w której ktoś zapomniałby to nazwać i możesz mieć subtelny błąd w twoich rękach. Powiedziałbym, że nie powinieneś dużo pracować w swoim konstruktorze, ale jeśli potrzebna jest jakakolwiek metoda init, to masz nietrywialny scenariusz budowy i nadszedł czas, aby spojrzeć na schematy kreacji. Funkcja budowniczego lub fabryka powinny być mądre, żeby się przyjrzeć. Z prywatnym konstruktorem upewniając się, że nikt oprócz funkcji fabryki lub budowniczego faktycznie nie tworzy obiektów, więc możesz mieć pewność, że jest on zawsze poprawnie skonstruowany.

Jeśli Twój projekt pozwoli na błędy we wdrażaniu, ktoś zrobi te błędy. Mój przyjaciel Murphy powiedział mi;)

W mojej dziedzinie pracujemy z wieloma podobnymi sytuacjami związanymi ze sprzętem. Fabryki zapewniają nam zarówno testowalność, bezpieczeństwo i lepsze sposoby na niepowodzenie w budowie.

2

Warto zastanowić się nad kwestiami dotyczącymi okresu użytkowania i łączenia/ponownego łączenia, jak zauważa Neal S.

Jeśli nie uda się połączyć z urządzeniem na drugim końcu łącza, często zdarza się, że "urządzenie" na twoim końcu jest użyteczne i nastąpi później, jeśli drugi koniec zacznie działać razem. Przykładami są połączenia sieciowe itp.

Z drugiej strony, jeśli spróbujesz uzyskać dostęp do lokalnego urządzenia sprzętowego, które nie istnieje i nigdy nie będzie istnieć w zasięgu twojego programu (na przykład karty graficznej, która nie jest obecna), to myślę, że jest to przypadek, w którym chcesz to wiedzieć w konstruktorze, aby konstruktor mógł rzutować, a obiekt nie może istnieć. Jeśli nie, możesz skończyć z obiektem, który jest nieważny i zawsze tak będzie. Rzucanie konstruktora oznacza, że ​​obiekt nie istnieje, a zatem funkcje nie mogą zostać wywołane na tym obiekcie. Oczywiście musisz zdawać sobie sprawę z problemów z oczyszczaniem, jeśli rzucisz konstruktor, ale jeśli nie w takich przypadkach, zwykle kończy się sprawdzaniem poprawności we wszystkich funkcjach, które można wywołać.

Uważam, że powinieneś zrobić tyle w konstruktorze, aby mieć pewność, że utworzono prawidłowy, użyteczny obiekt.

1

Chciałbym dodać do tego moje własne doświadczenia.

Nie powiem dużo o tradycyjnej debacie Constructor/Init ... na przykład Google Wytyczne odradzają cokolwiek w Konstruktorze, ale to dlatego, że odradzają Wyjątki i 2 działają razem.

Mogę mówić o klasie Connection, której używam.

Po utworzeniu klasy Connection, nastąpi próba połączenia się (przynajmniej jeśli nie jest skonstruowana domyślnie). Jeśli Connection zawiedzie ... obiekt jest nadal skonstruowany i nie wiesz o nim.

Podczas próby użycia klasy Connection jesteś więc w jednym z 3 przypadków:

  • żaden parametr kiedykolwiek zostały doprecyzowane> wyjątek lub błąd kodu
  • obiekt jest właściwie podłączony> drobnego
  • obiekt nie jest podłączony, to spróbuje połączyć> to się powiedzie, w porządku, to się nie powiedzie, pojawi się wyjątek lub kod błędu

myślę, że to całkiem przydatna mieć obydwa. Oznacza to jednak, że w każdej metodzie faktycznie korzystającej z połączenia należy sprawdzić, czy działa.

Warto jednak, ponieważ wystąpiły zdarzenia rozłączenia. Kiedy jesteś połączony, możesz utracić połączenie bez wiedzy o nim obiektu. Poprzez hermetyzację metody sprawdzania połączenia w metodzie reconnect, która jest wewnętrznie wywoływana przez wszystkie metody wymagające działającego połączenia, naprawdę izolujesz programistów od rozwiązywania problemów ... lub przynajmniej tyle, ile możesz, ponieważ gdy wszystko zawiedzie, masz nie ma innego rozwiązania, które pozwala im się dowiedzieć :)

0

Najlepiej unikać "prawdziwej pracy" w konstruktorze.

Jeśli skonfiguruję połączenia z bazami danych, otworzę pliki itp. Wewnątrz konstruktora i jeśli jeden z nich spowoduje zgłoszenie wyjątku, spowoduje to wyciek pamięci. Spowoduje to naruszenie Twojej aplikacji pod numerem exception safety.

Innym powodem, aby unikać pracy w konstruktorze jest to, że sprawi, że twoja aplikacja będzie mniej testowalna. Załóżmy, że piszesz procesor płatności kartą kredytową. Jeśli mówisz w konstruktorze klasy CreditCardProcessor, czy wykonujesz całą pracę związaną z łączeniem się z bramką płatności, uwierzytelniasz i rozliczasz kartę kredytową w jaki sposób mogę kiedykolwiek pisać testy jednostkowe dla klasy CreditCardProcessor?

Przechodząc do scenariusza, jeśli procedury odwołujące się do urządzenia nie wywołują żadnych wyjątków i nie zamierzasz testować klasy w izolacji, prawdopodobnie preferowane jest wykonywanie pracy w konstruktorze i unikanie połączeń z tym dodatkiem. Metoda init.

0

Istnieje kilka powodów, chciałbym użyć oddzielnego konstruktora/init():

  • Lazy/opóźnionego inicjalizacji. Umożliwia to szybkie utworzenie obiektu, szybką reakcję użytkownika i opóźnienie dłuższej inicjalizacji w celu późniejszego przetworzenia lub przetworzenia w tle. Jest to również część jednego lub więcej wzorców dotyczących pul obiektów wielokrotnego użytku, aby uniknąć kosztownej alokacji.
  • Nie wiem, czy ma to poprawną nazwę, ale być może podczas tworzenia obiektu informacje o inicjalizacji są niedostępne lub niezrozumiałe dla tego, kto tworzy obiekt (na przykład ogólne tworzenie obiektów ogólnych). Inna sekcja kodu zawiera know-how, aby go zainicjować, ale nie można go utworzyć.
  • Jako osobisty powód destruktor powinien móc cofnąć wszystko, co zrobił konstruktor. Jeśli to wymaga użycia wewnętrznego init/deinit(), nie ma problemu, o ile są one swoimi lustrzanymi odbiciami.
+0

Cele te są bardziej bezpiecznie osiągane poprzez stworzenie obiektu typu, którego zadaniem jest późniejsze stworzenie prawdziwego typu. Ważne jest to, że kiedy masz obiekt prawdziwego typu, wiesz, że jest w stanie bezpiecznym do użycia. Zamiast "skonstruować Thinga, wywołaj jego metodę init(), musisz zawsze sprawdzić, czy wywołałeś init()" musisz "skonstruować ThingBuilder, wywołaj metodę createThing(), a następnie po prostu użyj swojej rzeczy" – mabraham

Powiązane problemy