SOLID - ISP - Reguła Segregacji Interfejsów
Nadszedł czas na omówienie reguły segregacji interfejsów. Jest to już przedostatnia reguła jaka nam została do omówienia .
Do rzeczy
ISP (Interface Segregation Principle) - Reguła, która mówi, że wiele dedykowanych interfejsów jest lepszą opcją niż jeden ogólny. A klient nie powinien być zmuszony do implementacji interfejsu którego nigdy nie użyje.
Reguła ta może wydawać się skomplikowana - lecz jest naprawdę bardzo prosta w zrozumieniu .
Sens
Tworząc interfejs, nie powinniśmy nigdy dopuścić do sytuacji, w której choć jedna z klas będzie zmuszona do implementacji choć jednej metody której nie będzie wykorzystywać. Należy wówczas rozdzielić ten interfejs na kilka mniejszych.
Poszerzając
Klasa w której używamy/akceptujemy inną klasę, ma o niej wiedzę i jest od niej zależna (są to zależności tej klasy). W konsekwencji klasa ta również jest zależna od wszystkich zależności tej klasy (jeżeli takowe istnieją).
I jak możesz sobie wyobrazić - nie jest to sytuacja idealna.
Zbyt dużo wycieka nam tutaj wiedzy.
Jak może zauważyłeś, wszystkie te zasady SOLID sprowadzają się do tematu wiedzy. Wiedzy jaką jeden obiekt ma o drugim obiekcie.
Idąc dalej
Powinniśmy dostarczać jak najbardziej konkretne interfejsy (spełniające SRP). Które z kolei pozwolą nam nie polegać na całych klasach oraz na klasach które są używane przez te klasy.
Nie zagłębiać się w całą tę wiedzę. Zamiast tego jedynie polegać na konkretnej funkcjonalności której potrzebujemy (w danej klasie/metodzie) i minimum wiedzy (i zależności).
W połączeniu z regułą otwarte-zamknięte (OCP) powinniśmy zrobić taki konkretny interfejs, który pozwoli nam uniknąć sytuacji w której musielibyśmy znać typ obiektu, który został przekazany (mieć o nim wiedzę).
Zamiast tego powinniśmy pozwolić temu obiektowi zająć się całą logiką - a nam dostarczyć polimorficzny interfejs.
Zależności
Nie powinniśmy polegać na konkretnych klasach tylko na konkretnej funkcjonalności. Tym sposobem ulepszamy architekturę i zmniejszamy zależności.
W taki sposób moglibyśmy (jako klienci / klasa) używać jakiegokolwiek obiektu który implementuje konkretny (funkcjonalny) interfejs bez konieczności polegania na jakiejkolwiek konkretnej implementacji klasowej. Poleganie na funkcjonalnym interfejsie który implementuje dana klasa zamiast na całej klasie.
To klucz do zrozumienia całości.
Przykład
Złamanie reguły ISP (+OCP)
<?php
interface WorkerInterface
{
public function work();
public function sleep();
}
class HumanWorker implements WorkerInterface
{
public function work()
{
return 'Human working.';
}
public function sleep()
{
return 'Human sleeping.';
}
}
class AndroidWorker implements WorkerInterface
{
public function work()
{
return 'Android working.';
}
public function sleep()
{
/*
Problem ISP: Android nie potrzebuje snu, więc ta metoda
jest niepotrzebna choć jest wymuszona przez interfejs
*/
return null; //
}
}
class Captain
{
public function manage(WorkerInterface $worker)
{
$worker->work();
/*
Problem OCP: Aby uniknąć wywołania metody sleep
na obiekcie typu AndroidWorker
musielibyśmy złamać regułę OCP poprzez dodanie
sprawdzenia typu (instanceof).
*/
$worker->sleep();
}
}
Przestrzeganie reguły ISP (+OCP)
<?php
interface ManagableInterface
{
public function beManaged();
}
interface WorkableInterface
{
public function work();
}
interface SleepableInterface
{
public function sleep();
}
class HumanWorker implements WorkableInterface, SleepableInterface, ManagableInterface
{
public function work()
{
return 'Human working.';
}
public function sleep()
{
return 'Human sleeping.';
}
public function beManaged()
{
$this->work();
$this->sleep();
}
}
class AndroidWorker implements WorkableInterface, ManagableInterface
{
public function work()
{
return 'Android working.';
}
public function beManaged()
{
$this->work();
}
}
class Captain
{
public function manage(ManagableInterface $worker)
{
$worker->beManaged();
}
}
Konsekwencje łamania zasady ISP
Nie stosowanie się do tej reguły może doprowadzić do sytuacji, w której zamiast polegać na danej funkcjonalności (jak powinniśmy) to polegamy na klasie która (potencjalnie) jest zależna od innej klasy i co za tym idzie jesteśmy zależni również od tej innej klasy...
Może się to potem przełożyć na efekt fali. Gdy jedna mała zmiana w jednej części aplikacji wpłynie na inną część aplikacji która jest od niej zależna.
Inne spojrzenie
A patrząc na to dalej z punktu wiedzy - dlaczego klient (jakaś klasa) musi mieć wiedzę o jakiejś klasie na której - jeszcze - polega inna klasa? Dlaczego? I odpowiedź jest prosta - naprawdę nie musi. Musi tylko przyjąć jakiś dowolny obiekt który dostarczy niezbędną mu do działania funkcjonalność.
I tylko tyle ta klasa musi wiedzieć, tylko na tym polegać.
Dlatego wymagać należy potrzebnej nam do działania funkcjonalności zamiast całej konkretnej klasy która ją dostarcza. Wówczas klient w ogóle nie dba (niemalże) o to co mu damy.
Dba tylko oto, czy to co mu dajemy spełnia jego oczekiwana pod względem funkcjonalnym - co jest niezbędne do poprawnego działania. Coś (obiekt) co akceptuje wymagany przez niego interfejs.
Mniejsze lepsze niż większe
Pamiętaj, że interfejsy z pojedynczymi metodami są w 100% w porządku. Czasami to nawet najlepsze wyjście. Bywa że metoda potrzebuje po prostu jednej konkretnej funkcjonalności - a nie całej klasy. Takim interfejsem jej to zapewnimy.
Dużo gorsze są duże (grube) interfejsy, w których bardzo łatwo złamać regułę SRP (pojedynczej odpowiedzialności). Powinniśmy unikać takich interfejsów. Więc jak widzisz ponownie - wszystkie zasady SOLID są ściśle powiązane .
Podsumowując
Nie róbmy metod które polegają na całych konkretnych klasach oraz ich zależnościach. Nie powinniśmy tego robić - jest bardzo złe podejście do projektowania.
I to już wszystko na temat reguły ISP .
Przed nami została już tylko jedna reguła do omówienia... Ale to w następnym artykule .
Zapraszam do komentowania oraz lektury innych moich artykułów .
Dobrego dnia.