Refaktoryzacja i zasada otwarcia / zamknięcia

12

Niedawno czytam witrynę o czystym programowaniu (nie zamieszczam tutaj linku, ponieważ nie jest w języku angielskim).

Jedną z zasad reklamowanych na tej stronie jest zasada otwartej zamkniętej : każdy składnik oprogramowania powinien być otwarty na rozszerzenie i zamknięty na modyfikację. Na przykład, kiedy zaimplementowaliśmy i przetestowaliśmy klasę, powinniśmy ją modyfikować tylko w celu naprawienia błędów lub dodania nowej funkcjonalności (np. Nowych metod, które nie wpływają na istniejące). Istniejącej funkcjonalności i implementacji nie należy zmieniać.

Zazwyczaj stosuję tę zasadę, definiując interfejs Ii odpowiednią klasę implementacji A. Kiedy klasa Astała się stabilna (zaimplementowana i przetestowana), zwykle nie modyfikuję jej zbyt mocno (prawdopodobnie wcale), tj

  1. Jeśli pojawią się nowe wymagania (np. Wydajność lub zupełnie nowa implementacja interfejsu), które wymagają dużych zmian w kodzie, piszę nową implementację Bi używam Atak długo, jak długo Bnie jest dojrzała. Kiedy Bjest dojrzały, wystarczy zmienić sposób Itworzenia instancji.
  2. Jeśli nowe wymagania sugerują również zmianę interfejsu, definiuję nowy interfejs I'i nową implementację A'. Tak I, Asą zamrożone i pozostaje wdrożenie do systemu produkcyjnego jak długo I'i A'nie są wystarczająco stabilne, aby je zastąpić.

Tak więc, w świetle tych spostrzeżeń, byłem nieco zaskoczony, że strona internetowa zasugerowała następnie użycie złożonych refaktoryzacji : „... ponieważ nie jest możliwe napisanie kodu bezpośrednio w jego ostatecznej formie”.

Czy nie ma sprzeczności / konfliktu między egzekwowaniem zasady otwartej / zamkniętej a sugerowaniem stosowania złożonych refaktoryzacji jako najlepszej praktyki? A może chodzi o to, że podczas opracowywania klasy można zastosować złożone refaktoryzacje A, ale kiedy ta klasa zostanie pomyślnie przetestowana, należy ją zamrozić?

Giorgio
źródło

Odpowiedzi:

9

Uważam zasadę otwartego i zamkniętego za cel projektowy . Jeśli w końcu musisz go złamać, oznacza to, że twój początkowy projekt nie powiódł się, co z pewnością jest możliwe, a nawet prawdopodobne.

Refaktoryzacja oznacza, że ​​zmieniasz projekt bez zmiany funkcjonalności. Prawdopodobnie zmieniasz swój projekt, ponieważ jest z tym problem. Być może problem polega na tym, że podczas wprowadzania zmian w istniejącym kodzie trudno jest przestrzegać zasady otwartego zamknięcia i próbujesz to naprawić.

Być może robisz refaktoryzację, aby umożliwić wdrożenie kolejnej funkcji bez naruszenia OCP podczas jej wykonywania.

Scott Whitlock
źródło
Z pewnością nie powinieneś myśleć za jakąkolwiek zasadę jako cel projektu . Są narzędziami - nie sprawiasz, że oprogramowanie jest ładne i teoretycznie poprawne od wewnątrz, próbujesz wytworzyć wartość dla swojego klienta. Jest to wskazówka , nic więcej.
T. Sar
@ T.Sar Zasada jest wskazówką, do której dążysz, są one ukierunkowane na łatwość konserwacji i skalowalność. Dla mnie to wygląda na cel projektowy. Nie widzę zasady jako narzędzia w sposobie, w jaki postrzegam wzór lub ramę jako narzędzie.
Tulains Córdova
@ TulainsCórdova Utrzymanie, wydajność, poprawność, skalowalność - to są cele. Zasada otwartego i zamkniętego jest dla nich środkiem - tylko jednym z wielu. Nie musisz popychać czegoś w kierunku zasady otwartego zamkniętego, jeśli nie ma to zastosowania lub szkodzi to rzeczywistym celom projektu. Nie sprzedajesz klientowi „otwartego zamknięcia”. Jako zwykła wskazówka , nie jest lepsza zasada niż zasada, którą można odrzucić, jeśli w końcu znajdziesz sposób na zrobienie czegoś w bardziej czytelny i przejrzysty sposób. Wytyczne to w końcu narzędzia, nic więcej.
T. Sar
@ T.Sar Jest tak wiele rzeczy, których nie możesz sprzedać klientowi ... Z drugiej strony zgadzam się z tobą, że nie wolno robić rzeczy, które szkodzą celom projektu.
Tulains Córdova
9

