S.O.L.I.D.ne programowanie – część 4, czyli apartheid
S.O.L.I.D.ne programowanie – część 0, czyli wstęp
S.O.L.I.D.ne programowanie – część 1, czyli monogamia
S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się
S.O.L.I.D.ne programowanie – część 3, czyli podkładamy świnię
Witam w czwartej części cyklu S.O.L.I.D.ne Programowanie. Dzisiejszy temat zajęć to Interface Segregation Principle (ISP).
Co mnie to obchodzi?
Często gdy trafiamy na jakiś interfejs pierwszą myślą jest po kiego wała oni tu wpakowali tyle metod? Nadmiernie rozbudowana lista metod zaciemnia interfejs i powoduje, że jest on trudny w użyciu. ISP mówi nam, że:
Klient nie powinien być uzależniony od interfejsów z których nie korzysta.
Mówiąc prościej klient powinien operować na interfejsie zawierającym tylko potrzebne mu metody.
Nietzscheańska idea nad interfejsu
Jako, że sam popełniam ten błąd to zacznę od określenia czym jest super interfejs.
Super Interfejs jest to interfejs, który skupia w sobie wszystkie potrzebne w danej chwili metody i nie uwzględnia ich zadań. Inaczej mówiąc jest to interfejs, który łamie zasadę SRP. Zdefiniujmy przykładowy interfejs Robotnik:
Listing 1. Super interfejs – Robotnik
package pl.koziolekweb.solid.isp;
public interface Robotnik {
public void pracuj();
public void pobierzWyplatę();
}
Przyjrzyjmy się teraz dwóm innym interfejsom, które korzystaja z naszego robotnika. Manager i Księgowy.
Listing 2. Interfejsy klienckie – Manager i Księgowy
package pl.koziolekweb.solid.isp;
public interface Manager {
public void dodajPracownika(Robotnik robotnik);
public void zarządzaj();
}
// .... //
package pl.koziolekweb.solid.isp;
public interface Księgowy {
public void przyjmijNaStan(Robotnik robotnik);
public void wypłaćPensję();
}
Od razu widać co jest nie tak?
Czy roboty mają uczucia?
Dobre pytanie. Przypowieść programistyczna na temat ISP. Dawno dawno temu była sobie fabryka. W fabryce pracowali robotnicy, managerowie i księgowi. Robotnicy wykonywali swoje zadania, managerowie kontrolowali robotników i zlecali księgowym wypłacanie pensji. Jednak któregoś dnia prezes postanowił, że część prac wykonywać będzie robot. Zgodnie z procedurą robot został wciągnięty na listę nadzoru managerskiego i był pod opieką jednego z managerów. Gdy nadszedł dzień wypłaty manager przekazał listę swoich pracowników do księgowych, a ci rozpoczęli wykonywanie przelewów. Nastąpiła jednak konsternacja wielka wśród księgowych bo nikt nie wiedział jak poradzić sobie z problemem robota. Z jednej strony był on pracownikiem i należało mu się wynagrodzenie, a z drugiej jednak był tylko maszyną… konsternacja trwała tak długo, że inni robotnicy nie otrzymali swych wypłat, spalili opony przed siedzibą zarządu fabryki, a ta upadła.
Jedynie robot zaśmiał by się z zaistniałej sytuacji oczywiście gdyby miał uczucia.
Zmiana punku widzenia
Gdy zapytasz się twórcy interfejsu co miał na myśli tworząc interfejs Robotnik najprawdopodobniej usłyszysz, że robotnik przecież pracuje i otrzymuje wypłatę. Jeżeli jednak popatrzymy na ten problem z punktu widzenia managera to ważna jest tylko praca, ale znowu punkt widzenia księgowego bierze pod uwagę tylko wypłatę wynagrodzenia. Interfejs powinien definiować metody związane z zadaniem, ale z punktu widzenia użytkownika tego zadania, a nie klasy implementującej. Bardzo dobrym przykładem jest tu interfejs Comparable i jego użycie w narzędziach do sortowania kolekcji. Metoda sortująca żąda tylko czegoś co jest Comparable, czyli ma tylko jedną metodę compareTo. Nie jest dla narzędzia istotne jakie inne zadania ma obiekt. Ma tylko mieć możliwość porównania się z innym obiektem.
Naprawiamy świat
Interfejs Robotnik jest zły. Ponieważ klasa, która będzie go implementować MUSI uwzględniać zarówno fakt, że robotnik pracuje jak i fakt, że otrzymuje pensję. Jeżeli na stan wprowadzimy klasę Robot, której obiekty nie będą wymagały pensji, ale będą pracowały to albo księgowy będzie musiał jakoś sobie poradzić z błędami albo programista będzie musiał jakoś tak zaprojektować robota, żeby ten przyjmował bezpieczną pensję (np. 0). Rodzi to kolejne trudności jak na przykład uwzględnienie przy wypłacie grupy zaszeregowania, wysługi lat, amortyzacji itp. Problem taki będzie propagował się na inne części systemu, a lokalnie tworzone obejścia będą tylko utrudniały utrzymanie kodu.
Prawidłowym rozwiązaniem jest rozbicie interfejsu Robotnik na dwa interfejsy. Pracujący i Opłacany:
Listing 3. Interfejsy – Pracujący i Opłacany
package pl.koziolekweb.solid.isp;
public interface Pracujący {
public void pracuj();
}
// .... //package pl.koziolekweb.solid.isp;
public interface Opłacany {
public void pobierzWyplatę();
}
Zmianie ulegną oczywiście interfejsy Manager i Księgowy.
Listing 4. Interfejsy klienckie – Manager i Księgowy, nowa wersja
package pl.koziolekweb.solid.isp;
public interface Manager {
public void dodajPracownika(Pracujący robotnik);
public void zarządzaj();
}
// .... //
package pl.koziolekweb.solid.isp;
public interface Księgowy {
public void przyjmijNaStan(Opłacany robotnik);
public void wypłaćPensję();
}
Oczywiście te zmiany to tylko początek. Kolejnymi powinno być pozmienianie klas fabrykujących poszczególne rodzaje obiektów tak by roboty i ludzie trafiali do odpowiednich kolejek. Refaktoryzacja kodu, którą powoduje ISP zazwyczaj jest bardzo głęboka. Dlatego też należy stosować segregację interfejsów już na samym początku, a jeżeli zachodzi konieczność podziału interfejsu należy robić to natychmiast po zauważeniu tak by zmiany dotknęły jak najmniejszą część systemu.
Inne zalety
ISP powoduje tworzenie dużej ilości małych interfejsów (w skrajnym przypadku jedna metoda na interfejs) oraz rozwlekanie się definicji metod o kolejne elementy do implementacji. Choć wydaje się to głupie to takie rozwiązanie ma same zalety. Po pierwsze kod kliencki zawsze dostanie to czego potrzebuje w danej chwili. Po drugie nic nie broni łączyć interfejsów:
Listing 5. Interfejs połączony – RobotnikCzłowiek
package pl.koziolekweb.solid.isp;
public interface RobotnikCzłowiek extends Pracujący, Opłacany {
}
Jeżeli zachodzi taka potrzeba. Jest to szczególnie przydatne w klasach fabrykujących, które pracują według wzorca budowniczego. Dostarczają one potrzebny element zachowując jednocześnie wszystkie właściwości jego części. Po trzecie znacznie ułatwia to pisanie testów jednostkowych. Testowaniu podlega wtedy prosty interfejs co pozwala na znacznie lepsze napisanie przypadków testowych uwzględnienie różnych danych skrajnych i uzyskanie lepszego pokrycia kodu. Test staje się prawdziwie jednostkowy ponieważ testuje najmniejszą jednostkę kodu jaką jest metoda. Po czwarte podobnie jak z testami sprawa ma się z dokumentacją. Łatwiej jest udokumentować działanie pojedynczej metody. Można dokładnie opisać co robi, niż napisać dokumentację dla złożonego interfejsu w której uwzględniamy kontekst wywołania czy wpływ innych nie powiązanych metod (opis jak działa wypłacanie pensji dla robota skoro nie powinien on w ogóle pensji dostawać).
ISP jest jedną z najprzydatniejszych zasad programowania obiektowego i warto ją stosować zawsze i wszędzie.