Hej, dziś temat mający korzenie w korytarzach hotelu gdzie odbywało się 33degree.

Podczas jednej z przerw rozpoczeliśmy dyskusję nad odwiecznym problemem w javie: ile metod i kiedy powinno się nadpisywać z klasy Object. By zacieśnić grono podejrzanych metod: rozmawialiśmy o hashCode, equals, toString. Całość w kontekście naszych aplikacji wykorzystujących Hibernate'a. Cała sprawa dotyczy też jedynie obiektów VO (Value Object), entities, DTO (Data Transfer Object) a nie services, controllers i innych DAO i managerów.

Jako wstęp do wpisu zachęcam do przeczytania kilku zasad tworzenia dobrych metod equals i hashCode, zebranych przez Andrzeja Ludwikowskiego w wpisach na jego blogu: equals i hashcode. Jeśli ktoś nie czytał, a pisze kod w javie, to obowiązkowo zalecam też lekturę książki Joshua Blocha Effective Java. Joshua dokładnie wyjaśnia wady i zalety oraz jak nadpisywać wszystkie 3 wymienione metody.

Główną linią podziału pomiędzy nami było czy w equals i hashCode korzystać z ID generowanego przez hiberneta czy nie oraz czy nadpisywać metodę toString. Moim zdaniem korzystanie z id hibernatowego ma zalety do których należy

  • Pewność podczas identyfikacji obiektu. ID jest niepowtarzalne. Jeśli dwa obiekty są róne po porównaniu przez ID to na pewno jest to ten sam obiekt. Uwaga: owy obiekt może być w innym stanie, ale to historia na inny wpis.
  • Kolejną zaletą jest zwalenie odpowiedzialności za utworzenie identyfikatora na kogoś innego (czyt. Hibernate-a).
  • Przeważnie nikt też nie wpadnie na genialny pomysł zmiany wartości, którą postanowiliście zastosować do porównywania równości i generowania hashu (magia dwóch liter ID ;))

Są jednak też wady, a podstawową jest to, że ID potrzebne do obu metod będzie dostępne dopiero po zapisie obiektu do bazy danych. Można to obejść, ale przeważnie w mało ładny sposób, który może nas później ugryźć w najmniej odpowiednim momencie, pomińmy więc tą możliwość.

Dla wielu osób nie musi wyglądać to na poważny problem, ale niestety nim jest. Zastanówmy się teraz czy istnieje możliwość, że korzystamy z obiektów przed dodaniem ich do bazy?

Jeśli tak to trzeba się zabezpieczyć przed nullem w  id. Jak to zrobić?

  1. Możemy sprawdzać czy id == null i jeśli tak to zwracamy jakąś stałą, np String.EMPTY, zero, etc. Nie wydaje się to być złe, nigdy nie dostaniemy NPE. Jednak problem pojawia się gdy korzystamy z kolekcji, szczególnie haszujących. Z powodu tego, że 2 obiekty będą dla systemu identyczne, możliwe jest doprowadzenie do utraty danych. Problemem jest również korzystanie z obiektu po zapisaniu go do DB. Zmienia się ID (na wygenerowane przez hiberneta) więc próba skorzystania z metoda takich jak .contains(obj) zakończy się niepowodzeniem. Możemy na przykład wyświetlić tą samą informację na ekranie dwukrotnie.
  2. Możemy przed użyciem obiektu zapisywać go do bazy danych/pobierać przed skorzystaniem  z niego numer ID. Rozwiązanie pozbawione powyższych wad, jednak w wielu systemach zupełnie nie akceptowalne. Podstawowa wada to utrata kolejnego numeru ID oraz czasu potrzebnego na komunikację z bazą. Jeśli wyświetlamy numer ID użytkownikowi, może go zdziwić, że po zamówieniu nr  6, kolejne zamówienie ma numer 30. Inną sprawą jest, że ID bazodanowe nie powinno być prezentowane użytkownikowi, ale chyba każdy z nas miał gdzieś okazję widzieć system, który łamał tę zasadę.  Komunikacja z bazą to za to zupełnie inny problem. Jeśli mamy jakąś formę wizarda to wypadało by zadbać by obiekty stworzone przez użytkowników, a nigdy nie dokończone, zostały kiedyś usunięte z bazy. I kolejny problem wtedy mamy

Wracając do sedna i tego co Pan Michał radzi: da się uniknąć wykorzystywania ID w hashcode i equals więc starajmy się tego unikać. Pisząc encje zidentyfikujmy niezmienne własności obiektu i wykorzystajmy je w equals i hascode. Dobrymi kandydatami są data powstania obiektu czy wymagane pola, których później się nie da zmienić np login. Wykorzystując kombinację takich danych da się dobrze identyfikować obiekty i nie trzeba przy każdym obiekcie zastanawiać się nad strategią zapisywania do bazy.

Czasem można wykorzystać też dane zmienne w czasie. Jeden z dyskutantów podał przykład z aplikacji nad którą aktualnie pracował. System służy do przeprowadzania ankiet. Według autora, nie ma problemu w wcześniejszym zapisaniu danych w bazie, przed rozpoczęciem korzystania z nich. Nie zdążyliśmy dokończyć dyskusji na ten temat, ale oto moja propozycją dla tego typu zagadnień.

Jeśli posiadacie ograniczony zbiór możliwych obiektów to może nie warto zapisywać każdego z nich, tylko agregować je. Łatwo to uzyskać poprzez porównywanie wszystkich pól w obiekcie.

Weźmy za przykład ankietę z dwoma pytaniami i trzema możliwymi odpowiedziami na każde z nich. Dla takiej ankiety istnieje zbiór 9 możliwych do utworzenia obiektów. Jeśli wypełni ankietę 1000 użytkowników to my nie będziemy gromadzić 1000 rekordów a jedynie 9. Spora oszczędność.

Co zrobić jeśli mamy ankiety z pytaniami otwartymi i zamkniętymi? Nikt nie powiedział, że ankieta (a w zasadzie odpowiedzi do ankiety) musi być obiektem monolitycznym. Obiekt odpowiedzi możemy podzielić na podklasy: odpowiedzi do części otwartej i zamkniętej. Część zamknięta jest agregowana w wyżej opisane sposób, otwarta tradycyjnie. Sama tabela odpowiedzi staje się jedynie łącznikiem pomiędzy użytkownikiem, odpowiedziami do każdej części oraz samym zestawem pytań.

Na koniec  słowo o metodzie toString(). Większość dyskutantów stwierdziła, że jej nie nadpisuje. Jako powód wymienili przykład z Effective Java, gdize Joshua podaje jako zagrożenie, możliwość rozpoczęcia parsowania przez użytkowników, wartości zwracanych z toStringa. Dla mnie to przykład jedynie książkowy, być może z racji tego jak projektuję aplikację.

Nie uważam, by ukrywanie stanu w ValueObjects (do których zaliczam encje), było wskazane, gdyż może prowadzić jedynie do takich absurdów jak parsowanie toString czy nadużywania refleksji. Ok, kwestia tego jak piszemy aplikacje, jednak jeśli nie nadpisujecie toString, z wyżej wymienionego powodu, to nie wystawiajcie innej metody, która działa jak toString a jedynie inaczej się nazywa (zaproponowane podczas tej dyskusji).

Nie unikniecie problemów związanych z parsowaniem stringów a jeydnie skomplikujecie sobie życie. Jeśli już więc unikamy toString, to wystawmy, przez odpowiedni interface,  zestaw metod do komunikacji, np myNoToStringObject.log.(LoggerAdapter, logLevel, logTemplate). Nie unikniemy wprawdzie tego, że ktoś przekaże nam własną implementację loggerAdaptera i i tak sparsuje Stringa, ale może choć utrudnimy mu życie na tyle, że zrezygnuje ;) Temat jest jednak dość obszerny by stworzyć kolejny wpis.

Tymczasem w komentarzach wpisujcie swoje przemyślenia i dajcie znać ile wy metod zawsze nadpisujecie w obiektach.