Jak programować obiektowo? cz. 7 – final

Ale o co tyle szumu?

Do czego możemy wykorzystać słowo final? Do ograniczenia dalszego dziedziczenia bądź nadpisywania metody. Informuje ono (i chroni), że dana wersja klasy/metody jest wersją ostateczną i nikt nie powinien nigdy jej nadpisywać.

Czy rzeczywiście jest to tak istotna kwestia, że warto zawracać sobie tym głowę?

Nie namawiam was do tego, abyście każdą swoją klasę metodę w ten sposób oznaczali, ponieważ efektem byłoby jedynie zdewaluowanie siły jaka w słowie final brzmi. Jeżeli jednak piszecie funkcjonalność, gdzie istotne jest takie, a nie inne działanie i jesteście pewni, że wszelkie rozszerzenia byłyby pogwałceniem pierwotnych założeń projektowych to warto wykorzystać możliwość dopisania słowa final. Jak już wspomniałem wyżej, po pierwsze chroni kod, a po drugie jest informacją i sygnałem dla programisty, który z tym kodem pracuje.

Metody finalne

Wyobraźmy sobie, że implementujemy złożoną operację, która pozwala nam w określony sposób modyfikować przechowywane przez nas dane. Te dane jednak są trzymane w różnych miejscach: baza, pliki, sesja. Powodzenie całej operacji zależy w dużej mierze od kolejności wykonywanych instrukcji, a wynika to z zależności pomiędzy nimi. Wszystko ma na celu zapewnienie spójności danych.

Jedyną komplikacją jest fakt, że jest kilka różnych typów danych i wykonanie poszczególnych kroków choć logicznie ma ten sam cel to implementacyjnie może być różne. Mam nadzieję, że wyczuwacie podświadomie, że wykorzystanie wzorca Strategy albo Template Method samo się narzuca 🙂

Na potrzeby przykładu my zdecydujemy się na implementację Template Method.

Kod mógłby wyglądać następująco:

Oczywiście do tego dochodzą konkretne klasy rozszerzające CriticalDataManipulator oraz funkcjonalność, która na podstawie dostarczonych danych zadecyduje, który obiekt wybrać aby przeprowadzić na nich odpowiednie operacje.

Załóżmy, że pewnego dnia któryś programista rozszerzając funkcjonalność dochodzi do wniosku, że w metodzie manipulate() nie jest mu tak naprawdę potrzebne wywołanie metody doSomething2(). Widzicie już rezultat takiego myślenia? Programista rozszerza CriticalDataManipulator i nadpisuje manipulate(). Testy przeszły, kod zostaje dodany do repozytorium, wrzucamy go na produkcję i wszystko działa dobrze… aż pewnego dnia pojawia się zgłoszenie od klienta, który twierdzi, że „aplikacja zwariowała i generuje losowe rzeczy”. Nikt nie wie, gdzie tkwi błąd. Po pewnym czasie spędzonym na próbach znalezienia wadliwego miejsca okazuje się, że pomysł modyfikacji metody manipulate() nie był wcale taki dobry, ponieważ każdy krok rzeczywiście był niezbędny do tego aby cała operacja, bez względu na okoliczności, zakończyła się sukcesem.

Z pewnością domyśliliście się już, że właśnie do metody manipulate() wypadałoby dodać słowo final. Nawet w sytuacji, gdy programista ma bezpośredni dostęp do źródła i bez większych problemów może zmienić ten kod (np. usunąć słowo final) to z pewnością da mu ono do myślenia. Może skontaktuje się z autorem kodu? Może poświęci więcej uwagi na analizę kodu, do którego to słowo się odnosi? Istnieje spora szansa, że zdobyta w ten czy inny sposób pozwoli mu na stworzenie lepszego projektu tej funkcjonalności oraz ustrzeże przed błędem na produkcji.

A to przecież tylko jedno słowo.

Klasy finalne

Załóżmy, że stworzyliśmy sobie klasę JsonView, która służy nam do wyświetlania modeli w formacie JSON:

