Czy programiści .NET zapomnieli o konstruktorach?

Dzisiaj ABC, ale chyba warte przypomnienia. Wprowadzony w .NET 3.0 Initializer syntax jest bardzo wygodny. Pozwala skrócić zapis i spowodować, że kod jest trochę czytelniejszy. Jednak coraz częściej mam wrażenie, że przez niego programiści .NET zapomnieli o konstruktorach.

Initializer syntax posiada dwie postaci – collection initializer i object initializer. Pierwsza pozwala na bardziej zwięzłe budowanie kolekcji obiektów. Druga pozwala na wygodniejsze inicjalizowanie wielu właściwości jednego obiektu.

Pierwsza postać pozwala zamienić taki kod:

image

na taki

image

Wg mnie bardziej zwięźle i wygodniej. Wyeliminowane zostało mnóstwo "hałasu". Kod jest bardziej deklaratywny i czytelny.

Zapis ten jest wspierany przez każdy obiekt, który implementuje interfejs IEnumerable i udostępnia metodę Add() przyjmującą dowolną liczbę parametrów (Specyfikacja języka C#). Dlatego działa ze wszystkimi kolekcjami, w tym z tymi utworzonymi przez nas. Dla mnie ma same zalety i stosuję go zawsze.

Druga postać pozwala na zainicjalizowanie wielu właściwości jednego obiektu. I zamiast takiego kodu:

image

pozwala napisać coś takiego

image

Tutaj zalety są te same co w przypadku inicjalizacji kolekcji. Eliminacja powtarzającego się kodu, "hałasu". Dodatkowo object initializer syntax pozwala nie pisać konstruktora. Im mniej kodu tym lepiej prawda? Jednak w tym przykładzie jest pewien problem. Czy jest coś co powstrzymuje nas przed utworzeniem obiektu, który nie ma ustawionego imienia i nazwiska? Co jeżeli ktoś zapomni ustawić te dane? Czy reszta kodu musi być odporna na nieodpowiednio zainicjalizowane obiekty klasy Osoba? Czy będzie się wywalać?

Ten sposób inicjalizacji obiektów nie powstał po to, aby nie trzeba było pisać konstruktorów. A często jest tak stosowany. Zapis taki stał się bardzo popularny. Właściwie wydaje się, że domyślny. Ale bardzo często jest to błędem.

Zastanówmy się jak to powinno wyglądać. Gdybyśmy pisali oprogramowanie dla kostnicy być może taki zapis jak wyżej byłby zupełnie poprawny i nie musiałby sugerować błędu w projekcie klasy. W takim oprogramowaniu konstruktor klasy Osoba mógłby wyglądać następująco.

image

Ale w większości wypadków nie będzie miało sensu tworzenie obiektu Osoba bez podania jego danych podstawowych, dlatego klasa powinna mieć konstruktor przyjmujący parametry wymagane.

image

Dane takie jak drugie imię (niektórzy nie mają), data urodzenia (kobiet o wiek się nie pyta), czy miejsce urodzenia nie zawsze będą podane i nie mogą być wymagane. I właśnie do ustawienia takich właściwości fajnie sprawdza się object initializer syntax.

image

Teraz patrząc na kod wyraźnie widać co jest wymagane, a co opcjonalne. Czytając pierwsze dwa listingi dotyczące klasy Osoba możemy dojść do wniosku, że nic nie jest wymagane i utworzyć nie wypełniony obiekt, co objawi się błędem N linii / dni / miesięcy później. A moglibyśmy tego uniknąć w prosty sposób – pisząc konstruktor – samodokumentujący się kod. Zapewniając, że konstruktor nie pozwoli utworzyć niepoprawnego obiektu reszta kodu może spokojnie pracować na obiektach klasy Osoba będąc pewnym, że obiekty są poprawnie zainicjalizowane.

Inny przykład, pytanie na StackOverflow. Webowy system rezerwacji terminów posiada integrację z 28 innymi systemami. Do każdego z nich przekazywany jest parametr mówiący o początku i końcu rezerwowanego terminu. W każdej z 28 implementacji powtórzona jest ta sama walidacja – Czy DataOd < DataDo? A przecież można tego uniknąć. Wystarczy w obiekcie (np. Termin) dodać konstruktor, który nie pozwoli utworzyć obiektu z DataDo < DataOd. Niech kodu tworzący ten obiekt się martwi. Niech UI zapewni, że nie da się czegoś takiego wprowadzić. 28 implementacji nie powinno musieć się o to martwić, a powinno polegać na tym, że obiekty są utworzone w poprawnym stanie!

Przypomnijmy sobie o konstruktorach.

8 uwag do wpisu “Czy programiści .NET zapomnieli o konstruktorach?

  1. Osobiście wykorzystuje takie rozwiązanie głównie przy inicjalizacji znanych kolekcji, a także w klasach, które trzymają „suche” dane – np. klasy rezultatu dla API.

    Ale fakt jest taki, że rzeczywiście niektórzy nadużywają tego mechanizmu zbyt mocno.

    Polubienie

  2. Object initializer ma jeden minus – w przypadku gdy poleci np. NullReferenceException, to linia w stack trace będzie wskazywała na początek konstruktora, a nie na faktyczną linię, która spowodowała błąd. Jeżeli mam obiekt w formie kontenera, do którego wpisuje dane z serwisów, repozytoriów itp. to wole zastosować przypisywanie kolejno po właściwościach.

    Polubienie

  3. Wszystko ładnie pięknie i zgadzam się, że konstruktory z wymaganymi parametrami czynią nasz odporniejszy na błędy, ale co w przypadku deserializacji? Musimy mieć bezparametryczny konstruktor i cała nasza układanka się rozsypuje…

    Polubienie

    • To jest wyjątek. Zazwyczaj dotyczy klas z przestrzeni modeli (mówiąc o aplikacji ASP.NET MVC). A to nie jest problem. Te modele są konwertowane z/na modele naszej domeny. I to te model mają bezparametrowe konstruktory.
      Mogę się również mylić, ale wydaje mi się, że narzędzia do deserializacji potrafią wykorzystać konstruktor prywatny i protected. W takim przypadku znowu wszystko jest ok. Akurat dodawałem dzisiaj takie konstruktory. Jest to jedna z niewielu sytuacji, gdy uważam za stosowne dodanie komentarza w kodzie. // for JSON deserialization 🙂

      Polubienie

Dodaj komentarz