Czwarta zasada SOLID dotyczy interfejsów. Mimo, że Python nie posiada dla nich typowej składni, to ta zasada jest równie ważna i w tym języku.
SOLID to:
- Single Responsibility Principle [SRP]
- Open-Closed Principle[OCP]
- Liskov Substitution Principle [LSP]
- Interface Segregation Principle [ISP] – Temat tego postu
- Dependency Inversion Principle [DIP]
Czym właściwie jest interfejs?
Interfejs to typ podobny do klasy. Definiuje jakie metody muszą być zaimplementowane w klasie (sygnatury metod), która z niej korzysta. Jednak nie definiuje ich implementacji, tylko wymaga aby zrobiła to klasa korzystająca z interfejsu. Python nie zawiera tej konstrukcji w swoim arsenale.
O czym mówi zasada interface segregation?
Podczas prac nad kodem do drukarek w firmie Xerox, Robert C. Martin miał do czynienia z jedną wielką klasą robiącą wszystko. Niewielkie zmiany były trudne do wprowadzenia. Wtedy Robert wpadł na pomysł podzielenia klasy ze względu na funkcjonalności przy użyciu interfejsów.
Zasada jest dość prosta: “Klient nie powinien być zmuszony do polegania na interfejsie, którego nie używa“.
Innymi słowy, klasa używająca dany interfejs powinna implementować wszystkie jego metody. Jeśli klasa zależy od interfejsu, którego nie używa w całości, podczas gdy inne klasy to robią, pierwsza klasa staje się powiązana i zależna od zmian spowodowanych przez klasy implementujące cały interfejs. Chcemy unikać jakichkolwiek niepotrzebnych powiązań w kodzie.
Powinno unikać się rozszerzania wspólnego interfejsu tylko dlatego aby dodać sygnaturę metody dla jednej klasy korzystającej z tego interfejsu, podczas gdy inne tego nie potrzebują. To powoduje, że interfejs staje się niepotrzebnie duży. Poprawiając na przykład błąd w tej sytuacji, zmiana interfejsu dotyka wszystkie klasy z niego korzystające. A to bardzo zła zależność.
Interface separation vs single responsibility principle
Jeśli kojarzysz pierwszą zasadę SOLID – Single Responsibility Principle – to prawdopodobnie wydaje Ci się ona nieco podobna do ISP. Rzeczywiście tak jest. Więc jaka jest różnica? Najlepiej zacytować samego autora(link).
A więc, ISP twierdzi aby nie polegać na rzeczach, których nie potrzebujemy. Natomiast SRP mówi o grupowaniu razem rzeczy, które zmieniają się z tego samego powodu. Różnica jest delikatna.
Robert C. Martin dodał również prosty przykład ukazujący tę różnicę. Załóżmy, że posiadamy klasę stosu implementującą dwie metody: pop oraz push. Klient polegający na interfejsie tego stosu i korzystający tylko z metody push, polega również na metodzie pop, której nie potrzebuje. SRP w tym przypadku nie rozdziela metod pop i push, podczas gdy ISP zachęca do tego podziału.
ale czy to w ogóle ma zastosowanie w Pythonie?
Tak 🙂 Prawda, w Pythonie nie uświadczymy składni dla interfejsów. Python pozwala natomiast na implementację klas abstrakcyjnych(przy użyciu biblioteki abc).
Z klasy abstrakcyjnej nie można utworzyć obiektu. Definiuje ona metody, które muszą zostać zaimplementowane we wszystkich klasach po niej dziedziczących (metody abstrakcyjne dekoruje się dekoratorem @abstractmethod
wewnątrz klasy abstrakcyjnej). Przy ich użyciu możemy tworzyć à la interfejsy.
Dziedzicznie po klasie abstrakcyjnej łączy się ściśle z ostatnio opisywaną zasadą Liskov Substitution Principle. Załóżmy, że mamy dużą klasę abstrakcyjną z wieloma metodami abstrakcyjnymi oraz klasę dziedziczącą, która nie potrzebuje implementować metody X
. Musi jednak to zrobić, ponieważ wymaga tego dekorator @abstractmethod
. Częstym obejściem jest dodanie pustej implementacji tej metody, co jest złą praktyką. Dlaczego? Ponieważ łamie to zasadę podstawiania Liskov!
W takim przypadku warto pomyśleć o podzieleniu bazowej klasy abstrakcyjnej na kilka innych, kierując się funkcjonalnością. Dobrym przykładem jest podział klas ReadableFile
oraz WritableFile
w poprzednim artykule.
typowane python protocols
Jako ciekawostkę można wspomnieć o protokołach w Pythonie.
Python to język dynamicznie typowany, który wspiera min. podtypowanie strukturalne(obok podtypowania nominalnego). W dużym skrócie dzięki temu rodzajowi podtypowania porównujemy obiekty nie według typu, a metod/pól które implementuje(struktury 🙂 ). Ważne jest co dany obiekt robi, a nie jakiego jest typu/po jakiej klasie dziedziczy. Protokół w Pythonie to niejawny kontrakt ustalający jakie elementy musi mieć obiekt aby implementować dany protokół. Przykładowo protokół iteratora wymaga dwóch elementów: __iter__ oraz __next__.
Protokół różni się od tradycyjnego interfejsu(np. z Javy) tym, że nie wymaga wyraźnej implementacji, ponieważ Python opiera się na duck typingu – więc dla przykładu powyżej możliwe jest aby __iter__ był polem klasy, a niekoniecznie metodą. Protokół również może zawierać domyślną implementację oraz trzymać stan.
Python 3.8 wprowadził oprócz nowego operatora przypisania również wsparcie dla typowania protokołów . Jesteśmy więc w stanie używać statycznego typowania dla protokołów ‘prawie’ jak interfejs, nie musząc dziedziczyć po klasie abstrakcyjnej w celu zastosowania protokołu. Jednak jest to raczej mało przejrzyste.
Nie przesadzaj z rozdzielaniem interfejsów!
Pamiętajmy, aby nie popadać w skrajności w skrajność tj. zbyt przesadne rozdzielanie również nie jest dobrym wyjściem.
To czwarta, przedostatnia już zasada SOLID. Dotyczy ona niejako zastosowania Single Responsibility dla interfejsów. Równocześnie ściśle łączy się zasadą Barbary Liskov. W następnym artykule przejdziemy do ostatniej reguły – Dependency Inversion Principle. Do zobaczenia.