Wstęp
To już czwarty artykuł w serii dotyczącej skrótu SOLID. Jest to zbiór zasad, dzięki którym możemy pisać kod, który łatwiej będzie nam skalować, oraz zmieniać zachowanie naszej aplikacji, bez ruszania kodu dużej części aplikacji.
Na zbiór zasad składają się:
- Single Responsibility Principle,
- Open/Closed Principle,
- Liskov Substitution Principle,
- Interface Segregation Principle,
- Dependency Inversion Principle.
Dzisiaj zajmiemy się Interface Segregation Principle 🙂
Interface Segregation Principle
Credit: https://blog.larapulse.com/clean-code/solid-in-simple-words
Tak jak widać na obrazku – klienci nie powinni być zmuszani do polegania na interfejsach, których nie używają. Innymi słowy lepiej więcej mniejszych interfejsów, niż jeden duży.
Co to oznacza w praktyce?
Spójrzmy na przykład. Załóżmy, że w naszej aplikacji mamy następujący widok:
Jest to tabela z listą użytkowników. Pokazuje ona nazwę, oraz status danego użytkownika. Model został zaimplementowany w poniższy sposób:
1 2 3 4 5 |
export type User = { id: UserId; name: string; status: UserStatus; } |
Nasza aplikacja działa tak, że po podwójnym kliknięciu w wiersz przechodzi do widoku szczegółów:
Pokazujemy tutaj zdecydowanie więcej informacji, niż te, które są na widoku listy. Dlatego moglibyśmy model dostosować tak, aby ten sam był używany dla widoku listy, jak i widoku szczegółów (korzystając z opcjonalnych pól):
1 2 3 4 5 6 7 8 |
export type User = { id: UserId; name: string; status: UserStatus; profilePhoto?: ResourceUrl; email?: string; roles?: UserRole[]; } |
Takie podejście rodzi jednak kilka problemów. Po pierwsze, model dla widoku listy zawiera pola, które tam fizycznie nie występują. Po drugie, mając opcjonalne pola w wielu miejscach w aplikacji, bardzo szybko może się okazać, że nie wiemy gdzie dane pole jest zawsze wymagane, a gdzie nie. Po trzecie, w przypadku refaktoru (np. zmiany odpowiedzi z serwera), nie będziemy wiedzieć, które pola możemy zmienić/usunąć, a które nie.
Dlatego lepiej jest stworzyć oddzielny model dla widoku listy, i dla widoku szczegółów. Wtedy unikniemy opcjonalnych pól, a nasze komponenty będą miały jasno zdefiniowane zakresy wiedzy o modelach, jakie obsługują.
1 2 3 4 5 |
export type UserUiListItem = { id: UserId; name: string; status: UserStatus; } |
I dla widoku szczegółów:
1 2 3 4 5 |
export type UserUiDetail = UserUiListItem & { profilePhoto: ResourceUrl; email: string; roles: UserRole[]; } |
W ten sposób trzymamy się zasady Interface Segregation Principle.
Spójrzmy na inny przykład. Załóżmy że mamy aplikację do obsługi maili i użytkowników. Składa się ona z 2 głównych widoków. Pierwszy to widok administratora, gdzie możemy:
- usuwać,
- dodawać,
- aktualizować użytkowników.
Jest to panel widoczny tylko dla niewielkiej liczby użytkowników. Drugi widok to widok z filtrem wiadomości. W tym miejscu możemy tylko pobrać listę użytkowników lub szczegóły danego użytkownika. Widok ten jest używany przez większość użytkowników aplikacji. Oba widoki korzystają z serwisu do pobierania danych z serwera:
1 2 3 4 5 6 7 |
export interface UserResource { create(user: User): Observable<void>; delete(id: EntityUid): Observable<void>; getAll(): Observable<User[]>; getOne(id: EntityUid): Observable<User | undefined>; update(user: User): Observable<void>; } |
Widać więc, że panel filtrów wiadomości korzysta tylko z 2 metod – “getAll” i “getOne”. Pozostałe są mu zbędne. Idąc za definicją “lepiej mieć więcej interfejsów, ale mniejszych”, powinniśmy tutaj wydzielić podinterfejsy. Jeden, zawierający metody dostępne dla większości użytkowników:
1 2 3 4 |
export interface StandardUserResource { getAll(): Observable<User[]>; getOne(id: EntityUid): Observable<User | undefined>; } |
Oraz drugi, z akcjami wymagającymi odpowiednich uprawnień:
1 2 3 4 5 |
export interface PrivilagedUserResource { create(user: User): Observable<void>; delete(id: EntityUid): Observable<void>; update(user: User): Observable<void>; } |
Potem zamiast używać konkretnej implementacji w obu widokach, powinniśmy wstrzyknąć utworzone interfejsy:
1 2 3 4 5 6 |
@Component() export class MessageFilterComponent { constructor( @Inject(STANDARD_USER_RESOURCE) private standardUserResource: StandardUserResource ) {} } |
1 2 3 4 5 6 7 |
@Component() export class AdminComponent { constructor( @Inject(PRIVILAGED_USER_RESOURCE) private privilagedUserResource: PrivilagedUserResource, @Inject(STANDARD_USER_RESOURCE) private standardUserResource: StandardUserResource ) {} } |
W ten sposób komponenty mają dostęp tylko do tych metod, z których rzeczywiście korzystają. Dzięki temu reguła Interface Segregation Principle jest zachowana.
W następnym artykule zajmiemy się już ostatnią regułą składającą się na SOLID – Dependency Inversion Principle 🙂
Leave a Reply