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:
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!