Interface Segregation Principle – ISP
Teraz omówimy zasadę segregacji interfejsów jest to czwarta zasada SOLID, która opisuje, jak powinniśmy projektować i używać interfejsy w naszych aplikacjach. Zasada ta stanowi, że klienci (klasy) nie powinni być zmuszani do polegania na metodach, których nie używają. Kilka dedykowanych interfejsów jest lepsze niż jeden, który jest zbyt ogólny.
Następstwem tego jest to, że powinniśmy preferować małe, spójne interfejsy od dużych. Zasada ma za zadanie przede wszystkich wyeliminowanie nieporęcznych, niepotrzebnie rozbudowanych interfejsów. Każdy taki interfejs zgodnie z tą zasadą powinien zostać podzielony na mniejsze grupy metod.
Oznacza to, że wiele różnych interfejsów jest lepsze niż jeden duży i zbyt rozbudowany interfejs. Każdy interfejs powinien być tworzony w taki sposób aby zawierał jak najmniejszą ilość metod, czyli metody, które w danej chwili są niezbędne. Nie powinien zawierać metod nadmiarowych a wszystkie inne metody, jeśli nie są związane z konkretnym interfejsem, powinny znaleźć się w odrębnych interfejsach.
Generalnie chodzi o to aby nasze interfejsy które implementujemy były w możliwy sposób jak najbardziej „odchudzone” i zawierały tylko metody najbardziej istotne, z których nasze klasy implementujące będą w danej chwili korzystać. Zasada ta pozwala uniknąć implementowania niepotrzebnej ilości, nadmiarowych metod, z których nie koniecznie będziemy korzystać.
Przeanalizujmy teraz mały przykład.
Załóżmy, że posiadamy pewien interfejs oraz dwie klasy, które go implementują. Klasa Klient implementuje nasz przykładowy interfejs w całości wszystkie jego metody, natomiast klasa Produkt implementuje tylko część z nich, pozostałe, które są niepotrzebne pozostają puste lub zwracają domyślne wartości.
W takiej sytuacji czy powinniśmy polegać na tym ogromnym interfejsie, a co jeśli chcemy stworzyć nową klasę Zamówienie, ale jedyną rzeczą, na której Nam zależy, jest wykorzystanie tylko 1 lub 2 metod z tego interfejsu?
Jeśli użyjemy tego interfejsu, będziemy zmuszeni zaimplementować wszystkie jego metod. Duże interfejsy powodują więcej zależności. Więcej zależności skutkuje większym sprzężeniem, a kod jest wtedy bardziej kruchy z powodu zwiększonego sprzężenia. I zmiany w typach, które implementują takie duże interfejsy wymagają większej liczby testów dalszych zależności, co sprawia, że wdrożenia są trudniejsze i bardziej ryzykowne.
Jeśli mamy do czynienia z taką sytuacją, powinniśmy rozważyć rozdzielenie interfejsu na mniejsze tak, aby każdy z nich deklarował tylko te funkcje, które rzeczywiście są wywoływane przez określonego klienta lub grupę klientów (klasa/grupy klas implementujące dany interfejs).
Przeanalizujmy teraz kolejny przykład.
W przykładzie mamy interfejs IMyIterface, który został zaimplementowany przez klasy Person, Car i Plane. I w naszym przykładzie łamiemy zasadę segregacji interfejsów. Klasa Person nie potrzebuje metod Drive i Fly, mimo to musi takie metody zaimplementować. Klasa Car nie potrzebuje metod Go i Fly, mimo to musi takie metody zaimplementować. Klasa Plane nie potrzebuje metod Drive i Go, mimo to musi w tym przypadku takie metody zaimplementować.
ISP – zły przykład
public interface IMyIterface
{
void Name();
void Drive();
void Go();
void Fly();
}
public class Person : IMyIterface
{
public void Name()
{
Console.WriteLine("I am Person");
}
public void Go()
{
// ... going
Console.WriteLine("Person going");
}
public void Drive()
{
throw new System.NotImplementedException();
}
public void Fly()
{
throw new System.NotImplementedException();
}
}
public class Car : IMyIterface
{
public void Name()
{
Console.WriteLine("I am Car");
}
public void Drive()
{
// ... driveing
Console.WriteLine("Car driveing");
}
public void Go()
{
throw new System.NotImplementedException();
}
public void Fly()
{
throw new System.NotImplementedException();
}
}
public class Plane : IMyIterface
{
public void Name()
{
Console.WriteLine("I am Plane");
}
public void Fly()
{
// ... flying
Console.WriteLine("Plane flying");
}
public void Drive()
{
throw new NotImplementedException();
}
public void Go()
{
throw new NotImplementedException();
}
}
static void Main(string[] args)
{
var collection = new List<IMyIterface>
{
new Person(),
new Car(),
new Plane()
};
foreach (var item in collection)
{
item.Name();
item.Drive(); // Nieobsługiwany wyjątek
}
}
Czyli łamiemy zasadę ISP, łamiąc zasadę segregacji interfejsów, często łamiemy również kilka innych zasad SOLID.
Jeżeli nasze klasy mają więcej niż jedną odpowiedzialność SRP, to również ISP jest łamane – wtedy najczęściej są implementowane zbyt ogólne interfejsy.
Niestosowanie się do reguły ISP, może mieć swoje konsekwencje, gdy iterujemy się po kolekcji obiektów ze wspólnym interfejsem, może zostać rzucony wyjątek, przez co musielibyśmy sprawdzać typ obiektu w klasie. Również narusza to regułę LSP, ponieważ w miejsce bazowego obiektu, nie można podstawić dowolnego obiektu klasy pochodnej, bez znajomości tego obiektu.
Dlatego w tym przypadku IMyIterface jest uważany za zanieczyszczony interfejs. Jeśli zachowamy obecny projekt, nasze klasy są zmuszone do implementowania metod których nie potrzebują.
Zgodnie z zasadą ISP elastyczny projekt nie będzie zawierał zanieczyszczonych interfejsów. Prawdopodobnie domyślasz się już, jak powinna wyglądać prawidłowa implementacja powyższego kodu. W naszym przypadku interfejs IMyIterface powinien być podzielony na 3 różne interfejsy. Powinniśmy podzielić zbyt ogólny interfejs, na kilka bardziej szczegółowych interfejsów.
public interface IName
{
void Name();
}
public interface IDrive
{
void Drive();
}
public interface IGo
{
void Go();
}
public interface IFly
{
void Fly();
}
public class Person : IName, IGo
{
public void Name()
{
Console.WriteLine("I am Person");
}
public void Go()
{
// ... going
Console.WriteLine("Person going");
}
}
public class Car : IName, IDrive
{
public void Name()
{
Console.WriteLine("I am Car");
}
public void Drive()
{
// ... driveing
Console.WriteLine("Car driveing");
}
}
public class Plane : IName, IFly
{
public void Name()
{
Console.WriteLine("I am Plane");
}
public void Fly()
{
// ... flying
Console.WriteLine("Plane flying");
}
}
class Program
{
static void Main(string[] args)
{
var collection = new List<IGo>
{
new Person(),
};
foreach (var item in collection)
{
item.Go();
}
}
}
Jeśli projekt jest już wykonany, grube interfejsy można oddzielić za pomocą wzorca Adapter.
Jak każda zasada, zasada ISP wymaga dodatkowego czasu i wysiłku, aby ją zastosować i zwiększa złożoność kodu, ale tworzy elastyczny projekt. Jeśli zamierzamy zastosować ją w większym stopniu niż to jest konieczne, powstanie wtedy kod zawierający wiele interfejsów z pojedynczymi metodami, czyli stosowanie tej zasady powinno być oparte na doświadczeniu i zdrowym rozsądku w identyfikowaniu obszarów, w których rozszerzenie kodu jest prawdopodobne w przyszłość.
Bardzo podobał mi się twój post na blogu. Jeszcze raz wielkie dzięki. Bardzo fajny.
Dzięki.