Zasada Otwarte-Zamknięte jest raczej wskaźnikiem tego, jak dobrze zostało zaprojektowane twoje oprogramowanie ; nie jest to zasada, której należy przestrzegać dosłownie. Jest to także zasada, która pomaga nam uniknąć przypadkowej zmiany istniejących interfejsów (wywoływane przez ciebie klasy i metody oraz oczekiwanie, że będą działać).

Celem jest pisanie wysokiej jakości oprogramowania. Jedną z tych cech jest rozszerzalność. Oznacza to, że łatwo jest dodawać, usuwać i zmieniać kod, przy czym zmiany są ograniczone do jak najmniejszej liczby istniejących klas. Dodanie nowego kodu jest mniej ryzykowne niż zmiana istniejącego kodu, więc pod tym względem dobrze jest zrobić Open-Closed. Ale o jakim kodzie dokładnie mówimy? Przestępstwo polegające na naruszeniu OC jest znacznie mniejsze, gdy możesz dodać nowe metody do klasy zamiast zmieniać istniejące.

OC jest fraktalna . Jabłka na wszystkich głębokościach Twojego projektu. Wszyscy zakładają, że jest stosowany tylko na poziomie klasy. Ale ma to również zastosowanie na poziomie metody lub na poziomie zespołu.

Zbyt częste naruszanie OC na odpowiednim poziomie sugeruje, że być może nadszedł czas na refaktoryzację . „Odpowiedni poziom” to wezwanie do osądu, które ma wszystko wspólnego z twoim ogólnym projektem.

Dosłownie następujące „Otwarte-Zamknięte” oznacza, że ​​liczba klas wybuchnie. Niepotrzebnie utworzysz (duże „I”) interfejsy. Skończysz z częściami funkcjonalności rozłożonymi na klasy, a następnie będziesz musiał napisać dużo więcej kodu, aby połączyć to wszystko razem. W pewnym momencie zrozumie, że zmiana oryginalnej klasy byłaby lepsza.

radarbob
źródło
2
„Przestępstwo polegające na naruszeniu OC jest o wiele mniejsze, gdy można dodawać nowe metody do klasy zamiast zmieniać istniejące.”: O ile rozumiem, dodawanie nowych metod nie narusza zasady OC (otwartej na rozszerzenie) w ogóle . Problemem jest zmiana istniejących metod, które implementują dobrze zdefiniowany interfejs, a zatem mają już dobrze zdefiniowaną semantykę (zamknięta dla modyfikacji). Zasadniczo refaktoryzacja nie zmienia semantyki, więc jedyne ryzyko, jakie widzę, to wprowadzanie błędów w już stabilnym i dobrze przetestowanym kodzie.
Giorgio
1
Oto odpowiedź CodeReview, która ilustruje możliwość rozszerzenia . Ta konstrukcja klasy jest rozszerzalna. Natomiast dodanie metody modyfikuje klasę.
radarbob,
Dodanie nowych metod narusza LSP, a nie OCP.
Tulains Córdova
1
Dodanie nowych metod nie narusza LSP. Jeśli dodasz metodę, wprowadziłeś nowy interfejs @ TulainsCórdova
RubberDuck
6

