OCP (Open/Closed Principle) — druga zasada SOLID, która określa: jednostki programowe powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje. Ta zasada, sformułowana przez Bertranda Meyera w 1988 roku, pozwala dodawać nową funkcjonalność bez zmiany istniejącego kodu. Według książki Roberta C. Martina Clean Architecture (2017), zasada otwartości jest realizowana poprzez abstrakcje i polimorfizm, minimalizując ryzyko błędów regresyjnych.
Najważniejsze
OCP (Open/Closed Principle) — zasada otwartości na rozszerzenia i zamkniętości na modyfikacje. Klasy, moduły i funkcje powinny być zaprojektowane tak, aby nowe zachowanie było dodawane bez zmiany ich kodu źródłowego. Rozszerzenie osiąga się poprzez dziedziczenie, kompozycję lub podstawienie implementacji interfejsów.
Bertrand Meyer w książce Object-Oriented Software Construction (1988) po raz pierwszy opisał OCP poprzez dziedziczenie: klasa bazowa pozostaje niezmieniona, a podklasy rozszerzają jej zachowanie. Współczesna interpretacja OCP, zaproponowana przez Roberta C. Martina, opiera się na polimorfizmie i interfejsach: zamiast dziedziczenia stosuje się abstrakcyjne kontrakty.
Różnica między podejściami jest znacząca. Dziedziczenie tworzy sztywną zależność między klasą bazową a pochodną. Interfejsy i kompozycja dają elastyczność: implementacja jest podmieniana bez zmiany kodu klienckiego. Współczesny OCP to przede wszystkim abstrakcja, a nie dziedziczenie.
Polimorficzny OCP wykorzystuje abstrakcyjne klasy lub interfejsy do zdefiniowania kontraktu. Kod kliencki pracuje z abstrakcją, nie znając konkretnej implementacji. Nowa funkcjonalność jest dodawana poprzez utworzenie nowej klasy implementującej ten sam interfejs — bez żadnej zmiany istniejącego kodu. To czyni system odpornym na zmiany i przewidywalnym przy rozszerzaniu.
W programowaniu mobilnym to podejście jest wszechobecne: wzorzec Strategy pozwala podmieniać algorytmy (kompresja obrazów, buforowanie, uwierzytelnianie) poprzez wspólny interfejs. Dodanie nowej strategii nie wymaga zmiany kodu, który jej używa.
Realizacja OCP zaczyna się od wyodrębnienia zmiennego zachowania do abstrakcji. Jeśli w kodzie występuje konstrukcja switch lub łańcuch if-else sprawdzający typ obiektu — to sygnał do zastosowania OCP. Każda gałąź warunku potencjalnie wymaga dodania nowej gałęzi przy rozszerzaniu.
Proces refaktoringu zgodnie z OCP obejmuje trzy kroki: określić zmienny aspekt (to, co może być rozszerzane), wyodrębnić go do interfejsu lub klasy abstrakcyjnej, przepisać kod kliencki do pracy z abstrakcją zamiast konkretną klasą. Następnie nowa funkcjonalność jest dodawana bez zmiany klienta.
Ważne zastrzeżenie: zamkniętość na modyfikacje nie jest absolutna. Jeśli wymaganie dotyczy samej abstrakcji lub kontraktu — zmiana jest nieunikniona. OCP chroni przed zmianami w implementacjach, a nie w kontraktach. Dobry projekt zakłada, że kontrakty są stabilne, a implementacje zmienne.
Przy ocenie zgodności architektury z OCP warto patrzeć na punkty rozszerzeń. Każdy punkt, w którym programista dodaje if-else lub switch dla nowego typu — kandydat do abstrakcji. System zaprojektowany zgodnie z OCP ma przewidywalne punkty rozszerzeń: interfejsy z dokumentacją „zaimplementuj ten interfejs, aby dodać nowy typ". W Android takim przykładem jest wzorzec Factory w połączeniu z ViewModelProvider.Factory — dodanie nowego typu ViewModel nie wymaga zmiany istniejących fabryk.
Najbardziej efektywne wzorce dla przestrzegania OCP w programowaniu mobilnym obejmują Strategy, Template Method, Decorator i Factory. Każdy z nich rozwiązuje problem rozszerzania zachowania bez modyfikacji istniejącego kodu poprzez różne mechanizmy projektowania obiektowego.
Strategy pozwala podmieniać algorytmy w locie poprzez wspólny interfejs. W iOS strategie są używane do animacji i walidacji formularzy. Template Method definiuje szkielet algorytmu w klasie bazowej, a podklasy nadpisują kroki — nadaje się do ekranów o wspólnej strukturze, ale różnej zawartości.
Decorator dynamicznie dodaje zachowanie do obiektu bez zmiany jego klasy. W Android Decorator jest stosowany do opakowywania Repository warstwą cache lub logowania. Factory Method tworzy obiekty poprzez interfejs, pozwalając podklasom decydować, którą klasę zainicjować — podstawa OCP-zgodnego tworzenia zależności.
Wybór wzorca zależy od stabilności rozszerzanego zachowania. Strategy jest optymalna, gdy algorytmy są wymieniane w całości. Template Method — gdy struktura jest stała, ale kroki zmienne. Decorator — gdy rozszerzenie ma być przezroczyste dla klienta. W większości scenariuszy w Android i iOS wystarczy Strategy + wstrzykiwanie zależności.
Stosowanie tych wzorców bez OCP jest technicznie możliwe, ale traci sens. To właśnie OCP uzasadnia, po co wprowadzamy dodatkowy poziom abstrakcji: aby system mógł rosnąć bez przepisywania istniejącego kodu.
Rozważmy przykład z Androida z obsługą płatności. Bez OCP każdy nowy system płatności wymaga zmiany klasy obsługującej. Z OCP dodaje się nową implementację interfejsu bez zmiany istniejącego kodu.
// Naruszenie OCP: switch wymaga zmiany przy nowym systemie
class BadPaymentProcessor {
fun process(type: String) {
when (type) {
"card" -> // obsługa karty
"paypal" -> // obsługa PayPal
}
}
}
// Projekt zgodny z OCP
interface PaymentMethod {
fun pay(amount: Double)
}
class CardPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
class PayPalPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
// Nowy system — nowa klasa, bez zmiany istniejącego kodu
class ApplePayPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
Przykład z iOS z walidacją pól tekstowych demonstruje tę samą logikę poprzez protokoły Swift:
// Walidacja zgodna z OCP
protocol ValidationRule {
func validate(_ input: String) -> Bool
}
struct EmailRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.contains("@")
}
}
struct PhoneRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count == 11
}
}
// Dodanie nowej reguły nie wymaga zmiany kodu walidatora
struct PasswordRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count >= 8
}
}
Kluczowa zaleta OCP w tych przykładach: dodanie ApplePay lub PasswordRule nie wymaga zmiany istniejących klas. Kod rozszerza się horyzontalnie — poprzez nowe pliki, a nie przez zmianę starych. To zmniejsza ryzyko regresji i przyspiesza wdrażanie nowej funkcjonalności.
Najczęstsze naruszenie — konstrukcja switch lub when po typie obiektu. Za każdym razem przy dodawaniu nowego typu trzeba znaleźć wszystkie takie switch w kodzie i dodać nową gałąź. Pominięty switch to błąd w runtime, który trudno wykryć na etapie kompilacji.
W programowaniu mobilnym OCP jest naruszane przy użyciu gigantycznych klas enum z metodami zależnymi od wartości enum. Dodanie nowego elementu enum wymaga zmiany każdego switch w całym projekcie. Alternatywą jest polimorfizm przez interfejs, gdzie każdy typ realizuje swoje zachowanie.
Inne typowe naruszenie — God Adapter: RecyclerView.Adapter (Android) lub UITableViewDataSource (iOS), który przez if-else obsługuje różne typy komórek. Każdy nowy typ komórki wymaga rozszerzenia adaptera. Rozwiązaniem jest polimorficzny ViewHolder ze wspólną metodą bind, gdzie każdy typ komórki odpowiada za swoje wyświetlanie.
Działania zapobiegawcze obejmują: rezygnację z switch po typie na rzecz polimorfizmu, wstrzykiwanie zależności przez interfejsy oraz zastosowanie wzorca Factory do tworzenia obiektów według konfiguracji. Analiza kodu pod kątem „przełączników po typie" — obowiązkowa część code review w zespołach OCP-oriented.
Refaktoring istniejącego naruszenia OCP wykonuje się przez Replace Conditional with Polymorphism: każda gałąź warunku staje się osobną klasą z implementacją wspólnego interfejsu. Kod kliencki jest przepisywany do pracy z interfejsem, a konkretna implementacja jest podstawiana przez fabrykę lub kontener DI.
Ważne jest zrozumienie, że OCP i polimorfizm nie rozwiązują wszystkich problemów rozszerzania. Jeśli architektura została wybrana nieprawidłowo, dodanie nowej funkcjonalności będzie wymagać zmiany nie tylko implementacji, ale także kontraktów. Dobra architektura przewiduje kierunki rozszerzania i wprowadza abstrakcje właśnie w tych punktach. Inwestycja w OCP zwraca się tym bardziej, im dłużej żyje projekt i im częściej zmieniają się wymagania dotyczące konkretnych modułów.
Często zadawane pytania
Nie. OCP zabrania zmiany istniejącego kodu przy dodawaniu nowej funkcjonalności odnoszącej się do tej samej abstrakcji. Zmiana kontraktu, naprawianie błędów i refaktoring nie są naruszeniem OCP — zasada chroni przed kaskadowymi poprawkami przy rozszerzaniu.
Strategy — bezpośrednia implementacja OCP. Interfejs strategii określa kontrakt, klient zależy od abstrakcji, a konkretne strategie realizują zmienne zachowanie. Dodanie nowej strategii nie wymaga zmiany klienta — to właśnie otwartość na rozszerzenia przy zamkniętości na modyfikacje.
Tak, poprzez dziedziczenie i Template Method: klasa bazowa określa szkielet algorytmu, podklasy nadpisują kroki. Jednak dziedziczenie tworzy sztywną zależność i jest mniej elastyczne niż interfejsy. We współczesnym programowaniu interfejsy i kompozycja są uważane za preferowany sposób realizacji OCP.
Kod zgodny z OCP ułatwia testowanie: każda implementacja interfejsu jest testowana w izolacji. Kod kliencki jest testowany z mock-implementacją, co pozwala sprawdzać logikę bez przypisywania do konkretnego zachowania. Rozszerzanie systemu nie wymaga przepisywania istniejących testów.
Nie. OCP jest uzasadniony, gdy rozszerzanie funkcjonalności jest przewidywalne. Dla stabilnego kodu, który nie ma być rozszerzany, dodatkowa abstrakcja jest zbędna. YAGNI (You Ain't Gonna Need It) — dobry przeciwwaga dla OCP: abstrakcję wprowadza się, gdy pojawia się drugi wariant zachowania, a nie na zapas.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również