SOLID: Open-Closed

Cover Image for SOLID: Open-Closed
Paweł
Paweł

W poprzednim wpisie zrobiliśmy wprowadzenie, dotyczące słynnego mnemonika SOLID. W ramach niego omówiliśmy pierwszą zasadę, czyli Single Responsibility. Dzisiaj bierzemy na tapet kolejną, czyli Open-Closed. Co będziemy otwierać, a co zamykać? Tego dowiecie się w tym wpisie.

Open-Closed

Druga zasada (prawie jak u Newtona) mówi nam o tym, że klasy, funkcje, moduły i tym podobne składowe, powinny być otwarte na rozszerzenie, ale zamknięte na modyfikacje. Co to dokładnie oznacza?

  • Otwartość na rozszerzenie: wspomniane wyżej elementy systemu powinny być projektowane w taki sposób, aby można było je rozszerzać bez konieczności wprowadzania zmian w istniejącym kodzie. Innymi słowy, jeśli chcemy dodać nową funkcjonalność, nie powinniśmy być zmuszeni do modyfikowacji istniejącego kodu. Zamiast tego powinniśmy móc napisać nowy, który rozszerza istniejący system.

  • Zamknięcie na modyfikację: raz napisany kod powinien być stabilny i nie wymagać częstych zmian. Jeśli modyfikujemy istniejący fragment, aby dodać nową funkcjonalność, pojawia się ryzyko wprowadzenia błędów lub zakłócenia działania części systemu. Dzięki zasadzie Open-Closed tworzony kod, jest "zamknięty" i nie zmienia się zbyt często.

Jakie korzyści da Ci wdrożenie tej zasady w swoim kodzie?

  • Utrzymywalność Kod, którego istniejące fragmenty nie są często modyfikowane, jest bardziej stabilny. Utrzymanie takiego systemu staje się łatwiejsze. Zasada Open-Closed minimalizuje konieczność ingerencji w istniejący kod, co znacznie ułatwia konserwacje oprogramowania. Dodatkowo występuje tu pewna symbioza i promowanie zasady pojedynczej odpowiedzialności. Jeżeli element systemu pełni tylko jedną rolę tym mniejsza szansa, że będzie wymagał modyfikacji.

  • Współpraca Programiści w zespole mogą pracować nad różnymi częściami projektu, nie martwiąc się o to, że ich zmiany spowodują konflikty z innymi fragmentami kodu. To ułatwia współpracę i zwiększa produktywność zespołu.

  • Testowalność Kod, który nie ulega częstym zmianom, jest również łatwiejszy do testowania. Zmniejsza się kruchość testów (mała modyfikacja kodu powoduje konieczność poprawy wielu testów automatycznych), ponieważ nie trzeba ich często aktualizować/poprawiać w wyniku zmian w istniejącym kodzie.

  • Elastyczność Dzięki zasadzie Open-Closed system staje się bardziej elastyczny i łatwiej go przystosować do szybko zmieniających się wymagań projektowych. Nowe funkcje można dodawać bez obawy o uszkodzenie istniejących elementów.

Minusy

  • Trudność wdrażania do istniejącej bazy kodu Wprowadzenie OCP w istniejącym oprogramowaniu może być trudne, szczególnie jeśli nie zostało ono zaprojektowane z myślą o tej zasadzie od samego początku. Wymaga to czasu i wysiłku na zrozumienie i zmodyfikowanie istniejącego kodu.

  • Nadmierna abstrakcja Nadmiernie abstrakcyjny kod może być trudny w zrozumieniu, a nawet prowadzić do błędów w projektowaniu. Zdrowy rozsądek przy zastosowaniu OCP pomoże uniknąć nadmiernego rozdrobnienia kodu na zbyt wiele małych klas i interfejsów.

Implementacja

Chcesz sprawić, żeby twój kod był zgodny z zasadą Open-Closed ?

Możesz spróbować użyć następujących technik:

  • Zastosowanie interfejsów: Za pomocą interfejsów, definiujemy kontrakt, który może zostać zaimplementowany za każdym razem przez nowo utworzone klasy. Umożliwia to dodawanie nowych funkcjonalności bez ingerencji w istniejący kod.

  • Dziedziczenie i polimorfizm: Wykorzystanie dziedziczenia i polimorfizmu w językach programowania pozwala na tworzenie nowych klas, które rozszerzają istniejące klasy, bez potrzeby ich modyfikacji.

  • Wzorce projektowe: Wzorce, takie jak Strategia, Dekorator czy Fabryka to twoi sprzymierzeńcy w drodze do osiągnięcia zgodności z zasadą Open-Closed.

Techniki, technikami, ale najlepiej zobaczyć to na przykładzie. Spróbujmy zaimplementować OCP w naszym kodzie za pomocą interfejsów.

Wyobraźmy sobie, że mamy do napisania system, który służy do notyfikowania użytkownika za pomocą SMSów.

Model klasy SMS wygląda następująco:

class Sms { message: string; constructor(message: string) { this.message = message; } send() { // logika związana z wysyłaniem wiadomości sms } }

Jak widać mamy tutaj tylko jedno pole message, które służy do przechowywania informacji dla użytkownika oraz metodę send.

Teraz przejdźmy do klasy odpowiedzialnej za powiadamianie użytkownika:

class NotificationService { sendNotification(sms: Sms) { // logika wysyłania notyfikacji sms.send() } }

Jak widać powyższy serwis przyjmuje jeden parametr, który jest obiektem klasy Sms. Następnie w metodzie sendNotification wywołuje jego metodę send.

Co jest nie tak z tym kodem?

W każdej chwili wymagania mogą ulec zmianie i będziemy chcieli powiadamiać użytkownika również innymi kanałami, takimi jak np. powiadomienie push, e-mail itd. Będzie to wymagało modyfikacji klasy NotificationService w celu dodania kolejnej metody sendNotification , która przyjmie jako typ kolejny kanał informacyjny. Jest to sprzeczne z zasadą Open-Closed.

Jak możemy to poprawić?

Wdróżmy wspólny interfejs Notification:

interface Notification { message: string; send(): void; }

Klasa Sms powinna implementować powyższy interfejs:

class Sms implements Notification { message: string; constructor(message: string) { this.message = message; } send() { // logika związana z wysyłaniem wiadomości sms } }

Następnie modyfikujemy kod klasy NotificationService:

class NotificationService { sendNotification(notification: Notification) { // logika wysyłania notyfikacji notification.send(); } }

W powyższym kodzie dzięki utworzeniu wspólnego interfejsu Notification udało nam się zastosować zasadę Open-Closed. Każdy nowy kanał wiadomości będzie mógł go zaimplementować, co pozwoli przekazać jako parametr metody sendNotification. Unikniemy dzięki temu tworzenia kolejnej metody, za każdym razem gdy pojawi się nowy sposób przesyłania powiadomienia.

Ten przykład świetnie pokazuje działanie zasady Open-Closed. Powyższy kod pozostał otwarty na rozszerzenia przy jednoczesnym zamknięciu na modyfikację.

Podsumowanie

Mamy nadzieje, że Open-Closed nie skrywa już przed Tobą żadnych tajemnic. Dzięki zastosowaniu tej zasady Twój kod stanie się elastyczny oraz łatwiejszy w utrzymaniu. Trudno jest nie dostrzec również korzystnego wpływu na wydajność pracy zespołu. Minimalizując ilość modyfikacji istniejącego kodu nasz system staje się bardziej stabilny oraz mniej podatny na błędy. Zdecydowanie warto wcielać tę zasadę w życie w codziennej pracy.