W poprzednim wpisie w kilku zdaniach opisałem czym jest run-time  i czym on się różni od etapu compile-time . Aby zrozumieć jak działa polimorfizm dynamiczny możemy posłużyć się wieloma prostymi, mniej lub bardziej życiowymi przykładami. Jeśli kiedykolwiek szukałeś przykładów użycia polimorfizmu na pewno trafiłeś na przykłady z klasą bazową Animal i klasami pochodnymi Dog, Cat itd. Aby nie powielać tego przykładu, posłużmy się innym, nieco bardziej życiowym. Załóżmy taki oto scenariusz:

  1. Użytkownik pobiera dane klienta ze źródła danych. Być może będzie to relacyjna baza danych, firmowy system CRM, a może plik.
  2. Na podstawie pobranych danych klienta, a dokładnie biorąc pod uwagę kraj wykonywania pracy oraz wynagrodzenie, aplikacja obliczy podatek dochodowy jaki klient powinien zapłacić do swojego urzędu skarbowego. Dlaczego w zależności od kraju? Bo zapewne w każdym z krajów są różne skale i stawki podatkowe oraz pewnie masa innych przepisów.
  3. Na koniec wypiszemy na ekran informację, jaki podatek powinien zapłacić każdy z klientów.

Jeśli byśmy chcieli wykonać taki scenariusz bez komputera, w realnym życiu, to w uproszczeniu wyglądałoby to tak, że użytkownik, np.: księgowa sięga po teczkę z danymi klienta Joe Doe, w tym: fakturę, gdzie będą dane takie jak: nazwa firmy, NIP, kwota oraz dodatkowo księgowa bierze z teczki kwitek, dzięki któremu będzie wiedzieć na podstawie jakiej skali podatkowej należy rozliczyć klienta. Następnie księgowa przystąpiłaby do faktycznego obliczania podatku.

Pobranie danych ze źródła

Przypuśćmy, że dane klientów umieszczone są w relacyjnej bazie danych. W ramach ćwiczenia pozostawimy jednak  naszą aplikację otwartą na rozszerzenie, dopuszczając możliwość, że źródło danych jakim jest baza danych, będzie mogło być zastąpione innym źródłem danych np.: zewnętrznym systemem, który udostępnia te dane przy pomocy protokołu http. A może w przyszłości dane jednych klientów będą pobierane z relacyjnej bazy danych, a innych z zewnętrznego systemu? Tworzenie abstrakcji na zaś nie jest może najbardziej trafionym pomysłem, ale my w końcu tylko ćwiczymy.

Pobranie danych klienta jest abstrakcyjną, ogólną nazwą czynności, którą zidentyfikowaliśmy jako wymaganą do realizacji całego procesu. Nie wchodzimy w tej chwili w szczegóły jak ta czynność będzie dokładnie realizowana. O tym czym jest abstrakcja możesz przeczytać tu .

Z tyłu głowy mamy fakt, że nasz abstrakcyjny termin pobranie danych klienta może mieć więcej niż jeden sposób realizacji lub jedno rozwiązanie może zostać w przyszłości zastąpione innym.

Obliczenie podatku dochodowego

Prawdopodobnie sposób wyliczenia podatku dochodowego różni się w różnych krajach. Identyfikujemy kolejne abstrakcyjne, ogólne pojęcie, które samo w sobie jest zrozumiałe dla każdego zainteresowanego tj.: obliczenie podatku dochodowego. Nie wchodzimy w tej chwili w szczegóły w jaki sposób oblicza się podatek dochodowy. Wiemy natomiast, że pojęcie to może mieć również wiele sposobów realizacji w zależności od tego, gdzie ten podatek dochodowy będzie opłacany.

Implementacja

Słowo abstrakcja pojawiło się już kilka razy. Chyba jest dość mocno powiązane z naszym polimorfizmem, warto zapamiętać. 🙂

Sam fakt identyfikacji tych abstrakcyjnych pojęć takich jak pobranie danych klienta oraz obliczenie podatku dochodowego pozwala nam stworzyć w naszej aplikacji dwa interfejsy:

– interface ClientDataRetrieval

                – Client findBy(String name)

– interface TaxCalculator

                – BigDecimal calculate(BigDecimal salary)

Zauważ, że żadna z powyższych nazw nie mówi kompletnie nic o sposobie implementacji tj.: o aspektach technicznych. Nawet opracowując nazwy naszych klas / interfejsów itp., staramy się nie uzależniać pojęciowo od technologii (czyli ClientDataRetrieval zamiast SQLCustomerData czy DatabaseCustomerInfo). Programista, który będzie kiedyś czytał Twój kod doskonale zrozumie jakie jest przeznaczenie tych interfejsów i co miałeś dokładnie na myśli. Nawet jeśli sposób implementacji zmieni się dziesięciokrotnie (co oczywiście się raczej nie zdarza), to nasze nazwy interfejsów nadal będą aktualne. To realny, z życia wzięty proces obliczania podatku dochodowego narzucił nam to nazewnictwo, a nie technologia, którą wykorzystamy do jego implementacji.

Według założeń pełen scenariusz naszego procesu składa się z trzech punktów. Pod skórą można wyczuć, że fragment słowa polimorfizm, a mianowicie poli (wielość, mnogość) wchodzi do gry w dwóch pierwszych punktach.

Nasze początkowo zidentyfikowane abstrakcyjne pojęcie pobranie danych klienta (interfejs ClientDataRetrieval) może przybierać dowolnie wiele form np.: SQLClientDataRetrieval, w którym dobieramy się do danych klienta za pomocą zapytania SQL.

Tak samo jest ze sposobami realizacji abstrakcyjnego pojęcia obliczenie podatku dochodowego tzn.: nasz abstrakcyjny TaxCalculator może również przybierać dowolnie wiele form tj.: PolishTaxCalculator czy SpanishTaxCalculator. Sam w sobie TaxCalculator nie mówi kompletnie nic o implementacji. Wiemy co, ale nie jak będziemy robić.

Whoa! Fajnie. Pomyśl, że któregoś dnia przychodzi przełożony i mówi, że od dziś obsługujemy klientów z Radomia i ich dane trzymane są w plikach. Nic prostszego. Tworzysz kolejną implementację interfejsu ClientDataRetrieval oraz TaxCalculator np.:  FileClientDataRetrieval oraz RadomTaxCalculator, gdyż z pewnością w Radomiu stawka podatkowa jest inna niż w pozostałej części Polski i świata. Jest przyjemnie, bo robiąc taką zmianę tworzysz nowy plik z kodem zamiast modyfikować istniejące. Nie chcę popadać w paranoję, ale modyfikowanie istniejących plików siłą rzeczy zawsze jest bardziej ryzykowne niż tworzenie nowych. Przynajmniej nie popsujemy tego co istnieje.

W punkcie startowym naszej aplikacji (klasa App) budujemy, szumnie mówiąc, drzewo zależności naszego programu. Możemy wykorzystać do tego kontener IoC wbudowany w Springa, użyć innego istniejącego, użyć fabryki zwracającej konkretną implementację w zależności od danych wejściowych lub wywołać w naszym prostym przykładzie serwis przekazując do konstruktora ręcznie jego zależności.

W naszym przypadku przekazujemy (wstrzykujemy) do serwisu TaxService realizującego nasz scenariusz implementację źródła danych oraz implementację kalkulatora podatkowego (Rys. 1) w zależności od na sztywno przyjętych założeń.

Rys. 1 Punkt wejścia do programu. Wstrzykiwanie zależności poprzez konstruktor.

W zależności od wstrzykniętych zależności zostaną wykonane różne metody findBy oraz calculate w serwisie TaxService (Rys. 2)

Polimorfizm, którego zastosowanie pokazałem w ramach tej drobnej aplikacji jest tzw.: polimorfizmem dynamicznym (run-time). Przyjrzyj się poniższemu kodzikowi.

Rys. 2 Serwis TaxService

Patrząc na powyższy konstruktor (linia 15) oraz na wywołanie metody calculate oraz findBy (odpowiednio linie 22 oraz 23) nie wiemy (ani ja, ani Ty, ani kompilator) jaka implementacja zostanie wstrzyknięta do konstruktora i która metoda calculate findBy zostanie wywołana. Decyzja o tym, które implementacje tych metod zostaną wywołane zostanie podjęta dopiero podczas działania programu i zależy to od klienta, a konkretnie od kraju wykonywania przez niego pracy. W naszym przypadku utworzyliśmy w klasie App trzy obiekty typu TaxService do którego wstrzyknęliśmy interesujące nas implementacje.

Smutna definicja:

Podsumowując polimorfizm dynamiczny zakłada, że to nie referencja decyduje, która metoda zostanie wywołana tylko typ obiektu, na który ta referencja wskazuje. Referencja mówi jedynie jaką metodę możemy wywołać.

