Spis treści
OCP jest o tym, aby nie zmieniać kodu gotowego produkcyjnie.
I to według mnie jest wszystko, co mógłbym napisać o tej zasadzie po wielu godzinach poszukiwań. Jednak jak osiągnąć taki stan rzeczy? W tym artykule celowo zaczynam od bardziej teoretycznych rozważań na temat tej zasady, ponieważ błahych przykładów prezentujących zastosowanie jej jest mnóstwo w Internecie. Niemniej, na końcu prezentuję kilka przykładów, które według mnie są bardziej realne w codziennej sztuce programowania.
Czym jest OCP i jak do niego dążyć
Elementy oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozbudowę, ale zamknięte na modyfikację.
Agile principles, patterns, and practices in C# (tłumaczenie własne)
Zasadę OCP spełniamy głównie poprzez abstrakcję wspartą kontenerem zależności i ich wstrzykiwaniem (dependency container). Możemy robić to na kilka sposobów. Najlepiej zacząć od interfejsów, gdyż te pozwalają nam dowolnie podmieniać implementację. Następnie przejść do klas abstrakcyjnych, gdzie dużą część funkcjonalności można współdzielić. Dopiero na końcu wykorzystać dziedziczenie po pozostałych klasach, gdyż te rzadko są robione z myślą o byciu czyimś rodzicem.
Zasadzie OCP możemy pomóc wykorzystując zasady odwróconej kontroli (inversion of control) oraz pojedynczej odpowiedzialności (single responsibility). Jednak jak zobaczymy w przykładach i w trakcie własnej praktyki, niemożliwe jest wykorzystywanie tylko jednej z zasad SOLID – one wszystkie razem tworzą mur pozbawiony możliwie wszystkich słabych punktów.
Chciałbym jeszcze podkreślić, że w przypadku abstrakcji w zadnym wypadku nie chodzi o to, aby tworzyć rozbudowane drzewa klas. Powinniśmy starać się ograniczać przede wszystkim do implementacji możliwie prostych interfejsów, gdyż ich implementacje najprościej jest zastąpić przy pomocy kontenera zależności. W przypadku gdy chcemy uniknąć kopiowania kodu mamy często dwie możliwości: klasę abstrakcyjną (bazową) oraz kompozycję, gdzie w szczególności polecam to drugie.
Czym się kierować?
Fool me once, shame on you. Fool me twice, shame on me.
Agile principles, patterns, and practices in C#
Długo zastanawiałem się jak przetłumaczyć to przysłowie, ale nie potrafię zrobić tego wystarczająco gładko. W skrócie, dla tych nieanglojęzycznych, będzie tak: „oszukaj mnie raz – wstydź się, oszukaj mnie ponownie – będę wstydził się ja”. Tym cytatem Robert C. Martin ma na myśli to, żeby nie stosować abstrakcji zbyt pochopnie, co może prowadzić do nadmiernego skomplikowania kodu. Więc kiedy najlepiej zacząć wprowadzać interfejsy i klasy wirtualne? Za drugim razem! Miał być tylko jeden typ użytkownika, a teraz dochodzi drugi? Zrób tak, aby i trzeci dodać łatwo.
Im dłużej będziemy czekać, aby dowiedzieć się, jakiego rodzaju zmiany są prawdopodobne, tym trudniej będzie stworzyć odpowiednie abstrakcje
Agile principles, patterns, and practices in C# (tłumaczenie własne)
Coś w tym jest, że im dłużej czekamy, tym kolejne moduły wydają się coraz bardziej unikalne. A jednak i one składają się z małych komponentów, które można wydzielić. Biorąc pod uwagę tę i poprzednią złotą radę, można by dojść do wniosku, że odpowiednią abstrakcję należy dodać zawsze, gdy pojawia się drugi element lub dochodzi do drugiej zmiany działającego już produkcyjnie kodu. Co ważne: zmiany, nie naprawy.
Bez względu na to, jak „zamknięty” jest moduł, zawsze będzie istniał jakiś rodzaj zmiany, przed którą nie jest on zamknięty.
Agile principles, patterns, and practices in C# (tłumaczenie własne)
No i złota myśl na pocieszenie: nieważne jakbyśmy się starali i tak nie będzie idealnie. Dlatego odpuść sobie ciężką pracę, aby wszystko było przygotowane na przyszłe zmiany: tak nie będzie. Pamiętaj, że w aplikacji nie liczy się tylko odporność na przyszłe zmiany, ale również to aby dowieść funkcje potrzebne naszemu klientowi.
Jak pisze sam Robert C. Martin ([AgilePPP]) należy uważać, aby nie przesadzić ze zbyt rozbudowaną abstrakcją. Może się to skończyć przekombinowaniem całości.
Cóż nam daje OCP?
Jeśli dbamy o kod przetestowany produkcyjnie dostajemy kilka benefitów:
-
architekturę pluginową,
-
łatwiejsze wdrożenie juniorów,
-
kod odporny na błędy,
-
szybsze wdrażanie funkcji.
Elastyczność – architektura pluginowa
Według Roberta C. Martina najwyższą formą OCP są pluginy [theOCP]. Jako przykład można podać wszechobecne edytory kodu, przeglądarki czy gry, którym można dodawać całkowicie nową funkcjonalność za pomocą rozszerzeń (modów). Taka elastyczność daje nam łatwość dokonywania zmian w programie, poprzez ich odizolowywanie w osobnych modułach.
Konkretność w dokonywaniu zmian – łatwiejsze wdrożenie juniorów
Stanowczo łatwiej jest „wykorzystać” juniora w projekcie, który składa się z prostych interfejsów, bo cóż skomplikowanego może być w implementacji takiegoż interfejsu:
public interface ICalcOperation
{
string Name {get;}
double Calc(double left, double right);
}
Pomijając sensowność tego interfejsu, największe jego zalety to przede wszystkim przejrzystość: junior wie, w jakim zakresie ma wykonać swoją pracę. Do tego wykona swoją pracę w osobnych, nowych klasach, nie dotykając kodu produkcyjnego. Bardziej obrazowe porównanie znajduje się poniżej.
Solidność – kod odporny na błędy
Kod staje się odporny na błędy poprzez rzadsze zmienianie tych fragmentów oprogramowania, które są już przetestowane w boju. Co więcej, dzięki jasnemu podziałowi na klasy nadrzędne, odpowiadające za logikę, od tych wykonawczych (kłania się zasada odwróconej kontroli), można łatwiej ocenić, kto powinien zająć się ewentualnym błędem: junior, mid, czy może senior.
Wielorazowość i przejrzystość – szybsze wdrażanie funkcji
Dzięki odseparowaniu pomniejszych funkcjonalności poszczególne elementy oprogramowania mają większą szansę być wykorzystane w innym projekcie. Rosnąca przejrzystość, dzięki prostym interfejsom i pluginowej architekturze, pozwala nam na szybsze dołączanie nowych funkcjonalności, zwłaszcza w aspektach, które mogą poszczycić się największym wskaźnikiem ponownego wykorzystania kodu.
Przykład kodu
Teraz przejdźmy do przykładu. Weźmy na tapet wyświechtany już kalkulator, który oprzemy o interfejs przedstawiony powyżej. Możemy go zrobić na dwa sposoby: rozrzucając wszystko jak popadnie, bądź z uwzględnieniem OCP.
Sposób rozproszony
Możemy naszą aplikację pisać w sposób prosty, niczym na projekt zaliczeniowy. Jak to wtedy wygląda?
Krok 1: Przeniesienie
Pierwszym krokiem, i często ostatnim, jest przeniesienie poszczególnych funkcjonalności do osobnych klas:
Krok 2: Izolacja z ujednoliceniem
W tym kroku dokonamy odpowiedniej hermetyzacji obiektów w celu ukrycia zależności poszczególnych poleceń:
Krok 3: Wprowadzenie interfejsu
Ten krok nie zawsze jest obowiązkowy. Wiąże się on ze zmianą kilku warstw w sposób wymagający dużej wiedzy na temat języka i technologii, z której się korzysta – przez co, bez prawdziwych seniorów, może on być niewykonalny dla zespołu. Niemniej, czasem zdarza się, że wymagania co do warstwy prezentacji są tak szczegółowe, że ujednolicenie tej kwestii jest tak bardzo pracochłonne, że aż nieopłacalne.
Na co uważać?
Osobiście wyróżniam dwie rzeczy, na które trzeba w szczególnie uważać: enumy oraz programowanie strukturalne w połączeniu z obiektowym.
Na to pierwsze zwraca uwagę sam Robert C. Martin, mówiąc, ż że toleruje je tylko wtedy, gdy są używane do utworzenia obiektu i dodatkowo nie są dostępne z zewnątrz [CleanHandBook].
Co więcej warto zwrócić uwagę, że wykorzystanie enuma więcej niż w jednym zestawie instrukcji switch
…case
lub if
…else
jest świetnym wskaźnikiem miejsca, którym można by się zaopiekować w celu zastosowania zasady Open Close Principle.
Takie rozdwojenie kodu pomiędzy programowanie strukturalnym a obiektowym uważam za niebiezpieczne z prostego względu: zmiany w takim kodzie często są niezwykle kaskadowe a wyłuskanie odpowiedniej abstrakcji jest po prostu ciężkie. Zapewne lepiej byłoby po prostu pisać albo strukturalnie albo obiektowo - najlepiej po prostu się zdecydować.
Źródła i materiały dodatkowe
-
[theOCP] Martin, Robert C. „Clean Coder Blog”. Dostęp z dnia 17 listopada 2021. https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html.
-
[CleanHandBook] Martin, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. Repr. Robert C. Martin Series. Upper Saddle River, NJ Munich: Prentice Hall, 2012.
-
[AgilePPP] Martin, Robert C., i Micah Martin. Agile Principles, Patterns, and Practices in C#. Robert C. Martin Series. Upper Saddle River, NJ: Prentice Hall, 2007.
-
Samokhin, Vadim. „The Open-Closed Principle”. HackerNoon.Com (blog), 16 czerwiec 2018. https://medium.com/hackernoon/the-open-closed-principle-c3dc45419784.
-
Chovatiya, Vishal. „Open Closed Principle in C++ | SOLID as a Rock”. Vishal Chovatiya, 7 kwiecień 2020. http://www.vishalchovatiya.com/open-closed-principle-in-cpp-solid-as-a-rock/.
-
Azevedo, Gustavo Peixoto de. „The Open/Closed Principle: Concerns about Change in Software Design”. The Sympriser Blog, 23 czerwiec 2009. https://blog.symprise.net/articles/open-closed-principle-concerns-about-change-in-software-design.
-
Stackify. „SOLID Design Principles Explained: The Open/Closed Principle with Code Examples”, 28 marzec 2018. https://stackify.com/solid-design-open-closed-principle/.
Zdjęcie tytułowe: engin akyurt on Unsplash