SOLIDny tydzień – I jak Interface Segregation
Jako, że dzisiaj piątek to jest to czas na luźniejszą regułę z grupy SOLID. Dzisiaj na tapecie mamy literę I – Interface Segregation Principle, a więc zasadę segregacji interfejsów.
W skrócie
Reguła segregacji interfejsów mówi po prostu, że interfejsy powinny być małe i konkretne, tak aby klasy nie musiały implementować metod, których nie potrzebują. Tak więc koniec z uber-interfejsami, czas na minimalizm.
Czyli, że co?
W temacie segregacji interfejsów myślę, że dobrym przykładem z życia będzie temat przewodów. Wyobraź sobie, że producenci wymyślili super-przewód do komputerów, który zawiera wszystkie możliwe sposoby przesyłu danych i pozwala się wpiąć do wszystkiego. 50-pinowe gniazdko, kabel grubości palca, 18 wtyczek na drugim końcu. Cudo. Co prawda sporo komputerów nie wykorzystuje niektórych wyjść/wejść, np. nie wspierając wyjścia VGA, a jedynie HDMI, ale skoro są w przewodzie to da się podłączyć. Czy jest to wygodne? Absolutnie. Nie dość, że sam przewód w wersji >1m jest sprzedawany z osobną walizką do transportu bo waży tyle co laptop to dodatkowo trzeba się za każdym razem zastanawiać czy wtyczka, którą chce się użyć jest obsługiwana przez komputer i nie wisi wyłączona. Masakra. I to jest przykład tego uber-interfejsu, który zawiera wszystkie potencjalnie potrzebne metody.
Dlatego właśnie w komputerach jest tyle wyjść i wejść różnego typu żeby każdy producent i użytkownik mógł dobrać zestaw pod siebie. Ponieważ producent laptopa wybrał taki, a nie inny zestaw złączy to możemy być pewni, że każde z nich zostało obsłużone i na pewno możemy z niego skorzystać. W dodatku przewody są lekkie i poręczne. Da się ich również użyć w innych urządzeniach niż sam komputer. I to są właśnie te nasze małe interfejsy, o które walczymy. Przy okazji ładnie się tutaj wpasowuje reguła pojedynczej odpowiedzialności, którą omawiałem jako pierwszą – każdy interfejs odpowiada za konkretną funkcjonalność.
Przykłady
Jednym z przykładów dla segregacji interfejsów jest implementacja kolekcji w C#. Jeśli zdarzyło Ci się przekazywać gdzieś zbiory danych do odczytu lub zapisu to na pewno spotkałeś się z interfejsami IEnumberable i ICollection. I być może zauważyłeś (jeśli nie to możesz sprawdzić), że np. Lista implementuje oba te interfejsy bo pozwala na zapis i odczyt danych z niej. Jednak w przypadku kiedy chcemy mieć klasę pozwalającą tylko na odczyt danych po jej utworzeniu (np. tablica) to nie musimy implementować metod do dodawania, zostawiając je puste, bo wystarczy nam interfejs IEnumerable. A dzięki temu, że jest to ten sam interfejs co dla listy to możemy napisać metodę, która będzie potrafiła czytać zarówno z tablicy jak i z listy nie przejmując się który konkretnie typ dostała.
Załóżmy, że mamy w naszej aplikacji raporty. I dla tych raportów przygotowaliśmy interfejs:
interface IReport { void Print(); void SaveAsPdf(); void SaveAsCsv(); }
Teraz implementujemy różnego rodzaju raporty:
class StandardReport : IReport { public void Print() { // drukowanie raportu } public void SaveAsPdf() { // zapis jako PDF } public void SaveAsCsv() { // zapis jako CSV } } class BackendReport : IReport { public void Print() { throw new NotImplementedException(); } public void SaveAsPdf() { // zapis jako PDF } public void SaveAsCsv() { // zapis jako CSV } }
Wszystko fajnie tylko, że jeden z raportów nie potrzebuje metody do drukowania na drukarce, ma być tylko zapisany na dysku. Ale ponieważ nasz interfejs do raportów zawiera wszystko „co może się przydać” to również ta metoda musiała być jakoś obsłużona. Dlatego rzuca zgodnie z prawdą wyjątek o braku implementacji.
A wystarczyło rozdzielić ten interfejs na mniejsze i pozwolić na ich niezależną implementację:
interface IPrintable { void Print(); } interface ISaveableAsPdf { void SaveAsPdf(); } interface ISaveableAsCsv { void SaveAsCsv(); } class StandardReport : IPrintable, ISaveableAsPdf, ISaveableAsCsv { public void Print() { // drukowanie raportu } public void SaveAsPdf() { // zapis jako PDF } public void SaveAsCsv() { // zapis jako CSV } } class BackendReport : ISaveableAsPdf, ISaveableAsCsv { public void SaveAsPdf() { // zapis jako PDF } public void SaveAsCsv() { // zapis jako CSV } }
Dzięki temu nikt nie musi się zastanawiać czy użycie jakiejś metody nie spowoduje rzucenia wyjątku. Daje to też możliwość używania takich interfejsów dla innych klas. Np. IPrintable może być użyty dla wszystkich klas, które mogą być wydrukowane więc możemy na jego podstawie stworzyć kolejkę drukowania nie zastanawiając się co konkretnie ma być wydrukowane.
Leave a Comment