Zastanawialiście się kiedyś po co w Javie 8 wprowadzono interfejsy z (domyślną) implementacją? Oczywiście można powiedzieć, że dzięki temu mamy miksiny/domknięcia/traity czy jak to tam zwał w zależności od punktu odniesienia.
Ale czy taka konstrukcja nie jest przez przypadek rozwiązaniem znacznie poważniejszego problemu projektowego?

Jak zrobić API?

API, czyli po ludzku interfejs aplikacji/biblioteki/narzędzia itp. Java udostępnia dwa w miarę rozsądne rozwiązania pozwalające na tworzenie takich interfejsów.
Pierwsze to oczywiście interfejsy. Zawierają w sobie tylko definicje, sygnatury, metod. Całkowicie abstrakcyjne, niezwiązane z konkretną implementacją ani z określonym podejściem. Fajne są. Generalnie współcześnie w produkcji kodu kładzie się nacisk na programowanie z wykorzystaniem właśnie interfejsów. Dzięki temu zachowujemy elastyczność. O tym za chwilę.
Drugim rozwiązaniem są klasy abstrakcyjne. Sprawdzają się gdy wiemy iż implementacja, jej główny koncept, nie zmieni się w znaczący sposób, a użytkownik powinien zdefiniować szczegóły. To podejście pozwala na tworzenie rozwiązań, które mają mniej punktów swobody, ale jednocześnie już „odwalają” za nas część pracy. Dobry pomysł, gdy wiemy iż wszystkie możliwe, i zdroworozsądkowe, implementacje interfejsu będą powtarzać pewne schematy. Zresztą bardzo często jest tak iż klasa abstrakcyjna implementuje interfejs tak by użytkownik musiał już tylko dopisać niewielki kawałek. Sparametryzować kod.

Elastyczność API

To co napisałem dotychczas jest oczywiste do tego stopnia iż pytanie o różnicę pomiędzy interfejsem i klasą abstrakcyjną zadają już nawet panny z HRu na rozmowie. Na takie pytanie oczywiście pada odpowiedź o tym, że klasa abstrakcyjna to tak naprawdę zwykła klasa, że można tylko dziedziczyć po jednej z nich, a interfejsy to panie kochany implementujemy ile chcemy.
Ok, dobra odpowiedź. Przy czym nie o to chodzi 😀

Główna różnica polega na elastyczności w rozumieniu zarówno zmiany implementacji jak zmiany samego interfejsu. Interfejs jest po opublikowaniu praktycznie niezmienny. Jego implementacje mogą być najróżniejsze. Wyobraźmy sobie taki oto interfejs:

Listing 1. Prosty interfejs

interface Sort<T extends Comparable<T>>{

	Collection<T> sort(Collection<T> c);

}

Ilość implementacji jest naprawdę duża. Zresztą, znajomy z forum 4programmers rzeźbi ciekawy projekt na ten temat. Se weźcie poczytajcie. Ad rem, wyobraźmy sobie, że wydaliśmy naszą bibliotekę z tym interfejsem i ludzie z niej korzystają. Względnie wykorzystaliśmy ją w dużym projekcie i wirus naszego interfejsu rozlał się na całą zdrową tkankę kodu. Teraz chcemy dodać kolejną metodę i dupa…
Interfejsy po publikacji są mało elastyczne. Zmiana w interfejsie pociąga za sobą konieczność zmiany we wszystkich implementacjach.
Trochę inaczej ma się sprawa z klasami abstrakcyjnymi.

Listing 2. Prosta klasa abstrakcyjna

abstract class AbstractSort<T extends Comparable<T>>{

	abstract Collection<T> sort(Collection<T> c);

}

Tu dodanie kolejnej metody jest banalnie proste. Jeżeli nie będzie abstrakcyjna to nie wpływa na poszczególne podklasy. Pojawia się i już. Wadą tego rozwiązania jest oczywiście utrata elastyczności w budowaniu hierarchii klas. Klasy muszą dziedziczyć z naszej klasy abstrakcyjnej i tym samym wykluczają się z innych hierarchii.

Rozwiązanie pośrednie

Rozwiązaniem pośrednim tego problemu jest wielopoziomowy interfejs. Po prostu jeżeli zachodzi konieczność dodania nowej metody do interfejsu to tworzymy podinterfejs.

Listing 3. Hierarchia interfejsów

interface FreeSort<E> extends Sort{

	Collection<E> sort(Collection<E> c, Comparator<E> comp);

}

Teraz tam gdzie potrzebujemy zmieniamy implementowany interfejs. Mało zabawne.

Rozwiązanie w oparciu o Javę 8

Po tym dość długim wstępie czas na prezentację tego co dają interfejsy z domyślną implementacją w Javie 8. Otóż domyślna implementacja daje nam naprawdę dużo. Z jednej strony zachowujemy swobodę jaką dają interfejsy. Z drugiej otrzymujemy możliwość bezinwazyjnego dodawania metod tak jak w przypadku klas abstrakcyjnych.
Nasz interfejs po zmianie nie będzie miał już podinterfejsu. Otrzyma dodatkową metodę, a jej dodanie nie wpłynie na istniejące implementacje

Listing 4. Interfejs z dodatkową metodą

interface Sort<T extends Comparable<T>>{

	Collection<T> sort(Collection<T> c);

	default <E> Collection<E> sort(Collection<E> c, Comparator<E> comp ){
		throw new UnsupportedOperationException("Default impl");
	};
}

Podsumowanie

Domyślna implementacja w interfejsach wprowadzona w Javie 8 to nie tylko namiastka wielodziedziczenia czy też „coś na wzór scalowych traitów”. To rozwiązanie poważnego problemu projektowego, który bardzo często wpływa na kształt naszego kodu.
Co więcej dzięki temu rozwiązaniu można zrezygnować ze sztucznej hierarchii interfejs + klasa abstrakcyjna, z której wszystko dziedziczy. Takie uproszczenie pozwala na tworzenie API elastycznego zarówno pod względem dowolności implementacji jak i łatwego w rozszerzaniu o nowe możliwości.