Wydaje się, że zasada Open-Closed była bardziej rozpowszechniona. Chodzi o to, że refaktoryzacja kodu jest ryzykowna, ponieważ możesz coś zepsuć, więc bezpieczniej jest pozostawić istniejący kod w obecnej postaci i po prostu go dodać. W przypadku braku testów ma to sens. Minusem tego podejścia jest zanik kodu. Za każdym razem, gdy rozszerzasz klasę, zamiast ją refaktoryzować, dostajesz dodatkową warstwę. Po prostu nakładasz kod na wierzch. Za każdym razem, gdy naciskasz więcej kodu, zwiększasz szansę na powielenie. Wyobrażać sobie; w mojej bazie kodu jest usługa, z której chcę korzystać, okazuje się, że nie ma tego, czego chcę, dlatego tworzę nową klasę, aby ją rozszerzyć i uwzględnić moją nową funkcjonalność. Inny programista pojawia się później i również chce skorzystać z tej samej usługi. Niestety nie Zdaję sobie sprawę, że moja rozszerzona wersja istnieje. Kodują w stosunku do oryginalnej implementacji, ale potrzebują także jednej z kodowanych przeze mnie funkcji. Zamiast korzystać z mojej wersji, teraz rozszerzają implementację i dodają nową funkcję. Teraz mamy 3 klasy, oryginalną i dwie nowe wersje, które mają zdublowane funkcje. Postępuj zgodnie z zasadą otwartego / zamkniętego, a to powielanie będzie się powiększać przez cały czas trwania projektu, co prowadzi do niepotrzebnie złożonej bazy kodu.

Dzięki dobrze przetestowanemu systemowi nie ma potrzeby atrofii tego kodu, możesz bezpiecznie refaktoryzować kod, umożliwiając projektowi przyswojenie nowych wymagań zamiast konieczności ciągłego wprowadzania nowego kodu. Ten styl rozwoju nazywa się wschodzącym projektowaniem i prowadzi do baz kodu, które są w stanie zachować dobrą formę przez cały okres użytkowania, zamiast stopniowo gromadzić cruft.

opsb
źródło
1
Nie jestem zwolennikiem zasady otwartego zamknięcia ani TDD (w tym sensie, że ich nie wymyśliłem). Zaskoczyło mnie to, że ktoś zaproponował jednocześnie zasadę otwartego zamknięcia ORAZ zastosowanie refaktoryzacji ORAZ TDD. Wydawało mi się to sprzeczne, dlatego próbowałem wymyślić, jak połączyć wszystkie te wytyczne w spójny proces.
Giorgio
„Pomysł polega na tym, że refaktoryzacja kodu jest ryzykowna, ponieważ możesz coś zepsuć, więc bezpieczniej jest pozostawić istniejący kod w obecnej postaci i po prostu go dodać.”: Właściwie nie widzę tego w ten sposób. Chodzi raczej o to, aby mieć małe, niezależne jednostki, które można wymienić lub rozszerzyć (umożliwiając w ten sposób rozwój oprogramowania), ale nie należy dotykać każdej jednostki po jej dokładnym przetestowaniu.
Giorgio
Musisz pomyśleć, że klasa będzie używana nie tylko w twojej bazie kodu. Bibliotekę, którą piszesz, można wykorzystać w innych projektach. OCP jest więc ważny. Ponadto nowy programista, który nie zna rozszerzającej się klasy z funkcjonalnością, której potrzebuje, jest problemem w komunikacji / dokumentacji, a nie problemem projektowym.
Tulains Córdova
@ TulainsCórdova w kodzie aplikacji to nie dotyczy. W przypadku kodu bibliotecznego argumentowałbym, że wersja semantyczna lepiej pasowała do komunikowania przełomowych zmian.
opsb
1
@ TulainsCórdova ze stabilnością API kodu biblioteki jest znacznie ważniejsza, ponieważ nie można przetestować kodu klienta. Dzięki kodowi aplikacji Twój zasięg testowy natychmiast poinformuje Cię o wszelkich awariach. Innymi słowy, kod aplikacji może dokonywać przełomowych zmian bez ryzyka, podczas gdy kod biblioteki musi zarządzać ryzykiem, utrzymując stabilny interfejs API i sygnalizując
awarie
6

