Zasada otwartego zamknięcia (OCP) stanowi, że obiekt powinien być otwarty do rozszerzenia, ale zamknięty do modyfikacji. Wierzę, że rozumiem to i używam go w połączeniu z SRP do tworzenia klas, które robią tylko jedną rzecz. I staram się stworzyć wiele małych metod, które umożliwiają wyodrębnienie wszystkich elementów sterujących zachowania na metody, które mogą być rozszerzone lub zastąpione w niektórych podklasach. Tak więc kończę na klasach, które mają wiele punktów rozszerzenia, czy to poprzez: wstrzykiwanie zależności i skład, zdarzenia, delegowanie itp.
Rozważ następujące proste, rozszerzalne klasy:
class PaycheckCalculator {
// ...
protected decimal GetOvertimeFactor() { return 2.0M; }
}
Teraz powiedzmy na przykład, że OvertimeFactor
zmiany na 1.5. Ponieważ powyższa klasa została zaprojektowana z myślą o rozszerzeniu, mogę łatwo podklasę i zwrócić inną OvertimeFactor
.
Ale ... pomimo tego, że klasa została zaprojektowana do rozszerzenia i przestrzegania OCP, zmodyfikuję pojedynczą metodę, o której mowa, zamiast podklasować i zastępować tę metodę, a następnie ponownie okablować moje obiekty w moim kontenerze IoC.
W rezultacie naruszyłem część tego, co OCP próbuje osiągnąć. Mam wrażenie, że jestem po prostu leniwy, ponieważ powyższe jest nieco łatwiejsze. Czy źle zrozumiałem OCP? Czy naprawdę powinienem robić coś innego? Czy w inny sposób wykorzystujesz zalety OCP?
Aktualizacja : w oparciu o odpowiedzi wygląda na to, że ten wymyślony przykład jest kiepski z wielu różnych powodów. Głównym celem tego przykładu było wykazanie, że klasa została zaprojektowana w taki sposób, aby była rozszerzana poprzez dostarczenie metod, które w przypadku zastąpienia zmieniłyby zachowanie metod publicznych bez potrzeby zmiany kodu wewnętrznego lub prywatnego. Mimo to zdecydowanie źle zrozumiałem OCP.
źródło
During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other.
en.wikipedia.org/wiki/Open/closed_principleTak więc otwarta zasada zamknięta jest gotcha ... zwłaszcza jeśli spróbujesz zastosować ją w tym samym czasie, co YAGNI . Jak mogę przestrzegać obu jednocześnie? Zastosuj zasadę trzech . Przy pierwszej zmianie dokonaj zmiany bezpośrednio. I za drugim razem. Po raz trzeci nadszedł czas na streszczenie, które się zmieniło.
Innym podejściem jest „oszukuj mnie raz…”, kiedy musisz dokonać zmiany, zastosuj OCP, aby uchronić się przed tą zmianą w przyszłości . Chciałbym prawie posunąć się tak daleko, aby zaproponować, że zmiana stawki za nadgodziny to nowa historia. „Jako administrator listy płac chcę zmienić stawkę za nadgodziny, aby zapewnić zgodność z obowiązującymi przepisami prawa pracy”. Teraz masz nowy interfejs do zmiany stawki za godziny nadliczbowe, sposób jej przechowywania, a GetOvertimeFactor () po prostu pyta repozytorium, jaka jest liczba godzin nadliczbowych.
źródło
W opublikowanym przykładzie współczynnik nadgodzin powinien być zmienną lub stałą. * (Przykład Java)
LUB
Następnie, gdy rozszerzysz klasę, ustaw lub zastąp współczynnik. „Magiczne liczby” powinny pojawić się tylko raz. Jest to o wiele bardziej w stylu OCP i DRY (Don't Repeat Yourself), ponieważ nie jest konieczne tworzenie zupełnie nowej klasy dla innego czynnika, jeśli używa się pierwszej metody, a jedynie zmiana stałej w jednym idiomatycznym miejsce na drugim miejscu.
Pierwszego użyłbym w przypadkach, w których istniałoby wiele rodzajów kalkulatorów, z których każdy wymagałby różnych stałych wartości. Przykładem może być wzorzec łańcucha odpowiedzialności, który jest zwykle implementowany przy użyciu typów odziedziczonych. Obiekt, który widzi interfejs (tj.
getOvertimeFactor()
), Używa go do uzyskania wszystkich potrzebnych informacji, podczas gdy podtypy martwią się o faktyczne informacje, które należy podać.Drugi jest przydatny w przypadkach, w których stała prawdopodobnie nie ulegnie zmianie, ale jest używana w wielu lokalizacjach. Posiadanie jednej stałej do zmiany (w mało prawdopodobnym przypadku, gdy tak się dzieje) jest znacznie łatwiejsze niż ustawienie jej w dowolnym miejscu lub pobranie jej z pliku właściwości.
Zasada Otwarte-zamknięte jest mniejszym wezwaniem do nie modyfikowania istniejącego obiektu niż ostrzeżeniem o pozostawieniu interfejsu bez zmian. Jeśli potrzebujesz nieco innego zachowania niż klasa lub dodatkowej funkcjonalności dla konkretnego przypadku, rozszerz i zastąp. Ale jeśli zmieniają się wymagania dla samej klasy (jak zmiana współczynnika), musisz zmienić klasę. Ogromna hierarchia klas nie ma sensu, z której większość nigdy nie jest używana.
źródło
Naprawdę nie widzę twojego przykładu jako doskonałej reprezentacji OCP. Myślę, że tak naprawdę oznacza to:
Poniżej słaba implementacja. Za każdym razem, gdy dodajesz grę, musisz modyfikować klasę GamePlayer.
Klasa GamePlayer nigdy nie powinna wymagać modyfikacji
Teraz, zakładając, że mój GameFactory również przestrzega OCP, kiedy chcę dodać kolejną grę, musiałbym tylko zbudować nową klasę, która będzie dziedziczyć po
Game
klasie i wszystko powinno po prostu działać.Zbyt często klasy takie jak pierwsze budowane są po latach „rozszerzeń” i po prostu nigdy nie są poprawnie refaktoryzowane z oryginalnej wersji (lub, co gorsza, to, co powinno być wieloma klasami, pozostaje jedną dużą klasą).
Podany przykład to OCP-ish. Moim zdaniem poprawnym sposobem radzenia sobie ze zmianami stawek za godziny nadliczbowe byłaby baza danych z historycznymi stawkami, aby dane mogły być ponownie przetwarzane. Kod powinien być nadal zamknięty w celu modyfikacji, ponieważ zawsze ładowałby odpowiednią wartość z odnośnika.
Jako przykład ze świata rzeczywistego wykorzystałem wariant mojego przykładu, a Zasada Otwartego Zamknięcia naprawdę świeci. Funkcjonalność jest naprawdę łatwa do dodania, ponieważ muszę po prostu wyprowadzić się z abstrakcyjnej klasy bazowej, a moja „fabryka” wybiera ją automatycznie, a „gracz” nie dba o to, jaką konkretną implementację zwraca fabryka.
źródło
W tym konkretnym przykładzie masz tak zwaną „magiczną wartość”. Zasadniczo wartość zakodowana na stałe, która może, ale nie musi ulec zmianie w czasie. Spróbuję zająć się zagadką, którą wyrażasz ogólnie, ale jest to przykład tego rodzaju rzeczy, w których tworzenie podklasy wymaga więcej pracy niż zmiana wartości w klasie.
Powiedzmy, że mamy
PaycheckCalculator
.OvertimeFactor
Będzie więcej niż prawdopodobnie włączył się informacji o pracownika. Pracownik godzinowy może cieszyć się premią za nadgodziny, podczas gdy pracownik najemny nic nie dostanie. Mimo to niektórym pracownikom otrzymującym wynagrodzenie przysługuje bezpośredni czas z powodu umowy, nad którą pracowali. Możesz zdecydować, że istnieją pewne znane klasy scenariuszy płacowych i tak zbudujesz swoją logikę.W
PaycheckCalculator
klasie podstawowej uczynisz ją abstrakcyjną i określisz metody, których oczekujesz. Podstawowe obliczenia są takie same, tylko niektóre czynniki są obliczane inaczej. TwójHourlyPaycheckCalculator
by następnie wdrożyćgetOvertimeFactor
metody i powrót 1.5 lub 2.0, jak sprawa może być. TwójStraightTimePaycheckCalculator
zaimplementujegetOvertimeFactor
zwrot 1.0. Wreszcie trzecia implementacjaNoOvertimePaycheckCalculator
byłaby implementacjągetOvertimeFactor
zwrotu 0.Kluczem jest opisanie tylko zachowania w klasie bazowej, która ma zostać rozszerzona. Szczegóły części ogólnego algorytmu lub określonych wartości zostaną wypełnione podklasami. Fakt, że podałeś wartość domyślną
getOvertimeFactor
prowadzi do szybkiego i łatwego „naprawienia” do jednej linii zamiast rozszerzania klasy zgodnie z zamierzeniami. Podkreśla również fakt, że wysiłek związany jest z przedłużaniem zajęć. W zrozumieniu hierarchii klas w aplikacji konieczne są również wysiłki. Chcesz zaprojektować swoje klasy w taki sposób, aby zminimalizować potrzebę tworzenia podklas, a jednocześnie zapewnić potrzebną elastyczność.Myśl do przemyślenia: kiedy nasze klasy zawierają pewne czynniki danych, takie jak
OvertimeFactor
w twoim przykładzie, możesz potrzebować sposobu na pobranie tych informacji z innego źródła. Na przykład plik właściwości (ponieważ wygląda to jak Java) lub baza danych zawierałaby tę wartość, a użytkownikPaycheckCalculator
użyłby obiektu dostępu do danych do pobrania wartości. Umożliwia to właściwym osobom zmianę zachowania systemu bez konieczności przepisywania kodu.źródło