Rozkładając to na czynniki pierwsze:

  1. W serwisie TaxService, w konstruktorze przyjmujemy oprócz interfejsu ClientDataRetrieval, parametr o nazwie taxCalculator. Jest on typu TaxCalculator.
  2. Na tej referencji (de facto na kopii wartości przekazanej referencji, pamiętaj – pass by value) możemy wywołać metodę o sygnaturze calculate(BigDecimal salary) bo jej typ (TaxCalculator) ma w swoim wnętrzu tylko taką definicję.
  3. Jeśli do konstruktora przekazaliśmy np.: SpanishTaxCalculator to wóczas zostanie wywołana metoda calculate zaimplementowana właśnie w klasie SpanishTaxCalculator

W ramach testu możesz spróbować dodać metodę np.: void test() {} do klasy SpanishTaxCalculator i spróbować napisać poniższe dwie linijki kodu:

TaxCalculator taxCalculatorTest = new SpanishTaxCalculator();
taxCalculatorTest.test();

Linia taxCalculatorTest.test() zostanie podkreślona na czerwono przez kompilator, ponieważ referencja typu TaxCalculator nie oferuje metody takiej jak test(). Jeślibyśmy jednak dodali taką metodę dodatkowo w interfejsie TaxCalculator, to kompilacja oraz uruchomienie metody test powiedzie się.

Aby jeszcze w inny sposób spróbować zobrazować Ci czym jest dynamiczny polimorfizm to wyobraź sobie stronę internetową z dwoma przyciskami. Pierwszy przycisk to Losuj klienta, a drugi przycisk to Oblicz podatek dochodowy wylosowanego klienta.

Ten przykład najlepiej obrazuje brak możliwości przewidzenia przed uruchomieniem (w czasie kompilacji) programu jaka implementacja obliczająca podatek dochodowy zostanie wybrana (a w związku z tym która metoda zostanie wywołana). Raz możesz bowiem wylosować klienta hiszpańskiego, a za drugim razem polskiego. W zależności od tego co zostanie wylosowane, zostanie stworzony obiekt TaxService z wstrzykniętą implementacją SpanishTaxCalculator lub PolishTaxCalculator. Odpowie za to pewnie jakaś fabryka z ifami albo switchem. Mogłoby to wyglądać tak:

Rys. 3 Fabryka zwracająca implementację kalkulatora w zależności od kraju klienta

W Javie wszystkie metody które nie są finalne, prywatne, statyczne są polimorficzne (wirtualne). Tzn., że możemy zrobić override takiej metody, czyli zmienić jej zachowanie. W naszym przypadku nie tyle zmieniliśmy, co zaimplementowaliśmy zachowanie abstrakcyjnej metody zdefiniowanej w interfejsie.

Można dostrzec, że abstrakcja i polimorfizm grają w jednej drużynie. Dzięki opracowaniu abstrakcji TaxCalculator jesteśmy w stanie wykorzystać siłę polimorfizmu. Polimorfizm możemy również wykorzystać do odwrócenia zależności, o czym możesz przeczytać tu.

Z przykładów dostępnych w internecie na pewno wiesz, że polimorfizm pojawia się również w kontekście dziedziczenia. Możliwe, że widziałeś masę przykładów w internecie z klasą Animal itd. Zasada jest podobna i wiąże się ze smutną definicją oraz abstrakcją.

A kiedy i czy w ogóle stosować dziedziczenie, jak robić override zaimplementowanych metod w klasie bazowej, aby być zgodnym z SOLID i innymi kilkuliterowymi skrótowcami oraz o tym czy jest jakaś alternatywa dla dziedziczenia możesz przeczytać tu.

To był polimorfizm dynamiczny. Stety niestety to nie koniec. Mamy jeszcze polimorfizm statyczny oraz polimorfizm parametryczny. O tym wkrótce.

Przydatne linki:

https://blog.cleancoder.com/uncle-bob/2016/01/04/ALittleArchitecture.html

Uncle Bob (Robert C. Martin) opisuje jak wykorzystać polimorfizm do uzyskania odwrócenia zależności między modułami wysokopoziomowymi a niskopoziomowymi.

https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html

Można podsumować ten link stwierdzeniem że wiele z bloków kodu, w których mamy ify albo instrukcje switch można zastąpić kodem polimorficznym. Kod wykorzystujący polimorfizm jest łatwiejszy do utrzymania, testowania, czytania. Porównanie implementacji za pomocą kodu warunkowego i polimorficznego możesz znaleźć tu.

Jeśli interesuje Cię opis struktury aplikacji w IDE to zapraszam tu.

Github:

https://github.com/wprostychslowach/samples.git

W pakiecie polymorphism projektu znajdziesz przykład użycia polimorfizmu.

Leave a Reply

Your email address will not be published. Required fields are marked *