Słowami laika:

A. Zasada O / C oznacza, że specjalizacji należy dokonać poprzez rozszerzenie, a nie modyfikację klasy w celu dostosowania do specjalnych potrzeb.

B. Dodanie brakującej (niespecjalistycznej) funkcjonalności oznacza, że ​​projekt nie był kompletny i musisz dodać go do klasy podstawowej, oczywiście bez naruszenia umowy. Myślę, że to nie narusza zasady.

C. Refaktoryzacja nie narusza zasady.

Kiedy projekt dojrzewa , powiedzmy, po pewnym czasie w produkcji:

  • Powodów tego powinno być bardzo mało (punkt B), z czasem zmierzających do zera.
  • (Punkt C) zawsze będzie możliwy, choć rzadziej.
  • Cała nowa funkcjonalność ma być specjalizacją, co oznacza, że ​​klasy muszą być rozszerzone (dziedziczone z) (punkt A).
Tulains Córdova
źródło
Zasada otwarta / zamknięta jest bardzo źle rozumiana. Twoje punkty A i B dokładnie to robią.
gnasher729,
1

Według mnie Zasada Otwartego Zamknięcia jest wytyczną, a nie twardą i szybką zasadą.

Jeśli chodzi o otwartą część zasady, końcowe klasy w Javie i klasy w C ++ ze wszystkimi konstruktorami zadeklarowanymi jako prywatne naruszają otwartą część zasady otwartego zamknięcia. Istnieją dobre solidne przypadki użycia (uwaga: stałe, a nie SOLIDNE) dla klas końcowych. Ważne jest zaprojektowanie rozszerzalności. Jednak wymaga to dalekowzroczności i wysiłku, a ty zawsze omijasz linię naruszania YAGNI (nie będziesz go potrzebował) i wstrzykujesz kodowy zapach spekulatywnej ogólności. Czy kluczowe elementy oprogramowania powinny być otwarte do rozszerzenia? Tak. Wszystko? Nie. To samo w sobie jest spekulatywną ogólnością.

Jeśli chodzi o część zamkniętą, przechodząc z wersji 2.0 na 2.1 do 2.2 na 2.3 niektórych produktów, niezmodyfikowanie zachowania jest bardzo dobrym pomysłem. Użytkownikom naprawdę się to nie podoba, gdy każda niewielka wersja łamie własny kod. Jednak po drodze często stwierdza się, że początkowa implementacja w wersji 2.0 została zasadniczo złamana lub że ograniczenia zewnętrzne, które ograniczały początkowy projekt, nie mają już zastosowania. Czy uśmiechasz się i nosisz go i utrzymujesz ten projekt w wersji 3.0, czy też czynisz wersję 3.0 niekompatybilną pod jakimś względem? Kompatybilność wsteczna może być ogromnym ograniczeniem. Główne granice wydań są miejscem, w którym dopuszcza się łamanie wstecznej kompatybilności. Trzeba uważać, że może to spowodować zdenerwowanie użytkowników. Konieczne jest uzasadnienie, dlaczego konieczne jest zerwanie z przeszłością.

David Hammen
źródło
0

Refaktoryzacja z definicji zmienia strukturę kodu bez zmiany zachowania. A więc, gdy dokonujesz refaktoryzacji, nie dodajesz nowych funkcji.

To, co zrobiłeś jako przykład dla zasady Open Close, brzmi OK. Ta zasada dotyczy rozszerzenia istniejącego kodu o nowe funkcje.

Nie zrozum jednak źle tej odpowiedzi. Nie sugeruję, że powinieneś robić tylko funkcje lub refaktoryzować tylko duże fragmenty danych. Najczęstszym sposobem programowania jest wykonanie trochę funkcji niż natychmiastowe wykonanie refaktoryzacji (w połączeniu z testami, oczywiście, aby upewnić się, że nie zmieniłeś żadnego zachowania). Złożone refaktoryzacja nie oznacza „dużego” refaktoryzacji, oznacza stosowanie skomplikowanych i dobrze przemyślanych technik refaktoryzacji.