Pewnego dnia natrafiamy na przypadek, że dane z modelu muszą dodatkowo zostać przetłumaczone. Programista, który jest odpowiedzialny za implementację dochodzi do wniosku, że najlepszą drogą do rozszerzenia funkcjonalności jest rozszerzenie klasy, która tą funkcjonalność dostarcza:

Po pewnym czasie, kolejny programista odkrywa, że w jego przypadku musi istnieć możliwość określenia, które pola modelu mają zostać zwrócone, a które nie. W dodatku również mają być one tłumaczone. Rozwiązaniem problemu i w jego przypadku jest dziedziczenie.

A teraz co w przypadku, gdy będą kolejne wymagania dotyczące tej funkcjonalności? Kolejne dziedziczenia? A co gdy okaże się, że będziemy mieli konieczność posiadać różne wariacje „dodatków”? Jaki jest problem z dziedziczeniem w tym przykładzie? Jeżeli jest klasa z paroma metodami i podczas dziedziczenia (kilkukrotnego) są one nadpisywane, to zarządzanie czymś takim, to prawdziwy koszmar.

Dlatego może warto byłoby pokusić się o stworzenie JsonView jako klasy finalnej? Po to, aby uzmysłowić kolejnemu programiście, że jeżeli myśli nad rozszerzeniem funkcjonalności to rozszerzenie klasy najprawdopodobniej nie jest najlepszym wyjściem.

Na odchodne

Na koniec jeszcze powtórzę to, o czy wspominałem na samym początku – słowa final nie należy nadużywać. Jeżeli co druga klasa/metoda w Waszym projekcie będzie finalna to informacja przekazywana przez deklarację przestanie być widoczna, a co za tym idzie – najprawdopodobniej zostanie potraktowana jako nieistotna.

Używajcie słowa final jak znaku ostrzegawczego, gdy wiecie, że wszelkie próby rozszerzania powinny być przemyślaną i rozważoną decyzją. A najlepiej, gdyby decyzja w takim przypadku została zmieniona 🙂

Pozostałe artykuły z cyklu

Jestem fanatykiem obiektowego programowania i nieustannie pogłębiam swoją wiedzę we wszelkich tematach z nim związanych. Wszystko czego się dowiem konfrontuję z rzeczywistością, ponieważ teoria, która nie ma odzwierciedlenia w praktyce, traci swój sens tam, gdzie zaczyna się praca programisty :)

  • Aleksander Wons

    O ile w pełni mogę się zgodzić z tym, że „final” ma swoje specyficzne zastosowania, to jednak podany przykład (ten z metodami final) jest, IMO, kompletnie nietrafiony.

    1. Logical dependency. Coś, czego w ogóle nie powinno być w kodzie. Jeśli metody klasy X mają być wywoływane w danej kolejności, to musi pomiędzy nimi istnieć ścisła zależnośc fizyczna a nie tylko logiczna. Czyli metoda A zwraca to, co metoda B dostaje na wejściu a metoda C dostaje na wejściu to, co metoda B zwraca. Wtedy w ogóle nie da się doprowadzić do opisanej sytuacji.

    2. Implementation detail. Jeśli dane metody mają być wykonywane tylko w tej a nie innej kolejności, to mogą być opakowane w pojedyńczy call to prywatnej metody. Dalej możesz rozszerzyć klasę, ale już nie nadpisać prywatnej metody. Czyli opakowujesz wywołanie do A B i C (prywatnych metod) w inną prywatną metodę. A tutaj już widać, że to aż prosi się o wyciągniecie do odrębnej klasy.

    Generalnie podany w przykładzie problem da sie rozwiązać na wiele sposobów, a „final” jest IMO najgorszym z możliwych. Bo to tylko „workaround” a nie konkretne rozwiązanie.

    • Sebastian Malaca

      Cóż, z rozwiązaniami to tak już jest (a w naszej branży jest to naprawdę widoczne), że istenieje zazwyczaj więcej niż jedno poprawne 🙂

      IMO, dorzucenie słowa final mogłoby być wskazówką dla programisty, który by rozszerzał daną funkcjonalność do wykorzystania np. wzorca Dekoratora do ugryzienia problemu.

      Co do zależności pomiędzy klasami, to same nie muszą o tej zależności wiedzieć (tutaj można wykorzystać interfejs aby „uchronić” je od tej wiedzy. To inny obiekt powinien odpowiadać za zarządzanie kolejnością.

      Ogólnie nie widzę powodu dlaczego wykorzystanie słowa final uznajesz jako obejście problemu? Prawda jest taka, że dopóki nie będziemy mieli konieczności rozwinięcia danej funkcjonalności to nie istnieje żaden problem, który trzebaby obejść. Final używamy tylko w tym celu, aby pokazać, że jeżeli ktoś pomyśli nad rozszerzeniem, to powinien jeszcze raz się zastanowić czy jest to rzeczywiście najlepsze rozwiązanie.

      • Aleksander Wons

        W tym konkretnym przypadku uważam użycie final za obejście problemu, bo problemem, IMO, nie jest konieczność wymuszenia kolejności wywołań wewnętrznych metod klasy a sam fakt, że istnieje zależność logiczna, której należało by się pozbyć. Wtedy nie ma konieczności używania final, bo kolejność jest naturalna a zależność fizyczna. Wtedy w ogóle tematu final by nie było.

        Dla mnie ten przykład wygląda jak zamiatanie problemu pod dywan. Mam zależność logiczną (co jest bad practice) to nie pozbęde się jej tylko zablokuję innym możliwość grzebania w kodzie.

        Wiem, że zawsze isnieje wile rozwiązań jednego problemu, ale w tym konkretnym przypadku obaj widzimy problem w innym miejscu.

        • Sebastian Malaca

          Czy mógłbyś mi wyjaśnić o jakiej zależności logicznej mówisz, bo chyba coś mi umyka 🙂

          Co do final, to tak jak pisałem wyżej, to nie jest rozwiązanie jakiegoś problemu, bo na etapie dodawania słowa final nawet nie ma mowy o czymkolwiek do rozwiązania (czyli w chwili gdy mamy jedynie klasę JsonView).
          Natomiast final może wskazać kierunek rozwiązania, gdy pojawia się konieczność rozszerzenia funkcjonalności. Może dać do myślenia.

          • Aleksander Wons

            Ja cały czas mówię o pierwszym przykładzie metody finalnej. Absolutnie nie mówię tutaj o JsonView.

            doSomething1(), doSomething2() i doSomething3().
            To jest zależność logiczna, której w ogóle być nie powinno. Kod powinien być tak napisamy, żeby nie było najmniejszej wątpliwości które metody i w jakiej kolejności mają być wykonane. I to właśnie jest problem, który należy rozwiązać.

            I dla tego twierdzę, że final jest tutaj tylko i wyłącznie obchodzeniem problemu a nie jego rozwiązaniem.

            Jeśli mamy wymagania co do kolejności wykonywania pewnych metod, to zależność powinna być fizyczna – czyli być jawnie zadeklarowana w kodzie (tylko nie mylić z dodawaniem komentarzy).

          • Sebastian Malaca

            Czyli dobrze, że zapytałem, bo ja rzeczywiście odniosłem mylne wrażenie, że chodzi o drugi przykład.

            To co mówisz sprowadza się IMHO do pytania czy template method jest tak naprawdę dobrym wzorcem czy nie. Osobiście sam jestem zdania, że lepiej z niego zrezygnować z powodów, które już wymieniłeś.
            Niemniej jednak na potrzeby artykułu wydał mi się całkiem dobrym i wymownym przykładem demonstrującym co final pomaga chronić.

            Tak czy inaczej dzięki za cenne komentarze. Przykładu pozwolę sobie nie zmieniać, bo wtedy Twoje uwagi nie byłyby tak wymowne. A wydaje mi się, że same w sobie niosą wiedzę wartą przyswojenia.

Send this to a friend

webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL