Czy wykorzystujesz zalety zasady otwartego zamknięcia?

12

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 OvertimeFactorzmiany 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.

Kaleb Pederson
źródło

Odpowiedzi:

10

Jeśli modyfikujesz klasę podstawową, to tak naprawdę nie jest ona zamknięta!

Pomyśl o sytuacji, w której udostępniłeś bibliotekę światu. Jeśli zmienisz zachowanie swojej klasy podstawowej, zmieniając współczynnik nadgodzin na 1,5, naruszyłeś wszystkie osoby, które używają twojego kodu, zakładając, że klasa została zamknięta.

Naprawdę, aby klasa była zamknięta, ale otwarta, powinieneś pobierać współczynnik nadgodzin z alternatywnego źródła (może plik konfiguracyjny) lub udowadniać wirtualną metodę, którą można zastąpić?

Jeśli klasa była naprawdę zamknięta, to po twojej zmianie żadne przypadki testowe nie zawiodłyby (zakładając, że masz 100% pokrycia wszystkimi swoimi testami) i zakładam, że istnieje przypadek testowy, który sprawdza GetOvertimeFactor() == 2.0M.

Nie przesadzaj z inżynierem

Ale nie bierz tej zasady „otwórz-zamknij” do logicznego wniosku i miej wszystko do skonfigurowania od samego początku (to jest ponad inżynierią). Zdefiniuj tylko te bity, których aktualnie potrzebujesz.

Zasada zamknięta nie wyklucza ponownego zaprojektowania obiektu. To po prostu uniemożliwia zmianę aktualnie zdefiniowanego interfejsu publicznego na obiekt ( chronione elementy są częścią interfejsu publicznego). Nadal możesz dodać więcej funkcji, o ile stara funkcja nie jest zepsuta.

Martin York
źródło
„Zasada zamknięta nie wyklucza ponownego zaprojektowania obiektu”. Właściwie to robi . Jeśli przeczytasz książkę, w której po raz pierwszy zaproponowano zasadę otwartego zamknięcia, lub artykuł, który wprowadził akronim „OCP”, zobaczysz, że napisano: „Nikt nie może wprowadzać w nim zmian w kodzie źródłowym” (z wyjątkiem błędu poprawki).
Rogério,
@ Rogério: To może być prawda (w 1988 r.). Ale obecna definicja (upowszechniona w 1990 r., Kiedy OO stała się popularna) dotyczy utrzymania spójnego interfejsu publicznego. 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_principle
Martin York
Dzięki za odniesienie do Wikipedii. Nie jestem jednak pewien, czy „obecna” definicja jest naprawdę inna, ponieważ nadal opiera się na dziedziczeniu typu (klasy lub interfejsu). A ten cytat o „braku zmian kodu źródłowego”, o którym wspomniałem, pochodzi z artykułu Roberta Martina z OCP 1996, który (podobno) jest zgodny z „obecną definicją”. Osobiście uważam, że Zasada Otwartego Zamknięcia zostałaby już zapomniana, gdyby Martin nie nadał jej akronimu, który najwyraźniej ma dużą wartość marketingową. Sama zasada jest przestarzała i szkodliwa, IMO.
Rogério,
3

Tak 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.

Michael Brown
źródło
2

W opublikowanym przykładzie współczynnik nadgodzin powinien być zmienną lub stałą. * (Przykład Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

LUB

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

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.

Michael K.
źródło
To zmiana danych, a nie zmiana kodu. Nadgodziny nie powinny być zakodowane na stałe.
Jim C
Wygląda na to, że masz Get i Set do tyłu.
Mason Wheeler
Ups! powinienem był przetestować ...
Michael K
2

Naprawdę nie widzę twojego przykładu jako doskonałej reprezentacji OCP. Myślę, że tak naprawdę oznacza to:

Jeśli chcesz dodać funkcję, powinieneś dodać tylko jedną klasę i nie musisz modyfikować żadnej innej klasy (ale prawdopodobnie pliku konfiguracyjnego).

Poniżej słaba implementacja. Za każdym razem, gdy dodajesz grę, musisz modyfikować klasę GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

Klasa GamePlayer nigdy nie powinna wymagać modyfikacji

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

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 Gameklasie 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.

Austin Salonen
źródło
1

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.

Jest bardziej niż prawdopodobne, że określiłeś zachowanie zbyt wcześnie w swojej hierarchii klas.

Powiedzmy, że mamy PaycheckCalculator. OvertimeFactorBę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 PaycheckCalculatorklasie podstawowej uczynisz ją abstrakcyjną i określisz metody, których oczekujesz. Podstawowe obliczenia są takie same, tylko niektóre czynniki są obliczane inaczej. Twój HourlyPaycheckCalculatorby następnie wdrożyć getOvertimeFactormetody i powrót 1.5 lub 2.0, jak sprawa może być. Twój StraightTimePaycheckCalculatorzaimplementuje getOvertimeFactorzwrot 1.0. Wreszcie trzecia implementacja NoOvertimePaycheckCalculatorbyłaby implementacją getOvertimeFactorzwrotu 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ą getOvertimeFactorprowadzi 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 OvertimeFactorw 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żytkownik PaycheckCalculatoruż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.

Berin Loritsch
źródło