O zasadach SOLID. Są to naprawdę dobre wytyczne dotyczące tworzenia oprogramowania, ale nie są to religijne reguły, których należy ślepo przestrzegać. Czasami wiele razy, po dodaniu drugiej, trzeciej i n-tej funkcji, zdajesz sobie sprawę, że twój początkowy projekt, nawet jeśli szanuje Open-Close, nie jest zgodny z innymi zasadami lub wymaganiami oprogramowania. Ewolucja projektu i oprogramowania wymaga pewnych bardziej złożonych zmian. Chodzi o to, aby jak najszybciej znaleźć i zrealizować te problemy i jak najlepiej zastosować techniki refaktoryzacji.

Nie ma czegoś takiego jak idealny design. Nie ma takiego projektu, który mógłby i powinien być zgodny z wszystkimi istniejącymi zasadami lub wzorami. To jest utopia kodowania.

Mam nadzieję, że ta odpowiedź pomogła ci w twoim dylemacie. W razie potrzeby możesz poprosić o wyjaśnienia.

Patkos Csaba
źródło
1
„Więc kiedy dokonujesz refaktoryzacji, nie dodajesz nowych funkcji.”: Ale mogłem wprowadzić błędy w testowanym oprogramowaniu.
Giorgio
„Czasami wiele razy, po dodaniu drugiej, trzeciej i n-tej funkcji, zdajesz sobie sprawę, że twój początkowy projekt, nawet jeśli szanuje Open-Close, nie respektuje innych zasad ani wymagań programowych.”: Właśnie wtedy zacznij pisać nową implementację, Ba kiedy będzie gotowa, zastąp starą implementację Anową implementacją B(to jedno użycie interfejsów). AKod może służyć jako podstawa dla Bkodu, a następnie mogę używać refaktoryzacji Bkodu podczas jego tworzenia, ale myślę, że już przetestowany Akod powinien pozostać zamrożony.
Giorgio,
@Giorgio Kiedy refaktoryzujesz, możesz wprowadzać błędy, dlatego piszesz testy (a nawet lepiej TDD). Najbezpieczniejszym sposobem na refaktoryzację jest zmiana kodu, gdy wiesz, że działa. Wiesz o tym, przechodząc zestaw testów. Po zmianie kodu produkcyjnego testy muszą przejść pomyślnie, więc wiesz, że nie wprowadziłeś błędu. I pamiętaj, testy są równie ważne jak kod produkcyjny, więc stosujesz wobec nich tę samą regułę, co kod produkcyjny, utrzymujesz je w czystości i okresowo i często je refaktoryzujesz.
Patkos Csaba
@Giorgio Jeśli kod Bjest zbudowany na kodzie Ajako ewolucja A, Bto kiedy zostanie wydany, Apowinien zostać usunięty i nigdy więcej nie użyty. Klienci, którzy wcześniej używali, Abędą po prostu korzystać Bbez wiedzy o zmianie, ponieważ interfejs Inie został zmieniony (może trochę Zasada Podstawienia
Liskowa
Tak, właśnie to miałem na myśli: nie wyrzucaj działającego kodu, dopóki nie uzyskasz prawidłowego (dobrze przetestowanego) zamiennika.
Giorgio
-1

Zgodnie z moim rozumieniem - jeśli dodasz nowe metody do istniejącej klasy, nie spowoduje to uszkodzenia OCP. jestem jednak trochę pomylony z dodaniem nowych zmiennych w klasie. Ale jeśli zmienisz istniejącą metodę i parametry w istniejącej metodzie, to z pewnością zepsuje OCP, ponieważ kod jest już przetestowany i przekazany, jeśli celowo zmienimy metodę [Gdy zmiana wymagań], to będzie problem.

Narender Parmar
źródło