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 :)

Send this to a friend