Jak mniej kompilować przy pomocy SOLID (na przykładzie Javy)
Opowiem tutaj tylko o jednej z zasad SOLID - i postaram się nie zanudzić Cię teorią. Ostatnio ponownie trafiłem na SOLID i chcę Ci pokazać kilka świetnych rzeczy, których tam wcześniej nie widziałem. Przenieśmy się trochę do przeszłości i stwórzmy od zera projekt Gradle. Napiszemy go w Javie. Nie wprowadzamy żadnej dodatkowej konfiguracji budowania. Strukturę projektu możesz zobaczyć na poniższym diagramie:
W momencie pisania tego artykułu najnowszą wersją Gradle było v6.8.3. Funkcję kompilacji przyrostowych włącza się domyślnie przy pomocy wtyczki Javy.
Wyobraźcie sobie, że Client1
korzysta tylko z metody m1
, Client2
z metody m2
, a Client3
korzysta z metody m3
klasy Service
. Jak myślicie, co się stanie, jeśli zmienimy ciało funkcji m3
i zrekompilujemy nasz projekt?
Można pomyśleć, że gdy zmiana ciała metody jest zmianą kompatybilną binarnie, to tylko Service
zostanie ponownie skompilowane. Niestety to nie tak. W rzeczywistości cały projekt ulega ponownej kompilacji.
Ci, którzy załapali, o co chodzi, mają mój szacunek.
Abyśmy mogli zobaczyć wynik bezpośrednio z Gradle, korzystamy z opcji listFiles
do kompilacji w Javie - loguje ona nasze pliki do skompilowania.
tasks.withType(JavaCompile) {
options.listFiles = true
}
Uruchamiamy kompilację plików źródłowych Java za pomocą kompilatora JDK, wywołując zadanie compileJava, które jest zwykle wykonywane przy każdym zadaniu kompilacji lub asemblacji.
./gradlew compileJava
Widzimy teraz, że wszystkie pliki źródłowe zostały ponownie skompilowane!
> Task :compileJava
Source files to be compiled:
/ISP_java/src/main/java/com/jurcik/ivet/service/Service.java
/ISP_java/src/main/java/com/jurcik/ivet/client/Client1.java
/ISP_java/src/main/java/com/jurcik/ivet/client/Client2.java
/ISP_java/src/main/java/com/jurcik/ivet/client/Client3.java
Dlaczego się tak dzieje?
Z Gradle nie dzieje się nic złego. Otrzymany wynik to oczekiwane zachowanie Javy. Kompatybilność binarna nie ma wpływu na listę skompilowanych plików źródłowych.
Wszystkie deklaracje, które użytkownicy muszą zaimportować w kodzie źródłowym, tworzą zależność kodu źródłowego. Kod źródłowy Client1
zależy zatem od metod m2
i m3
, mimo że ich nie wywołuje. Zależność ta oznacza, że zmiana kodu źródłowego m2
lub m3
w Service spowoduje rekompilację Client1
, pomimo że ten nie zwraca uwagi na zmiany.
Interface Segregation Principle nam pomoże
Interface Segregation Principle (ISP) polega na tym, że żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa.
Postępujmy zgodnie z tą zasadą, tworząc oddzielny interfejs dla każdej metody. Każdy interfejs udostępnia danemu elementowi Client
tylko to, czego ten klient naprawdę potrzebuje. Strukturę ilustruje poniższy diagram.
Przyjrzymy się temu samemu pytaniu jeszcze raz. Jak myślisz, co się stanie, jeśli zmienimy ciało metody m3 od klasy Service, która jest zmianą kompatybilną binarnie, i ponownie skompilujemy projekt?
./gradlew compileJava
No i proszę!
> Task :compileJava
Source files to be compiled:
/ISP_java/src/main/java/com/jurcik/ivet/service/Service.java
Service
jest jedyną rzeczą, która została ponownie skompilowana - wszystko dzięki wprowadzeniu interfejsów.
Jak to wygląda w Kotlinie?
Kompilator Kotlina jest wystarczająco inteligentny, aby rekompilować tylko dotknięte pliki, śledząc zmiany Application Binary Interface („API” dla plików binarnych - więcej o tym procesie przeczytasz tutaj). Nie oznacza to jednak, że zasada ta jest przestarzała! Kompilator to tylko ułatwia. W każdym razie dobrze jest wiedzieć, że trzeba włożyć w to trochę więcej pracy.
Podsumowanie
Każdy programista, który zna programowanie obiektowe, pewnie słyszał o zasadach SOLID. Towarzyszą nam na co dzień, czasem nawet, jak tego nie widzimy. Pomimo że wszyscy mamy je z tyłu głowy, to warto jest poświęcić im trochę więcej czasu. Bo zasady SOLID mają znaczenie i nie kończą się na architekturze systemu.
Oryginał tekstu w języku angielskim możesz przeczytać tutaj.