Czym jest Zasada Otwarte-Zamknięte?
Zasada ta, sformułowana przez Bertranda Meyera, mówi, że:
Encje oprogramowania (klasy, moduły, funkcje) powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje.
Co to tak naprawdę oznacza?
- Otwarte na rozszerzenia: Powinniśmy mieć możliwość dodawania nowej funkcjonalności bez zmieniania istniejącego kodu.
- Zamknięte na modyfikacje: Gdy dana klasa jest już napisana, przetestowana i działa, powinniśmy unikać jej modyfikacji, aby nie wprowadzić nowych błędów.
Kluczem do osiągnięcia tego celu jest abstrakcja. Zamiast uzależniać kod od konkretnych implementacji, uzależniamy go od interfejsów lub klas abstrakcyjnych. Dzięki temu możemy “podmieniać” implementacje i dodawać nowe, nie ruszając kodu, który z nich korzysta.
Kluczowe aspekty OCP
Stosowanie tej zasady prowadzi do:
- Stabilności systemu: Rdzeń aplikacji pozostaje nietknięty, co minimalizuje ryzyko regresji (wprowadzenia błędów w działających już funkcjach).
- Większej elastyczności: System można łatwo rozbudowywać o nowe funkcje, co jest kluczowe w dynamicznie zmieniających się projektach.
- Lepszej skalowalności: Nowe moduły można dodawać bez ingerencji w stare, co ułatwia pracę w większych zespołach.
- Mniejszego sprzężenia (coupling): Moduły wysokiego poziomu nie zależą od detali implementacyjnych modułów niskiego poziomu.
Przykład naruszenia zasady
Wyobraźmy sobie system, w którym mamy różne typy pracowników. Działy Finances i Hr muszą wykonywać na nich specyficzne operacje. Zobaczmy, jak wygląda kod, który nie stosuje się do OCP.
Diagram UML

Kod
W tym przykładzie klasy Finances i Hr muszą znać każdy konkretny typ pracownika.
// Klasy pracowników (konkretne implementacje)
public class Ceo {
public void calculateSalary(){ System.out.println("Pay Ceo"); }
public void showIdCard(){ System.out.println("Greet Ceo"); }
}
public class Programmer {
public void calculateSalary(){ System.out.println("Pay Programmer"); }
public void showIdCard(){ System.out.println("Greet Programmer"); }
}
// Klasy operacyjne, które łamią OCP
public class Finances {
public void calculateSalaries(Object[] employees){
for(Object employee : employees){
if(employee instanceof Ceo){
((Ceo) employee).calculateSalary();
}
if(employee instanceof Programmer){
((Programmer) employee).calculateSalary();
}
// Co jeśli dodamy Managera? Musimy dodać kolejny 'if'!
}
}
}
public class Hr {
public void showIdCards(Object[] employees){
for(Object employee : employees){
if(employee instanceof Ceo){
((Ceo) employee).showIdCard();
}
if(employee instanceof Programmer){
((Programmer) employee).showIdCard();
}
// Tutaj też trzeba będzie dodać kolejny 'if'...
}
}
}
// Główna klasa aplikacji
public class Main {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
Object[] employees = new Object[] { new Ceo(), new Programmer() };
finances.calculateSalaries(employees);
hr.showIdCards(employees);
}
}Analiza i Problem
Głównym problemem jest tutaj konieczność modyfikacji istniejących klas Finances i Hr za każdym razem, gdy dodajemy nowy typ pracownika (np. Accountant). Musielibyśmy dopisać kolejny blok if (employee instanceof Accountant). To sprawia, że kod jest:
- Kruchy: Każda zmiana niesie ryzyko zepsucia czegoś, co już działało.
- Sztywny: Jest odporny na zmiany i trudny w rozbudowie.
- Trudny w utrzymaniu: Logika jest rozproszona w wielu warunkach
if-elselubswitch.
Jest to jawne złamanie zasady OCP – klasy nie są zamknięte na modyfikacje.
Poprawne zastosowanie zasady
Naprawmy nasz projekt, wprowadzając wspólną abstrakcję – interfejs Employee.
Diagram UML

Kod po refaktoryzacji
1. Tworzymy interfejs (naszą abstrakcję):
public interface Employee {
void calculateSalary();
void showIdCard();
}2. Klasy pracowników implementują interfejs:
public class Ceo implements Employee {
@Override
public void calculateSalary() { System.out.println("Pay Ceo"); }
@Override
public void showIdCard() { System.out.println("Greet Ceo"); }
}
public class Programmer implements Employee {
@Override
public void calculateSalary(){ System.out.println("Pay Programmer"); }
@Override
public void showIdCard(){ System.out.println("Greet Programmer"); }
}
// Możemy teraz łatwo dodać nową klasę!
public class Accountant implements Employee {
@Override
public void calculateSalary(){ System.out.println("Pay Accountant"); }
@Override
public void showIdCard(){ System.out.println("Greet Accountant"); }
}3. Modyfikujemy klasy operacyjne, by zależały od abstrakcji:
import java.util.List;
public class Finances {
public void calculateSalaries(List<Employee> employees){
employees.forEach(employee -> employee.calculateSalary());
}
}
public class Hr {
public void showIdCards(List<Employee> employees){
employees.forEach(Employee::showIdCard); // Inny zapis tego samego
}
}4. Aktualizujemy główną klasę aplikacji:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
List<Employee> employees = new ArrayList<>();
employees.add(new Ceo());
employees.add(new Programmer());
employees.add(new Accountant()); // Dodanie nowego pracownika jest banalnie proste!
finances.calculateSalaries(employees);
hr.showIdCards(employees);
}
}Analiza
Teraz klasy Finances i Hr są zamknięte na modyfikacje. Nie obchodzi ich, jakie konkretne typy pracowników przetwarzają – wiedzą tylko, że każdy obiekt na liście jest typu Employee i na pewno ma metody calculateSalary() i showIdCard().
Jednocześnie system jest otwarty na rozszerzenia. Możemy dodać dziesięć nowych stanowisk (np. Tester, Manager, Designer) – wystarczy, że stworzymy nowe klasy implementujące interfejs Employee. Nie musimy dotykać ani jednej linijki kodu w klasach Finances i Hr.
Porównanie przykładów
| Cecha | Przed refaktoryzacją | Po refaktoryzacji |
| Rozszerzalność | Wymaga modyfikacji istniejących klas | Nowe funkcje dodawane przez nowe klasy |
| Zależności | Od konkretnych implementacji (np. Ceo, Programmer) | Od abstrakcji (Employee) |
| Stabilność | Niska – istniejący kod jest często zmieniany | Wysoka – przetestowany kod pozostaje nietknięty |
| Struktura kodu | Złożone warunki if-instanceof-then | Proste, polimorficzne wywołania metod |
Podsumowanie
Zasada Otwarte-Zamknięte jest sercem projektowania obiektowego. Promuje użycie abstrakcji i polimorfizmu, co prowadzi do tworzenia systemów, które są jednocześnie stabilne i elastyczne. Pamiętaj – projektuj swój kod tak, aby można było go rozszerzać, a nie przepisywać od nowa.
Bibliografia
- Martin, R. C. (2003). Agile Software Development, Principles, Patterns, and Practices. Pearson.
- Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall.