Jak stosować w praktyce zasadę otwartego i zamkniętego

14

Rozumiem intencję zasady otwartego zamknięcia. Ma to na celu zmniejszenie ryzyka zepsucia czegoś, co już działa, podczas modyfikowania go, poprzez nakazanie próby rozszerzenia bez modyfikacji.

Miałem jednak pewne problemy ze zrozumieniem, w jaki sposób ta zasada jest stosowana w praktyce. W moim rozumieniu istnieją dwa sposoby zastosowania tego. Przed i po możliwej zmianie:

  1. Przedtem: programuj abstrakcje i „przewiduj przyszłość” tak bardzo, jak potrafisz. Na przykład metoda drive(Car car)będzie musiała ulec zmianie, jeśli Motorcycles zostaną dodane do systemu w przyszłości, więc prawdopodobnie narusza OCP. Jednak metoda drive(MotorVehicle vehicle)ta prawdopodobnie nie będzie musiała ulec zmianie w przyszłości, więc jest zgodna z OCP.

    Jednak dość trudno jest przewidzieć przyszłość i wiedzieć z wyprzedzeniem, jakie zmiany zostaną wprowadzone w systemie.

  2. Po: gdy potrzebna jest zmiana, rozszerz klasę zamiast modyfikować jej bieżący kod.

Ćwiczenie nr 1 nie jest trudne do zrozumienia. Jednak jest to praktyka nr 2, że mam problem ze zrozumieniem, jak się zgłosić.

Na przykład (wziąłem go z video na YouTube): powiedzmy, że mamy metodę w klasie, który akceptuje CreditCardobiekty: makePayment(CraditCard card). Jeden dzień Vouchersą dodawane do systemu. Ta metoda ich nie obsługuje, więc należy go zmodyfikować.

Wdrażając metodę, nie udało nam się przewidzieć przyszłości i program w bardziej abstrakcyjny sposób (np. makePayment(Payment pay)Teraz musimy zmienić istniejący kod.

Ćwiczenie nr 2 mówi, że powinniśmy dodać tę funkcjonalność, rozszerzając zamiast modyfikować. Co to znaczy? Czy powinienem podklasować istniejącą klasę zamiast po prostu zmieniać jej istniejący kod? Czy powinienem zrobić wokół niego jakieś opakowanie, aby uniknąć przepisywania kodu?

Czy też zasada nie odnosi się nawet do „jak poprawnie zmodyfikować / dodać funkcjonalność”, ale raczej do „jak uniknąć konieczności wprowadzania zmian w pierwszej kolejności (tj. Programu do abstrakcji)?

Aviv Cohn
źródło
1
Zasada Otwarta / Zamknięta nie dyktuje mechanizmu, którego używasz. Dziedziczenie jest zwykle złym wyborem. Nie da się też chronić przed wszystkimi przyszłymi zmianami. Najlepiej nie próbować przewidywać przyszłości, ale gdy potrzebna jest zmiana, zmodyfikuj projekt, aby możliwe było uwzględnienie przyszłych zmian tego samego rodzaju.
Doval

Odpowiedzi:

14

Zasady projektowania zawsze muszą być zrównoważone. Nie możesz przewidzieć przyszłości, a większość programistów robi to okropnie, kiedy próbują. Dlatego mamy zasadę trzech , która dotyczy przede wszystkim powielania, ale dotyczy również refaktoryzacji w przypadku innych zasad projektowania.

Gdy masz tylko jedną implementację interfejsu, nie musisz się zbytnio przejmować OCP, chyba że jest to krystalicznie jasne, gdzie miałyby miejsce jakieś rozszerzenia. W rzeczywistości często tracisz jasność podczas próby przeprojektowania w tej sytuacji. Po jednorazowym przedłużeniu zmieniamy go tak, aby uczynić go przyjaznym dla OCP, jeśli jest to najłatwiejszy i najczystszy sposób. Po rozszerzeniu go na trzecią implementację pamiętaj o jego refaktoryzacji, biorąc pod uwagę OCP, nawet jeśli wymaga to nieco więcej wysiłku.

W praktyce, gdy masz tylko dwie implementacje, refaktoryzacja po dodaniu trzeciej zwykle nie jest zbyt trudna. To wtedy, gdy pozwalasz mu przekroczyć ten punkt, utrzymanie go staje się trudne.

Karl Bielefeldt
źródło
1
Dzięki za odpowiedź. Zobaczę, czy rozumiem, co mówisz: mówisz, że powinienem dbać o OCP głównie po tym, jak zmuszono mnie do wprowadzenia zmian w klasie. Czyli: wdrażając klasę po raz pierwszy, nie powinienem martwić się o OCP, ponieważ i tak trudno przewidzieć przyszłość. Kiedy muszę go rozszerzyć / zmodyfikować po raz pierwszy, być może dobrym pomysłem jest przebudowanie go trochę, aby był bardziej elastyczny w przyszłości (więcej OCP). I po raz trzeci muszę rozszerzyć / zmodyfikować klasę, nadszedł czas na trochę refaktoryzacji, aby była bardziej zgodna z OCP. Czy to masz na myśli?
Aviv Cohn
1
To jest pomysł.
Karl Bielefeldt
2

Myślę, że patrzysz zbyt daleko w przyszłość. Rozwiąż bieżący problem w elastyczny sposób, przylegający do otwarcia / zamknięcia.

Powiedzmy, że musisz zaimplementować drive(Car car)metodę. W zależności od języka masz kilka opcji.

  • W przypadku języków obsługujących przeciążanie (C ++), po prostu użyj drive(const Car& car)

    W pewnym momencie możesz potrzebować drive(const Motorcycle& motorcycle), ale nie będzie to przeszkadzało drive(const Car& car). Nie ma problemu!

  • W przypadku języków, które nie obsługują przeciążania (Cel C), należy podać nazwę typu w metodzie -driveCar:(Car *)car.

    W pewnym momencie możesz potrzebować -driveMotorcycle:(Motorcycle *)motorcycle, ale znowu nie będzie to przeszkadzało.

Pozwala drive(Car car)to być zamknięte na modyfikacje, ale jest otwarte na rozszerzenie na inne typy pojazdów. To minimalistyczne planowanie przyszłości, które pozwala wykonać pracę dzisiaj, ale powstrzymuje cię przed blokowaniem się w przyszłości.

Próba wyobrażenia sobie najbardziej podstawowych typów, których potrzebujesz, może doprowadzić do nieskończonego regresu. Co się dzieje, gdy chcesz prowadzić Segue, rowerem lub odrzutowcem Jumbo. Jak zbudować jeden ogólny typ abstrakcyjny, który może uwzględniać wszystkie urządzenia, z których ludzie korzystają i korzystają z mobilności?

Jeffery Thomas
źródło
Modyfikowanie klasy w celu dodania nowej metody narusza zasadę otwartego-zamkniętego. Twoja sugestia eliminuje również możliwość zastosowania zasady zastępowania Liskowa do wszystkich pojazdów, które mogą prowadzić, co zasadniczo eliminuje jedną z najsilniejszych części OO.
Dunk
@Dunk Swoją odpowiedź oparłem na polimorficznej zasadzie otwartego / zamkniętego, a nie ścisłej zasadzie otwartej / zamkniętej Meyera. Dozwolone jest aktualizowanie klas w celu obsługi nowych interfejsów. W tym przykładzie interfejs samochodu jest oddzielony od interfejsu motocykla. Można je sformalizować jako osobne abstrakcyjne klasy jazdy dla samochodów i motocykli, które klasa wspierająca mogłaby wspierać.
Jeffery Thomas
@Dunk Zasada substytucji Liskowa jest przydatna, ale nie jest bezpłatna. Jeśli oryginalna specyfikacja wymaga tylko samochodu, stworzenie bardziej ogólnego pojazdu może nie być warte dodatkowych kosztów pieniędzy, czasu i złożoności. Ponadto jest mało prawdopodobne, aby bardziej ogólny pojazd był doskonale przystosowany do obsługi nieplanowanych podklas. Albo interfejs motocykla będzie musiał zostać podłączony do interfejsu pojazdu (który został zaprojektowany tylko do obsługi samochodu), lub będziesz musiał zmodyfikować pojazd do obsługi motocykla (prawdziwe naruszenie otwarcia / zamknięcia).
Jeffery Thomas
Zasada Liskov-Substitution nie jest darmowa, ale również nie wiąże się z dużymi kosztami. I zwykle zwraca dużo więcej niż kiedykolwiek kosztuje wiele razy, nawet jeśli kolejna podklasa nigdy nie jest dziedziczona z niej w głównej aplikacji. Zastosowanie LSP sprawia, że ​​automatyczne testowanie jest znacznie łatwiejsze, co już jest sukcesem. Ponadto, chociaż na pewno nie powinieneś szaleć i zakładać, że wszystko będzie potrzebować LSP, jeśli tworzysz aplikację i nie masz pojęcia, co może jej potrzebować w przyszłej wersji, to nie wiedzieć wystarczająco dużo o swojej aplikacji lub jej domenie.
Dunk
1
W odniesieniu do definicji OCP. Mogą to być branże, w których pracowałem, które wymagają wyższego poziomu weryfikacji niż zwykła firma komercyjna, ale ogólnie mówiąc, jeśli plik / klasa się zmieni, to nie tylko musisz ponownie przetestować plik / klasę, ale wszystko, co korzysta z tego pliku / klasy w testach regresji. Nie ma więc znaczenia, czy ktoś mówi, że polimorficzne otwieranie / zamykanie jest w porządku, zmiana interfejsu ma szerokie konsekwencje, więc nie jest tak dobrze.
Dunk
2

Rozumiem intencję zasady otwartego zamknięcia. Ma to na celu zmniejszenie ryzyka zepsucia czegoś, co już działa, podczas modyfikowania go, poprzez nakazanie próby rozszerzenia bez modyfikacji.

Chodzi również o to, aby nie zniszczyć wszystkich obiektów, które korzystają z tej metody, nie zmieniając zachowania już istniejących obiektów. Gdy obiekt zareklamuje zmianę zachowania, jest to ryzykowne, ponieważ zmieniasz znane i oczekiwane zachowanie obiektu, nie wiedząc dokładnie, czego inne obiekty oczekują od tego zachowania.

Co to znaczy? Czy powinienem podklasować istniejącą klasę zamiast po prostu zmieniać jej istniejący kod?

Tak.

„Akceptuje tylko karty kredytowe” jest zdefiniowane jako część zachowania tej klasy poprzez jej interfejs publiczny. Programista oświadczył światu, że metoda tego obiektu przyjmuje tylko karty kredytowe. Zrobiła to za pomocą niezbyt jasnej nazwy metody, ale już zrobione. Reszta systemu polega na tym.

W tamtym czasie mogło to mieć sens, ale teraz, jeśli trzeba to zmienić, powinieneś stworzyć nową klasę, która akceptuje rzeczy inne niż karty kredytowe.

Nowe zachowanie = nowa klasa

Na marginesie - dobrym sposobem przewidywania przyszłości jest zastanowienie się nad nazwą, którą nadałeś metodzie. Czy podałeś naprawdę ogólną nazwę brzmiącą metodę, taką jak makePayment, dla metody z określonymi regułami w metodzie co do tego, jaką dokładnie płatności może dokonać? To zapach kodu. Jeśli masz określone reguły, należy je wyraźnie zaznaczyć w nazwie metody - makePayment powinien być makeCreditCardPayment. Zrób to, gdy piszesz obiekt po raz pierwszy, a inni programiści ci za to podziękują.

Cormac Mulhall
źródło