W kolejnym artykule dotyczącym zasad SOLID zajmę się drugą regułą z tego zestawu – Open/Closed Principle (OCP), czyli Zasadą otwarte/zamknięte.

Motto tego podejścia brzmi następująco:

Elementy oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozbudowę, ale zamknięte na modyfikację.<span class="su-quote-cite">Bertrand Meyer</span>
Początkowo ta definicja może wydawać się trochę niejasna i nielogiczna. Ktoś mógłby zapytać: „Niby jak mam rozbudować coś, co jest zamknięte na modyfikację?”. Otóż na to pytanie jest prosta odpowiedź: w tej regule rozbudowa oznacza rozszerzalność poszczególnych elementów programu, tzn. dodając funkcjonalność X, nie modyfikuję klasy Y, za to rozszerzam swój kod wykorzystując m. in. mechanizm abstracji, polimorfizmu, dziedziczenia itp.

OCP — po co i dlaczego?

Podłączenie słuchawek lub głośników do komputera czy smartfona jest banalnie proste – wystarczy je wpiąć do gniazda mini-jack. A co by było, gdybyśmy w tym celu musieli rozebrać na części całe urządzenie i dodać parę komponentów, inne z kolei usunąć bądź umiejscowić gdzie indziej, a w dodatku jeszcze tu i ówdzie coś przewiercić, przylutować itp.? Wówczas nie dość, że proces stałby się zbyt skomplikowany dla przeciętnego Kowalskiego, to również wiązałby się z ryzykiem ewentualnego uszkodzenia sprzętu.

Nie inaczej jest w programowaniu. Jeśli nowa funkcjonalność (zwłaszcza, gdy jest powiązana z inną, już istniejącą funkcjonalnością) wymaga modyfikacji klasy X, następnie Y itd., to taki zabieg jest proszeniem się o kłopoty. W takiej sytuacji znacznie wzrasta ryzyko wystąpienia błędów w programie. Nietrudno również pogubić się podczas prób rozbudowy programu.

OCP na przykładzie

Oczywiście, żeby tradycji stało się zadość, po raz kolejny posłużę się przykładowym kodem, abyście lepiej mogli przyswoić wiedzę z tego artykułu 🙂. Tym razem nie będzie to C++. Nie będzie to również Java. Zamiast nich, wykorzystam w przykładzie język PHP .

Przedmiotem tego przykładu będzie program, do którego zadań należałoby generowanie raportów (np. ze sprzedaży) w różnych formatach.

<?php

class ReportGenerator
{
    private $sourceFileName;
    private $destinationDirectory;

    public function __construct($sourceFileName, $destinationDirectory)
    {
        $this->sourceFileName = $sourceFileName;
        $this->destinationDirectory = $destinationDirectory;
    }

    public function generatePdfReport($destinationFileName)
    {
        /*
         * Miejsce na kod zapisujący raport do pliku
         * */
        echo '<p>Report from source file ' . $this->sourceFileName 
        echo ' was generated in ' . $this->destinationDirectory
        echo "/" . $destinationFileName . '.pdf</p>';
    }

    public function generateCsvReport($destinationFileName)
    {
        /*
         * Miejsce na kod zapisujący raport do pliku
         * */
        echo '<p>Report from source file ' . $this->sourceFileName
        echo ' was generated in ' . $this->destinationDirectory 
        echo "/" . $destinationFileName . '.csv</p>';
    }
} 

Mam tutaj klasę ReportGenerator, która zawiera metody generujące raporty w dwóch formatach: PDF i CSV.

Celowo pominąłem rzeczywiste operacje na plikach, żeby jak najbardziej uprościć kod i nie wprowadzać mętliku w głowach czytelników, w szczególności tych, którzy albo dopiero co poznają PHP albo nie znają tego języka w ogóle. Tutaj chodzi tylko o zrozumienie samej zasady OCP 🙂.

Załóżmy, że chcemy, by nasz generator mógł zapisać raport do innego pliku, dajmy na to XML. To proste, wystarczy dodać metodę generateXmlReport. To samo, gdybyśmy chcieli generować raporty w jeszcze innych plikach. Tylko tutaj pojawia się problem: wraz z uzupełnianiem generatora o kolejne możliwości, kod klasy ReportGenerator rozrósłby się do dużych rozmiarów, co wpłynęłoby negatywnie na jego czytelność. Ponadto, gdyby okazało się, że musimy zmienić sposób generowania plików, to należałoby w takiej sytuacji zmodyfikować klasę w wielu miejscach, a konkretnie jej metody. Jeszcze pół biedy, jeśli jest ich kilka na krzyż.

Chciałbym również dodać, że powyższy kod łamie nie tylko OCP, ale też zasadę DRY (Don’t Repeat Yourself).

Jak w takim razie naprawić program wg wytycznych OCP? Poniżej macie odpowiedź:

<?php

interface ReportInterface
{
    public function render($destinationFileName);
}

class PdfReport implements ReportInterface
{
    public function render($destinationFileName)
    {
        /*
         * Miejsce na kod zapisujący raport do pliku
         * */
        return $destinationFileName . ".pdf";
    }
}

class CsvReport implements ReportInterface
{
    public function render($destinationFileName)
    {
        /*
         * Miejsce na kod zapisujący raport do pliku
         * */
        return $destinationFileName . ".csv";
    }
}

class ReportGenerator
{
    private $sourceFileName;
    private $destinationDirectory;
    private $report;

    public function __construct($sourceFileName, $destinationDirectory, ReportInterface $report)
    {
        $this->sourceFileName = $sourceFileName;
        $this->destinationDirectory = $destinationDirectory;
        $this->report = $report;
    }

    public function generateReport($destinationFileName)
    {
        $reportFile = $this->report->render($destinationFileName);
        echo '<p>Report from source file ' . $this->sourceFileName
        echo ' was generated in ' . $this->destinationDirectory
        echo "/" . $reportFile . "</p>";
    }
} 

Tak właśnie wygląda otwartość na rozbudowę i zamknięcie na modyfikację. Teraz, gdy np. chciałbym dodać możliwość generowania raportów w formacie XML, nie muszę modyfikować klasy ReportGenerator. Wystarczy utworzyć kolejną klasę będącą implementacją interfejsu ReportInterface. Dzięki mechanizmowi polimorfizmu, który tutaj wykorzystałem, klasie ReportGenerator wystarczy tylko jedna metoda generująca raport zamiast wielu, a w niej wywołanie metody render (nie licząc komunikatu informującego o powodzeniu operacji), którą każda implementacja ww. interfejsu może wykonać po swojemu.

Podsumowanie

Zmiany są nieodłączną częścią programowania. Warto pójść drogą otwierania kodu na rozszerzenia i zamknięcia na modyfikacje (chyba, że modyfikowanie okaże się naprawdę konieczne). Stosując regułę OCP w swoich projektach możemy mieć większą pewność, że rozwijany przez nas system będzie miał większą stabilność i łatwiej zlokalizujemy ewentualne błędy. Zredukujemy tym samym konieczność modyfikacji kodu w wielu miejscach.


1 komentarz

Katarzyna · 20 października 2021 o 18:35

Przykłady do mnie przemawiają ? Lubię to!

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *