Zasada otwarty-zamknięty
Zasadę otwarty-zamknięty (ang. Open-Closed Principle, OCP) po raz pierwszy sformułował Bertrand Meyer [1]. Mówi ona, ze:
Klasa powinna być otwarta na rozszerzanie, ale zamknięta na modyfikacje.
Na
pierwszy rzut oka powyższa zasada wydaje się być sama w sobie
sprzeczna. Czy rzeczywiście możliwe jest, aby klasa była jednocześnie
otwarta na rozszerzanie i zamknięta na modyfikacje? Czy rozszerzanie
nie wiąże się z modyfikacją klasy? Otóż w kontekście zasady
otwarty-zamknięty rozszerzanie rozumiane jest jako dodawanie nowej
funkcji bez modyfikacji tej już istniejącej.
Przypuśćmy, że
pracujemy nad programem do zarządzania budżetem domowym. Chcemy
wykonywać rożne operacje, takie jak wpływy, wydatki i przelewy.
Zdefiniowaliśmy sobie klasę Operation reprezentującą operację. Jej
implementacja znajduje sie w przykładzie 1.
Przykład [C#] 1. Klasa reprezentująca operację.
public class Operation
{
private OperationType type;
public OperationType Type
{
get { return this.type; }
}
private decimal amount;
public decimal Amount
{
get { return this.amount; }
set { this.amount = value; }
}
public Operation (OperationType type)
{
this.type = type;
}
}
W
innej części programu definiujemy metodę, której zadaniem jest
przetwarzanie operacji. Jej realizacje przedstawia przykład 2.
Przykład [C#] 2. Metoda przetwarzająca operacje
public void Execute ( Operation[] operationList )
{
for ( int i = 0; i < operationList.Length; i++)
{
Operation operation = operationList[i];
if ( operation.Type == OperationType.Income )
{
ProcessIncome( operation );
}
else if ( operation.Type == OperationType.Outcome )
{
ProcessOutcome ( operation );
}
else if ( operation.Type == OperationType.Transfer )
{
ProcessTransfer( operation );
}
}
}
Działanie
metody z przykładu 2 jest proste. Iteruje ona po liście dostarczonych
do przetworzenia operacji i bazując na typie danej operacji deleguje
wykonanie operacji do odpowiedniej metody pomocniczej. Zastanówmy sie
teraz dlaczego powyższa realizacja nie spełnia zasady
otwarty-zamkniety. Według tej zasady klasa powinna być otwarta na
rozszerzanie i zamknięta na modyfikacje. Spróbujmy zobaczyć jak
wyglądałby proces rozszerzania tej realizacji o nowy typ operacji.
Podstawową modyfikacją jaką musielibyśmy wprowadzić jest dodanie
dodatkowej klauzuli else if w metodzie Execute tak, aby obsługiwać
także nowy typ operacji. To jest właśnie przyczyna niezgodności z
zasadą otwarty-zamknięty. Dodając nową funkcję w postaci nowego typu
operacji musimy zmienić metodę Execute. Zmuszeni będziemy zmodyfikować
istniejący już kod. Problemem, z jakim się tutaj spotykamy, jest
uzaleznienie realizacji metody Execute od pewnego zbioru elementów. Jak
możemy ten problem rozwiązać? Musimy zwolnić metodę Execute z
konieczności decydowania co trzeba zrobić z daną operacją. Rozwiązaniem
jest przeniesienie tej odpowiedzialności do samej operacji oraz
ujednolicenie dostępu do niej. Możemy tego dokonać wykorzystując
delegację. Definiując interfejs operacji wyszczególnimy metodę służąca
do jej wykonania. Wtedy metoda Execute będzie mogła oddelegować
wykonanie operacji do samej operacji. Przykład 3 zawiera definicje
interfejsu operacji.
Przykład [C#] 3. Interfejs operacji.
public interface IOperation
{
void Calculate();
}
Dzięki
niemu realizując metodę Execute możemy abstrahować od typu operacji.
Ulepszona jej realizacja znajduje sie w przykładzie 4.
Przykład [C#] 4. Ulepszona realizacja metody Execute.
public void Execute( IOperation[ ] operationList )
{
for ( int i = 0; i < operationList.Length; i++ )
{
IOperation operation = operationList[i];
operation.Calculate();
}
}
W
ulepszonym projekcie każda operacja musi być implementacją interfejsu
IOperation. Zatem musimy zdefiniować trzy nowe klasy, które
implementują interfejs IOperation i w metodzie Calculate wykonują
operacje, które do tej pory wykonywane były przez odpowiednie metody
pomocnicze wywoływane przez metodę Execute z przykładu 3.2.
Dlaczego
takie rozwiązanie jest lepsze i zgodne z zasadą otwarty-zamknięty?
Zastanówmy się ponownie nad tym, co należy zrobić, aby dodać nową
operację. Ulepszona realizacja wymaga jedynie zdefiniowania nowej klasy
implementującej interfejs IOperation. Taka klasa będzie już w pełni
obsługiwana przez metodę Execute. Zauważmy, że nie modyfikujemy przy
tym żadnego istniejącego do tej pory kodu. Jedynie dodajemy nową klasę.
Zatem taki projekt jest w pełni zgodny z zasadą otwarty-zamknięty.
Zasada
otwarty-zamknięty określa cechy charakterystyczne dobrego projektu.
Dobry projekt to taki, w którym dodając nową funkcję nie musimy
zmieniać istniejącego kodu. Wystarczy jedynie dodanie nowych podklas
oraz przeciążanie metod. Poza tym nie modyfikując istniejącego kodu nie
możemy niczego zepsuć. To jest bardzo ważna cecha z punktu widzenia
pielęgnacji systemu.
[1] Meyer B.: Programowanie zorientowane obiektowo. Gliwice. Helion, 2005.