Home > English > Ile metod ZAWSZE nadpisujesz w swoich obiektach?

Ile metod ZAWSZE nadpisujesz w swoich obiektach?

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.

Categories: English
  1. 06/06/2011 at 11:31

    A mi się wydaje dość dobrym rozwiązaniem na equals:
    jeżeli id nie-nullowe to je porównaj, a jeśli nullowe to porównaj referencje.

    W tym przypadku mogę operować np hashListami spokojnie, bez dublowania tworzonych (niezapisanych jeszcze w bazie) obiektów, a jednocześnie korzystać z wygody porównywania id kiedy już je mam.

    Nie rozumiem tylko jak widzisz rozwiązanie kawałka z ankietami (jak się to ma do equals/hashCode), Hibernate nie wczyta wszystkiego z bazy i nie puści equals, żeby wiedzieć którego rekordu użyć chyba. W tym przypadku (poza opcją enumów) chyba by było miejsce dla złożonych z wielu pól Id hibernate’owych?

    • 06/06/2011 at 12:03

      Id może być równe hashCodowi, a hashcode wylicza się na podstawie wszystkich pól a więc samych hashcodów mamy ograniczony zbiór.
      Jedyne co trzeba zapewnić to unikalność hashCode, ale to już kwestia budowy obiektu i tego z jakich obiektów będziemy go składać, generalnie nie powinno być to problemem.

      Nie sprawdzałem czy hibernate to łyknie, ale nie spodziewałbym się z nim większych problemów (choć zawsze mnie zaskakuje czymś więc mogę się mylić)

      A co do Twojego rozwiązania to też ciekawe. Muszę jeszcze przemyśleć czy nie widzę żadnych luk, ale wydaje się być ok.

  2. 06/06/2011 at 12:27

    Mi się rozwiązanie Wojtka nie podoba.

    MyObject obj1 = new …
    MyObject obj2 = new …
    List list = …
    list.add(obj1);
    list.add(obj2);

    Możemy sobie utworzyć 2 indentyczne obiekty (w sensie wartości pól skladowych), z null’owym ID. Dodajemy je do listy i otrzymujemy w liście 2 te same (pod względem zawartości) obiekty, a tego raczej nie chcemy.
    Ponadto jak byś do tego napisał hashCode()?

    Ja tam jestem zwolennikiem napisania equals i hashCode, ktore porownuja pola jednoznacznie identyfikujace obiekt (wszystkie final, lub wynikające z danej domeny).

    Co do toString() to jest ona dla nas, programistow, dla celów debugowania, loggowania, wypisywania na konsoli. O pomyśle parsowania toString() w celu budowy obiektu pierwsze słysze, ale brawo za inwencję ;)

    • 06/06/2011 at 13:42

      Tak naprawdę to zrozumiałem pomysł Wojtka jako posiadanie dwóch equals i hashCode. Z listą nie będzie problemu, gorzej, że Hash kolekcja może się wykaszanić, ale to do przemyślenia, po pracy ;)

      toStringa parsowanie opisał Joshua Bloch w Effective Java i ten sam powód usłyszałem na 33. Dla mnie to dziwne, bo w celu logowania i debugowania nie wyobrażam sobie bym miał kombinować na najdziwniejsze sposoby. Może gdybym pisał biblioteki, które dalej byśmy sprzedawali. No nic, ja żyję w krainie szczęśliwości gdzie należy nadpisywać toString()

  3. 06/06/2011 at 14:14

    Marcin ale zastanów się co ten kod miałby robić (który wkłada dwa identyczne obiekty i chciałby, żeby się nadpisały).

    Też o tym myślałem, ale w codziennych sytuacjach nie chcę aby się nadpisywały te obiekty, bo to dla mnie rodzi więcej problemów. Stąd zacząłem stosować tę metodę.

    Przykładowo request 1:

    Podaj ile masz dzieci, gość podaje x.
    (1..x).each {
    ojciec.addChild(new Child())
    }

    Twoim tokiem, gość właśnie miałby jedno dziecko, niezależnie od x.

  4. 06/06/2011 at 19:44

    Dobra praktyka czy nie, tożsamość encji i inne teoretyczne rozważania sobie, ale opieranie hashCode na ID jest po prostu niebezpieczne i prowadzi do nieprzyjemnych i trudnych do wyłapania bugów.

    Z oczywistych względów może ugryźć przy zapisywaniu kolekcji. Hibernate przypisuje ID, zmienia się hashCode, i łamiemy kontrakt na wszelkie HashSety itd.

    Inne problemy potrafią wyskoczyć przy odczycie. Zdarzyło mi się mieć kolekcje one-to-many już istniejące w bazie i zapisane dawno temu, gdzie w jednej sesji po odczycie ten sam rodzic miał inną ilość dzieci w zbiorze niż w drugiej (link poniżej). Nie dłubałem w implementacji, nie wiem czy to cache (1 czy 2 poziom), leniwa inicjalizacja, czy coś innego, ale tak czy inaczej radziłbym tego unikać.

    http://stackoverflow.com/questions/5199978/hibernate-partial-lazy-initialization

  5. 06/06/2011 at 22:39

    Wojtku, ja bym nie nazwał dzieci tak samo i odróżniał bym je po imieniu (ewentualnie dodatkowo nazwisko, data urodzenia, PESEL, lub odcisk palca :P – zależnie od wymagań).

    Konrad przedstawił kolejny przykład na NIE twojej metodzie: Hibernate (lub cokolwiek innego) nadaje polu ID wartość i HashSet’y się zmieniają. Nie będziemy mogli np. usunąć obiektu z HashMap’y, bo będzie przeszukiwało nie ten kubełek.

    Equals i hashCode mają nam pomagać, a nie utrudniać życie. Jak dla mnie twoje podejście może to prowadzić do trudnych do wykrycia błędów i godzin debugowania.

  6. 06/06/2011 at 23:15

    Ok, dzieci można ponazywać w kolejnych żądaniach, w innych transakcjach, a teraz chciałbyś wypełnić kolekcję.

    Czyli uważacie, że ID w ogóle nie powinno stanowić części hashCode i equals ze względu na trudności z Hibernate? W takim razie jak rozwiążecie powyższy problem, jeśli w danej transakcji po prostu nie macie dodatkowych danych do wypełnienia elementów kolekcji (na USG już widać trojaczki, ale jeszcze nie widać płci i nie wymyśliliśmy imion, jeśli tak będzie sobie łatwiej wyobrazić ;-) ).

    Ciekaw jestem, jak tak podstawowe zagadnienie nie doczekało się jeszcze jednego jasnego rozwiązania w dokumentacji Hibernate (a może się mylę?) tylko garści ostrzeżeń przed złymi praktykami?

    • 06/06/2011 at 23:32

      Doczekało się: http://community.jboss.org/wiki/EqualsAndHashCode
      Jednak kwestia dalej pozostaje dyskusyjna. Łatwo polegać jest na ID, które zostaje nam dane, nie musimy kombinować jakie stałe przyjąć do wyliczania hashCode i equals.
      Widziałem dość projektów gdzie to działało by wiedzieć, że można z tym żyć.

      Jednak pociąga to za sobą cenę, o której cała ta dyskusja.

      Co do pól to dla mnie jasnym pktem zawsze jest data stworzenia rekordu. Jest to śledzone przy większości encji, dość unikalne i stąd jest dobrym pkt wyjścia.
      Dla owego płodu była by to data pierwszego badania i nazwisko + imiona rodziców.
      Te dane się nie zmienią a sprawią, że mam unikalny obiekt.

      Pomińmy scenariusz gdy w tym samym szpitalu 2 pary o tym samym imieniu i nazwisku w tej samej milisekundzie rozpoczęły badanie ;)

  7. 06/06/2011 at 23:38

    Jedyny sens jaki widzę do używania ID w equals i hashCode, to gdy nigdy nie tworzysz obiektów tylko zawsze je dostajesz z bazy danych (z wypełnionym już polem ID). Wówczas zamiast CRUD’a masz tylko RUD’a ;)

    Jak dla mnie pola typu ID są sztucznymi polami narzuconymi nam przez relacyjne bazy danych. Nie są one tym co pozwala odróżniać od siebie dwa obiekty, a jedynie polem “technicznym”. W obiektowych bazach danych nie ma pól w stylu ID, ale za to jest tożsamość obiektu (nadawana po zapisaniu obiektu w bazie i stała, aż do usunięcia obiektu). Ale to już całkiem osobny i długi temat.

    Co do trojaczków na USG, to albo bym wprowadził obiekt pośredni (który niekoniecznie będę zapisywał w bazie danych) typu Płód, lub zastosował bym Buildera, w którym zgromadzę potrzebne informacje (może być w różnych transakcjach) i jak już będę miał wszystko, to zbuduję potrzebne instancje dzieci.

    Michał jeszcze twierdzi, że można z tymi ID w equals i hashCode żyć, ale z pewnością niesie to za sobą wysoką cenę.

    • 06/06/2011 at 23:46

      To też nie jest złe rozwiązanie by zawsze mieć ID, było chyba o tym w tekście. Jest jednak problem: trzeba by kombinować jak zapewnić, że obiekt *na pewno* ma ID oraz jaki jest koszt tego.

      Z tym się da żyć i zespół tworzący to będzie umiał się po swoim kodzie poruszać, o ile będzie świadom zagrożenia płynącego z owej sytuacji. Jeśli nie będą, to szybko życie może nauczyć dlaczego należy dbać o to cenne ID.
      Schody się jednak zaczynają gdy projekt trzeba przekazać.
      Miałem okazję pracować z kodem, który dostałem w spadku. Do tego bez możliwości konsultacji z twórcami.
      Jedno jest pewne, gdy zespół hakuje, to piekło spada na kolejne osoby pracujące z taką aplikacją.

      I czy tylko Wojtek i ja widzieliśmy ID w tych 2 metodach?
      Bosz, naprawdę pracuję z legacy code :o

    • 14/06/2011 at 08:52

      Samo pole ID nie jest narzucone przez DB. Narzuca je “niechcica” osoby odpowiedzialnej za zaprojektowanie danej klasy. W większości obiektów biznesowych można z powodzeniem zastąpić klucz w postaci ID za pomocą klucza złożonego z pól obiektów. Tyle tyko, że nikomu nie chce się kombinować nad budową takiego klucza (“bo komplikuje”) ani nikt nie będzie chciał go implementować.
      Bardzo częsta sytuacja w aplikacjach “otwartych na świat” gdzie rekord użytkownika ma swoje ID, ale od użyszkodniak wymagamy podania email. Dodatkowo testujemy unikalność emaila w bazie.

      W przypadku używania klucza opartego o pola obiektu, a nie sztuczne ID problem nam znika. Możemy wymusić zarówno unikalność danych (np. email) jak i ich kompletność -> patrz JSR 303 + klauzula NotNull w modelu po stronie bazy.

      • 14/06/2011 at 21:51

        Zaczynam pisać po raz drugi to więc się streszczę :P

        Dokładnie taki problem rozwiązywałem w przeciągu ostatniego miesiąca. Chciałem wprowadzić klucz złożony z 2-3 pól. Razem złączone tworzyły by unikalny klucz główny i umożliwiały proste (i logiczne) określenie czym jest obiekt.
        Odbiłem się jednak o ścianę niepewności czy to zadziała, czy inni developerzy zrozumieją moje intencję, nigdy tak nie programowaliśmy i kilka podobnych. Mój poziom innowacji też ma swoje granice więc mam kolejną tabelę z sztucznym kluczem głównym :/

        Jednak czynnik psychologii w prowadzeniu zespołu i projektu to zagadnienie na zupełnie inny post.

        Ostatecznie narzekać nie mogę. Pomagałem kolegom ogarnąć projekt gdzie była duża DB bez pozakładanych PK. Tam dopiero była masakra :)

  8. 07/06/2011 at 08:36

    Na zakończenie i pocieszenie napiszę, że nie tylko wy pracujecie z legacy codem :)

  1. 21/06/2011 at 22:28

Leave a reply to Wojtek Cancel reply