Interfejs czy klasa abstrakcyjna
Dla większości początkujących programistów, wybór pomiędzy interfejsem, a klasą abstrakcyjną zazwyczaj jest bardzo trudny. Najnowsze wersje Javy, poprzez zatarcie różnic pomiędzy tymi dwoma strukturami, wprowadzają dodatkową konsternację. Kiedy w takim razie używać interfejsu, a kiedy klasy abstrakcyjnej? Myślę, że najlepiej korzystać ze starych i sprawdzonych kryteriów.
Główne różnice pomiędzy klasami abstrakcyjnymi i interfejsami, dotychczas (Java do wersji 7. włącznie) w wielu wypadkach determinowały ich użycie:
-
można dziedziczyć po jednej klasie ale implementować wiele interfejsów (interfejsy są bardziej elastyczne),
-
klasa abstrakcyjna, w przeciwieństwie do interfejsu, może posiadac implementację części metod oraz metody prywatne (klasy abstrakcyjne są bardziej reużywalne).
Tak się złożyło, że Java 8 wprowadziła metody domyślne, przez co wyższość klas abstrakcyjnych objawiała się już tylko przy próbie użycia metod prywatnych. Z kolei kolejna wersja Javy poszła za ciosem i wprowadziła możliwość użycia tychże metod i od tego momentu, na pierwszy rzut oka, klasy abstracyjne przestały posiadać jakiekolwiek zalety w stosunku do interfejsów. Biorąc pod uwagę, że te nowości zostały wprowadzone w celu dodania nowych funkcjonalności do istniejących kolekcji (m.in. strumienie) z zachowaniem kompatybilności wstecznej (czyli uniknięcia potrzeby implementacji dodatkowych interfejsów w starym kodzie), powstrzymywałbym się przed zarzuceniem korzystania z klas abstrakcyjnych ze względu na ich pozorną zbędność. Kiedy w takim razie korzystać z klas abstrakcyjnych, a kiedy z interfejsów? Są 2 podstawowe pytania, na które odpowiadając sobie, łatwo podejmiemy odpowiednią decyzję.
Czy obiekty, które będą korzystać z naszej struktury, będą należeć do pewnej hierarchii?
Klasa abstrakcyjna jest dedykowana do tworzenia hierarchii klas i współdzielenia kodu pomiędzy obiektami.
Dobrym przykładem może być klasa Person
, która stanowi bazę ze współdzielonym kodem dla kolejnych klas: Female
i Male
.
public abstract class Person {
public void eat() {
System.out.println("I'm eating");
}
public void sleep() {
System.out.println("I'm sleeping");
}
public abstract void talk();
}
public class Female extends Person {
@Override
public void talk() {
System.out.println("I'm talking soprano");
}
}
public class Male extends Person {
@Override
public void talk() {
System.out.println("I'm talking bass");
}
}
Dzięki klasie abstrakcyjnej i klasom dziedziczącym z niej uzyskaliśmy całkiem klarowną hierarchię. Każdy człowiek musi jeść i spać w podobny sposób, w związku z czym klasy dziedziczące mogą korzystać z bazowych metod. W przypadku mowy, głos kobiecy różni się od męskiego, dlatego metoda ta jest abstrakcyjna i wymaga implementacji przez każdą z płci.
Czy obiekty, które będą korzystać z naszej struktury będą posiadać wiele różnych funkcjonalności z których każda jest ogólna i niezależna od hierarchii klas?
Interfejsy są idealnym rozwiązaniem do tworzenia kontraktów określających co ma dana klasa robić bez określania w jaki sposób.
Przykładem może być interfejs Worker
, definiujący pracownika, który powinien pracować bez określania w jaki sposób lub tego kim powinien być.
public interface Worker {
void work();
}
public class John extends Male implements Worker {
public void work() {
System.out.println("I need food and rest to work");
}
}
public class R2D2 implements Worker {
public void work() {
System.out.println("I need electricity to work");
}
}
Dzięki użyciu interfejsu, możemy w łatwy sposób nakazać klasom John
i R2D2
implementować metodę work()
, a co za tym idzie udostępnić pewne publiczne API.
Brak deklaracji tej metody w klasie abstrakcyjnej, uniezależnia potrzebę implementacji danej funkcjonalności od hierarchii klas.
Pozwala to na nakazanie pracy zarówno konkretnemu człowiekowi (niekoniecznie wszystkim), a także robotowi.
Jeśli przykładowo klasa Adam
reprezentowałaby dziecko, poprzez rezygnację z implementacji interfejsu, pozwalamy na niepracowanie danej klasie.
Równocześnie, nic nie stoi na przeszkodzie, aby dodać kolejny interfejs oznaczający umiejętność projekcji hologramów i dodać ją dodatkowo naszemu robotowi.
Przykładowy projekt: