Mam projekt. W tym projekcie chciałem przefaktoryzować go, aby dodać funkcję, i przebudowałem projekt, aby dodać funkcję.
Problem polega na tym, że kiedy skończyłem, okazało się, że muszę wprowadzić niewielką zmianę interfejsu, aby to uwzględnić. Więc dokonałem zmiany. I wtedy klasa konsumująca nie może zostać zaimplementowana z obecnym interfejsem pod względem nowego, więc potrzebuje również nowego interfejsu. Teraz minęły trzy miesiące i musiałem naprawić niezliczone, praktycznie niezwiązane ze sobą problemy, i patrzę na rozwiązywanie problemów, które zostały zaplanowane na rok od teraz lub po prostu wymienione jako nie naprawione z powodu trudności, zanim rzecz się skompiluje jeszcze raz.
Jak mogę uniknąć tego rodzaju refaktoryzacji kaskadowej w przyszłości? Czy to tylko symptom moich poprzednich zajęć, które zbyt mocno od siebie zależą?
Krótka edycja: w tym przypadku refaktorem była cecha, ponieważ refaktor zwiększył rozszerzalność określonego fragmentu kodu i zmniejszył pewne sprzężenie. Oznaczało to, że zewnętrzni programiści mogli zrobić więcej, co było funkcją, którą chciałem dostarczyć. Tak więc sam pierwotny refaktor nie powinien być zmianą funkcjonalną.
Większa edycja, którą obiecałem pięć dni temu:
Zanim zacząłem ten refaktor, miałem system, w którym miałem interfejs, ale w implementacji po prostu dynamic_cast
przechodziłem przez wszystkie możliwe implementacje, które wysłałem. To oczywiście oznaczało, że nie można po prostu odziedziczyć po interfejsie, po drugie, a po drugie, nie byłoby możliwe, aby ktokolwiek bez dostępu do implementacji implementował ten interfejs. Zdecydowałem więc, że chcę rozwiązać ten problem i otworzyć interfejs do publicznego użytku, aby każdy mógł go wdrożyć, a wdrożenie interfejsu było wymaganiem całej umowy - oczywiście poprawa.
Kiedy znajdowałem i zabijałem ogniem wszystkie miejsca, które to zrobiłem, znalazłem jedno miejsce, które okazało się być szczególnym problemem. Zależało to od szczegółów implementacji wszystkich różnych klas pochodnych i zduplikowanych funkcji, które zostały już zaimplementowane, ale lepiej gdzie indziej. Zamiast tego mógł zostać zaimplementowany w postaci interfejsu publicznego i ponownie wykorzystać istniejącą implementację tej funkcjonalności. Odkryłem, że do poprawnego działania wymagał określonego kontekstu. Z grubsza mówiąc, wywołanie poprzedniej implementacji wyglądało trochę jak
for(auto&& a : as) {
f(a);
}
Jednak, aby uzyskać ten kontekst, musiałem zmienić go na coś bardziej podobnego
std::vector<Context> contexts;
for(auto&& a : as)
contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
f(con);
Oznacza to, że dla wszystkich operacji, które kiedyś były częścią f
, niektóre z nich muszą stać się częścią nowej funkcji, g
która działa bez kontekstu, a niektóre z nich muszą być częścią części odroczonej f
. Ale nie wszystkie metody f
nazywają potrzebę lub chcą tego kontekstu - niektóre z nich potrzebują odrębnego kontekstu, który uzyskują osobnymi środkami. Więc dla wszystkiego, co f
kończy się dzwonieniem (czyli, mówiąc z grubsza, prawie wszystko ), musiałem ustalić, jaki, jeśli w ogóle, potrzebny im kontekst, skąd go wziąć i jak podzielić je ze starego f
na nowe f
i nowe g
.
I tak skończyłem tam, gdzie teraz jestem. Jedynym powodem, dla którego kontynuowałem, jest to, że i tak potrzebowałem tego refaktoryzacji z innych powodów.
Odpowiedzi:
Ostatnim razem, gdy próbowałem rozpocząć refaktoryzację z nieprzewidzianymi konsekwencjami, i nie mogłem ustabilizować kompilacji i / lub testów po jednym dniu , poddałem się i przywróciłem bazę kodu do punktu przed refaktoryzacją.
Następnie zacząłem analizować, co poszło nie tak, i opracowałem lepszy plan, jak wykonać refaktoryzację w mniejszych krokach. Więc moja rada dotycząca unikania refaktoryzacji kaskadowej jest po prostu: wiedz, kiedy przestać , nie pozwól, aby sprawy wymknęły się spod kontroli!
Czasami musisz ugryźć kulę i odrzucić cały dzień pracy - zdecydowanie łatwiej niż wyrzucić trzy miesiące pracy. Dzień, w którym tracisz, nie jest całkowicie próżny, przynajmniej nauczyłeś się, jak nie podchodzić do problemu. I z mojego doświadczenia wynika, że zawsze można zrobić mniejsze kroki w refaktoryzacji.
Uwaga dodatkowa : wydaje się, że jesteś w sytuacji, w której musisz zdecydować, czy chcesz poświęcić pełne trzy miesiące pracy i zacząć od nowa z nowym (i mam nadzieję, że bardziej udanym) planem refaktoryzacji. Mogę sobie wyobrazić, że nie jest to łatwa decyzja, ale zadaj sobie pytanie, jak wysokie jest ryzyko, że potrzebujesz kolejnych trzech miesięcy nie tylko w celu ustabilizowania kompilacji, ale także w celu usunięcia wszystkich nieprzewidzianych błędów, które prawdopodobnie wprowadziłeś podczas przepisywania, które zrobiłeś w ciągu ostatnich trzech miesięcy ? Napisałem „przepisz”, ponieważ myślę, że tak właśnie zrobiłeś, a nie „refaktoryzacja”. Nie jest wykluczone, że możesz szybciej rozwiązać obecny problem, wracając do ostatniej wersji, w której projekt się kompiluje, i zacznij od prawdziwego refaktoryzacji (w przeciwieństwie do „przepisywania”) ponownie.
źródło
Pewnie. Jedna zmiana powodująca mnóstwo innych zmian jest właściwie definicją sprzężenia.
W najgorszym rodzaju baz kodowych jedna zmiana będzie się kaskadować, ostatecznie powodując zmianę (prawie) wszystkiego. Częścią każdego refaktora, w którym występuje powszechne połączenie, jest izolowanie części, nad którą pracujesz. Konieczne jest dokonanie refaktoryzacji nie tylko w miejscu, w którym nowa funkcja dotyka tego kodu, ale w miejscu, gdzie wszystko inne dotyka tego kodu.
Zwykle oznacza to, że niektóre adaptery pomagają staremu kodowi pracować z czymś, co wygląda i działa jak stary kod, ale korzysta z nowej implementacji / interfejsu. W końcu jeśli wszystko, co robisz, to zmieniasz interfejs / implementację, ale zostawiasz połączenie, nic nie zyskujesz. To szminka na świni.
źródło
Wygląda na to, że refaktoryzacja była zbyt ambitna. Refaktoryzacja powinna być wykonywana małymi krokami, z których każdy może zostać ukończony w (powiedzmy) 30 minut - lub, w najgorszym przypadku, co najwyżej dziennie - i pozostawia projekt do zbudowania, a wszystkie testy wciąż trwają.
Jeśli minimalizujesz każdą indywidualną zmianę, naprawdę nie powinno być możliwe, aby refaktoryzacja zepsuła twoją kompilację na długi czas. Najgorszym przypadkiem jest prawdopodobnie zmiana parametrów na metodę w powszechnie używanym interfejsie, np. W celu dodania nowego parametru. Ale wynikające z tego zmiany są mechaniczne: dodanie (i zignorowanie) parametru w każdej implementacji oraz dodanie wartości domyślnej w każdym wywołaniu. Nawet jeśli istnieją setki referencji, wykonanie takiego refaktoryzacji nie powinno zająć nawet jednego dnia.
źródło
Myślenie życzeniowe
Celem jest doskonały projekt i wdrożenie OO nowej funkcji. Celem jest także unikanie refaktoryzacji.
Zacznij od zera i zaprojektuj nową funkcję, która jest tym, czego sobie życzysz. Nie spiesz się, aby zrobić to dobrze.
Zauważ jednak, że kluczem jest tutaj „dodaj funkcję”. Nowe rzeczy pozwalają nam w dużej mierze ignorować obecną strukturę bazy kodu. Nasz projekt myślenia życzeniowego jest niezależny. Ale potrzebujemy jeszcze dwóch rzeczy:
Heurystyka, wyciągnięte wnioski itp.
Refaktoryzacja była tak prosta, jak dodanie domyślnego parametru do istniejącego wywołania metody; lub pojedyncze wywołanie metody klasy statycznej.
Metody rozszerzenia istniejących klas mogą pomóc utrzymać jakość nowego projektu przy absolutnie minimalnym ryzyku.
„Struktura” jest wszystkim. Struktura jest realizacją zasady jednolitej odpowiedzialności; konstrukcja ułatwiająca funkcjonalność. Kod pozostanie krótki i prosty aż do hierarchii klas. Czas na nowy projekt jest nadrabiany podczas testów, przeróbek i unikania włamań przez starą dżunglę kodu.
Zajęcia polegające na pobożnym życzeniu koncentrują się na zadaniu. Ogólnie rzecz biorąc, zapomnij o rozszerzeniu istniejącej klasy - po prostu ponownie wywołujesz kaskadę refaktorów i musisz radzić sobie z narzutem „cięższej” klasy.
Usuń wszelkie pozostałości tej nowej funkcjonalności z istniejącego kodu. Tutaj pełna i dobrze zamknięta funkcjonalność nowej funkcji jest ważniejsza niż unikanie refaktoryzacji.
źródło
Z (cudownej) książki Working Effective with Legacy Code Michaela Feathersa :
źródło
Wygląda na to, że (zwłaszcza z dyskusji w komentarzach) wprowadziłeś własne zasady, które oznaczają, że ta „drobna” zmiana to tyle samo pracy, co całkowite przepisanie oprogramowania.
Rozwiązaniem musi być „nie rób tego” . Tak dzieje się w prawdziwych projektach. Wiele starych interfejsów API ma w wyniku tego brzydkie interfejsy lub porzucone (zawsze zerowe) parametry lub funkcje o nazwie DoThisThing2 (), które działają tak samo jak DoThisThing () z całkowicie inną listą parametrów. Inne popularne sztuczki to ukrywanie informacji w globach lub oznaczanie wskaźników w celu przemycenia ich przez dużą część frameworka. (Na przykład mam projekt, w którym połowa buforów audio zawiera tylko 4-bajtową wartość magiczną, ponieważ było to o wiele łatwiejsze niż zmiana sposobu, w jaki biblioteka wywoływała swoje kodeki audio).
Trudno udzielać konkretnych porad bez określonego kodu.
źródło
Zautomatyzowane testy. Nie musisz być fanatykiem TDD, ani nie potrzebujesz 100% zasięgu, ale zautomatyzowane testy pozwalają na pewne zmiany. Ponadto wygląda na to, że masz projekt z bardzo wysokim sprzężeniem; powinieneś przeczytać o zasadach SOLID, które zostały opracowane specjalnie w celu rozwiązania tego rodzaju problemów w projektowaniu oprogramowania.
Poleciłbym te książki.
źródło
Najprawdopodobniej tak. Chociaż podobne efekty można uzyskać za pomocą ładnej i czystej bazy kodu, gdy wymagania zmienią się wystarczająco
Obawiam się, że oprócz przestania pracować nad starszym kodem. Ale możesz użyć metody, która pozwala uniknąć efektu braku działającej bazy kodu przez kilka dni, tygodni lub nawet miesięcy.
Ta metoda nosi nazwę „Metoda Mikado” i działa w następujący sposób:
zapisz cel, który chcesz osiągnąć, na kartce papieru
dokonaj najprostszej zmiany, która zaprowadzi cię w tym kierunku.
sprawdź, czy działa przy użyciu kompilatora i zestawu testów. Jeśli tak, przejdź do kroku 7. W przeciwnym razie przejdź do kroku 4.
na papierze zwróć uwagę na rzeczy, które należy zmienić, aby obecna zmiana zadziałała. Rysuj strzały, z bieżącego zadania, do nowych.
Cofnij zmiany To jest ważny krok. Jest to sprzeczne z intuicją i na początku boli fizycznie, ale skoro właśnie wypróbowałeś prostą rzecz, wcale nie jest tak źle.
wybierz jedno z zadań, które nie ma błędów wychodzących (brak znanych zależności) i wróć do 2.
zatwierdzić zmianę, przekreślić zadanie na papierze, wybrać zadanie, które nie zawiera błędów wychodzących (brak znanych zależności) i powrócić do 2.
W ten sposób będziesz mieć działającą bazę kodu w krótkich odstępach czasu. Gdzie możesz także scalić zmiany z resztą zespołu. I masz wizualną reprezentację tego, co wiesz, że nadal musisz zrobić, to pomaga zdecydować, czy chcesz kontynuować przedsięwzięcie, czy też powinieneś go zatrzymać.
źródło
Refaktoryzacja jest uporządkowaną dyscypliną, różniącą się od czyszczenia kodu według własnego uznania. Przed rozpoczęciem musisz napisać testy jednostkowe, a każdy krok powinien składać się z konkretnej transformacji, o której wiesz, że nie powinna wprowadzać żadnych zmian w funkcjonalności. Testy jednostkowe powinny przejść po każdej zmianie.
Oczywiście podczas procesu refaktoryzacji naturalnie odkryjesz zmiany, które należy zastosować, które mogą spowodować uszkodzenie. W takim przypadku postaraj się zaimplementować podkładkę kompatybilności dla starego interfejsu korzystającego z nowej struktury. Teoretycznie system powinien nadal działać jak poprzednio, a testy jednostkowe powinny przejść pomyślnie. Można oznaczyć podkładkę zgodności jako przestarzały interfejs i wyczyścić ją w odpowiednim czasie.
źródło
Jak powiedział @Jules, Refaktoryzacja i dodawanie funkcji to dwie bardzo różne rzeczy.
... ale czasami trzeba zmienić wewnętrzne działanie, aby dodać swoje rzeczy, ale wolę nazwać to modyfikacją niż refaktoryzacją.
Tam rzeczy się psują. Interfejsy są rozumiane jako granice izolujące implementację od sposobu jej użycia. Gdy tylko dotkniesz interfejsów, wszystko po obu stronach (zaimplementowanie go lub użycie) będzie musiało zostać zmienione. To może się rozprzestrzeniać tak daleko, jak się tego doświadczyło.
To, że jeden interfejs wymaga zmiany, brzmi dobrze ... że rozprzestrzenia się na inny oznacza, że zmiany rozprzestrzeniają się jeszcze bardziej. Wygląda na to, że jakaś forma danych / danych wymaga spłynięcia w dół łańcucha. Czy tak jest w przypadku?
Twoje przemówienie jest bardzo abstrakcyjne, więc trudno to rozgryźć. Przykład byłby bardzo pomocny. Zwykle interfejsy powinny być dość stabilne i niezależne od siebie, umożliwiając modyfikację części systemu bez szkody dla reszty ... dzięki interfejsom.
... w rzeczywistości najlepszym sposobem uniknięcia kaskadowych modyfikacji kodu są właśnie dobre interfejsy. ;)
źródło
Myślę, że zwykle nie możesz, chyba że chcesz zachować rzeczy takimi, jakie są. Jednak w sytuacjach takich jak Twoja myślę, że lepiej jest poinformować zespół i poinformować go, dlaczego należy przeprowadzić pewne refaktoryzacje, aby kontynuować zdrowszy rozwój. Nie chciałbym po prostu sam naprawiać. Rozmawiałbym o tym podczas spotkań Scruma (zakładając, że je macie) i systematycznie podchodziłem do niego z innymi programistami.
źródło