SOLID: Interface Segregation
Dzisiaj kontynuujemy serię SOLID. Przyszła pora na intuicyjną zasadę Interface Segregation. Nazwa pewnie już podpowiada Ci co będziemy segregować więc do dzieła.
Interface Segregation
Idea zasady jest prosta: unikajmy projektowania struktur, które zmuszają implementujące je klasy do posiadania nadmiarowych pól. Powinniśmy tworzyć dedykowane interfejsy, zawierające tylko te metody, które są niezbędne dla konkretnego klienta.
Innymi słowy, zamiast tworzyć jeden "wszechstronny" scyzoryk szwajcarski, który zawiera wszystkie metody, lepiej jest mieć kilka małych, dedykowanych narzędzi, skierowanych specyficznie do potrzeb każdego klienta.
Przykład z życia
Wyobraź sobie że projektujesz sklep internetowy handlujący elektroniką. W ofercie są laptopy, telefony, ale również słuchawki, drukarki itp. Podstawowym budulcem mógłby być Product i całkiem możliwe, że udałoby Ci się oprzeć na nim implementację wszystkich przedmiotów.
Weź jednak pod uwagę że drukarka będzie miała nieco inne parametry niż laptop. Tutaj pojawiają się pierwsze komplikacje. Jeśli postanowisz wrzucić wszystkie swoje produkty do jednego “worka” to implementacja może wyglądać następująco:
interface Product { brand: string; price: number; weight: number; operatingSystem: string; batteryLife: number; RAMSize: number; screenSize: number; cameraMegaPixels: number; call(number: string): void; printSpeed: number; isColorPrintAvailable: boolean; print(document: string): void; type: "over-ear" | "on-ear" | "in-ear"; hasMicrophone: boolean; connectionType: "wired" | "bluetooth" | "both"; maxVolume: number; adjustVolume(level: number): void; }
Oczy bolą od samego patrzenia, nie wspominając już o tym, że każdy dodatkowy przedmiot zacznie nadwyrężać scroll Twojej myszy, tak, że nawet panoramiczny monitor tego nie ogarnie.
Oprócz tego, takie podejście naraża nas na błędy, ponieważ telefon nie drukuje, a drukarka nie dzwoni. Dlatego ktoś może przez pomyłkę użyć złych pól i metod w nieodpowiednich miejscach, co może wprowadzić zamieszanie w naszym sklepie.
Rozwiązanie
W jaki sposób podejść do rozwiązania tego problemu?
Najlepiej wyciągnąć część wspólną dla wszystkich produktów i utworzyć interfejs bazowy, np. taki jak ten poniżej:
interface Product { brand: string; price: number; weight: number; }
Następnie możemy “uszyć na miarę” metody oraz właściwości dla każdego urządzenia osobno i optymalnie je dopasować
interface Laptop extends Product { operatingSystem: string; batteryLife: number; RAMSize: number; } interface Smartphone extends Product { screenSize: number; cameraMegaPixels: number; call(number: string): void; } interface Printer extends Product { printSpeed: number; isColorPrintAvailable: boolean; print(document: string): void; } interface Headphones extends Product { type: "over-ear" | "on-ear" | "in-ear"; hasMicrophone: boolean; connectionType: "wired" | "bluetooth" | "both"; maxVolume: number; adjustVolume(level: number): void; }
Teraz każdy produkt można w elastyczny sposób dostosowywać do zmieniających się potrzeb. Na wypadek dodania do naszej oferty nowego przedmiotu wystarczy utworzyć nowy interfejs bez konieczności modyfikacji starego kodu.
Brzmi znajomo?
Właśnie upiekliśmy 2 pieczenie na 1 ogniu, zachowując zgodność z zasadą Open Closed. Pisaliśmy o niej w tym artykule
Podsumowanie
Zasada jest niezwykle intuicyjna, a jej zastosowanie przynosi liczne korzyści. Całość można łatwo podsumować jednym zdaniem:
“Nie ma co wciskać wszystkiego do jednego interfejsu, bo skończy się na tym, że Wasz laptop zacznie drukować, a smartfon parzyć kawę.”