Przychodzi taki dzień, kiedy masz opanowane wszystkie tematy. Znasz składnię, rozumiesz działanie. Czas na Twój najlepszy projekt, najlepszą rozmowę kwalifikacyjną … i pada to niezwykłe pytanie – interfejs, czy klasa abstrakcyjna w Java?
I pojawia się problem 😀 Zamknijmy więc cykl o interfejsach i klasach abstrakcyjnych tym tematem.
Zmierzmy się z nim. Lecimy!
Dylemat doskonałości
Kiedy przychodzi do pytania, czy w tym miejscu lepiej zastosować interfejs, czy klasę abstrakcyjną, to czasem odpowiedź nie jest wcale taka prosta i oczywista. Pewnie w danym momencie czujesz pod skórą, że któreś z rozwiązań jest lepsze.
Ale w zasadzie dlaczego? Skąd ta pewność? Kiedy nasz kod będzie bliżej doskonałości? 😀
Minimalna teoria
Jeśli chodzi o modelowanie abstrakcji w języku Java mamy dwie możliwości, a są nimi klasa abstrakcyjna i interfejs.
Ich zastosowanie różni się przypadkami użycia. Czasem na pierwszy rzut oka ciężko zdecydować. Z doświadczeniem te wybory stają się prostsze. Podpowiada nam ono, że hey, w tym miejscu to będzie interfejs, a w tym klasa abstrakcyjna. Inaczej przecież będzie źle. I często, jako programiści, wpadamy w długie rozważania na sensem tego wyboru.
Jest kilka wzorców, które pomogą Ci w prosty sposób szybko przejść dalej z tym tematem.
Spróbujmy zatem je tutaj zebrać. Pomogą nam w rozróżnieniu, kiedy stosować interfejsy, a kiedy klasy abstrakcyjne w Java.
Nasze wzorce
Wiele różnych klas będzie implementowało Twój kontrakt
O co chodzi? Popatrzmy na JDK 14 i interfejs (nasz kontrakt) java.io.Serializable. Interfejs, ten nie zawiera metod, jest tzw. marker interface’em. Istotna jest semantyka, czyli jego znaczenie. Mówi ona o tym, że obiekt klasy implementującej ten interfejs będzie mógł podlegać serializacji.
Czy na tym etapie wiemy, jakie to będą klasy? Czy wiemy, jaka będzie ich hierarchia?
Nie możemy odpowiedzieć na te pytania. Mogą to być różne klasy i różne ich hierarchie. Zarówno w JDK, jak i w różnych bibliotekach zewnętrznych, a także w naszym kodzie.
Jednak będą dzieliły wspólny kontrakt, czyli zdolność (teoretyczną) do poprawnej serializacji.
Podobnie będzie np. z interfejsem java.io.Closeable, który będzie gwarantował (czasem teoretycznie), że źródło naszych danych może zostać zamknięte i dzięki temu zasoby zostaną zwolnione. Co dokładnie zostanie zamknięte i jak to zamknięcie będzie przebiegać – tego nie wiemy, a różnorodność klas oznacza, że będą to różne rozwiązania.
Jeśli popatrzymy na listę klas implementujących ten interfejs, to znajdziemy na niej szereg niezwiązanych koncepcyjnie ze sobą bytów, np. AbstractInterruptibleChannel, FileSystem, URLReader i wiele innych. Włączenie ich wszystkich we wspólną hierarchię klas byłoby błędem. Koncepcyjnie są to różne byty.
Decyzja: Interfejs
Chcesz dzielić kod między bliskimi koncepcyjnie klasami
Kiedy przychodzi do dzielenia się konkretnym kodem w ramach wspólnej koncepcji klas wybór wydaje się naturalny. Przykłady z poprzedniego artykułu, takie jak kształty, samochody, w naturalny sposób tworzą naturalne hierarchie.
Posłużmy się tutaj przykładem z JDK. Skorzystajmy z klasy abstrakcyjnej java.util.AbstractMap.
Warto zauważyć, że klasa AbstractMap implementuje interfejs java.util.Map dający deklaracje zachowań. Jednak szkielet implementacji, wspólnego kodu, jest w klasie abstrakcyjnej. Dzielimy kod, mamy bliską relację miedzy pojęciami. Wybieramy klasy abstrakcyjne.
Decyzja: klasa abstrakcyjna
Zależy Ci na implementowaniu wielu kontraktów jednocześnie
Tutaj sprawa jest prosta. W ramach klas Javy nie możemy dziedziczyć po wielu klasach. Takie mamy ograniczenie języka.
Tak po prostu jest. Dlaczego?
Najlepiej odda to fragment tekstu twórcy języka Java, czyli Jamesa Goslinga. W lutym 1995 roku napisał on:
JAVA omits many rarely used, poorly understood, confusing features of C++ that in our experience bring more grief than benefit. This primarily consists of operator overloading (although it does have method overloading), multiple inheritance, and extensive automatic coercions.
James Gosling, Java: an Overview
I jak możemy wnioskować z tego fragmentu, James Gosling uważał wówczas, że wielodziedziczenie klas nie jest zbyt przejrzystym konceptem.
Musimy pamiętać, że w warstwie abstrakcji Javy mamy wielodziedziczenie na poziomie interfejsów. Dotyczy ono jednak zachowań.
Jeśli chodzi o wielodziedziczenie na poziomie klas, elementem powodującym konfuzję stają się zmienne instancyjne. A to prowadzi do problemu znanego jako Deadly Diamond of Death (ale o tym, być może innym razem).
Decyzja: Interfejs
Kiedy wiesz, że obiekty będą miały wiele wspólnych implementacji metod
Szkielety metod (np. wykorzystywane w algorytmach, template methods), fragmenty implementacji to dobra wskazówka do wyboru klasy abstrakcyjne.
Uważny czytelnik może zauważyć, że w Interfejs w Java – na rozmowę kwalifikacyjną pisałem o tym, że interfejs Iterable zawiera m. in. metodę domyślną forEach. Dzięki temu klasy implementujące ten interfejs nie muszą same jej implementować.
Należy tu jednak pamiętać, że metody domyślne w interfejsach pojawiły się w ramach gwarancji kompatybilności. A my staramy się wyprowadzić nieco bardziej ogólne wskazówki do modelowania. Nie szukamy tutaj technicznych sztuczek.
Decyzja: klasa abstrakcyjna
Gdy chcesz określić zachowanie, ale nie chcesz odpowiadać za implementacje na tym etapie
Wyobraźmy sobie takie zagadnienie. Projektujemy aplikacje w wybranej domenie biznesowej. Pracujemy w Domain-Driven Design (DDD) i dbamy modelowanie domeny.
Dla przykładu weźmiemy sobie koncepcję repozytorium w domenie, np. repozytorium obiektów płatności – PaymentRepository.
Na poziomie naszego modelu domenowego zależy nam na określeniu zachowania. Repozytorium ma nam pomóc w zapisaniu i późniejszym odtworzeniu danego obiektu.
Z punktu widzenia biznesu nasz model domenowy będzie wiedział, że jest takie repozytorium i co potrafi zrobić.
Nie musi on jednak wiedzieć, jak dokładnie PaymentRepository będzie zapewniało persystencję danych. Tym zajmie się warstwa infrastruktury. Być może do tego celu w infrastrukturze wykorzystamy bazę Oracle, a być może Mongo. Być może w testach napiszemy prostą implementację pamięciową.
Liczy się zatem to, co chcemy robić, czyli deklaracje naszych metod. Jak to chcemy robić, czyli implementacja, jest być może mniej istotna biznesowo.
Inny przykład z JDK, już trochę występujący wcześniej. Interfejs java.util.Map mówi nam, jakie operacje powinny dostarczać mapy. Nie mówi jednak w dużej mierze, jak będzie wyglądała ich konkretna implementacja.
Decyzja: Interfejs
Chcesz korzystać z pół instancyjnych
Kiedy przychodzi do zarządzani stanem obiektu, jego polami instancyjnymi, sprawa robi się bardzo prosta. W interfejsach nie możemy korzystać ze zmiennych instancyjnych. Wybór jest zatem oczywisty.
Decyzja: klasa abstrakcyjna
Creme de la Creme, czyli podsumowanie dla Ciebie
Tym razem podsumowanie w formie infografiki:
Jestem bardzo ciekawy Twojego zdania. Napisz, co o tym sądzisz ❓ A może widzisz inne wzorce, które można zastosować do wyboru: klasa abstrakcyjna, czy interfejs w Java ❓
Zostaw komentarz, wyshare’uj infografikę 😎
BTW Jeśli chcesz pobrać infografikę w PDF to tutaj LINK.
Nieprzeciętnie popularny artykuł o interfejsach znajdziesz tutaj: Interfejs w Java – na rozmowę kwalifikacyjną
Równie świetny artykuł o klasach abstrakcyjnych też jest na wyciągnięcie ręki: Klasa abstrakcyjna w Java