Co zrobić z plikiem źródłowym C ++ zawierającym 11000 wierszy?

229

Mamy więc w naszym projekcie ten ogromny plik źródłowy mainmodule.cpp (11000 linii?) I za każdym razem, gdy go dotykam, kulę się.

Ponieważ ten plik jest tak centralny i duży, gromadzi coraz więcej kodu i nie mogę wymyślić dobrego sposobu, aby faktycznie zaczął się kurczyć.

Plik jest używany i aktywnie zmieniany w kilku (> 10) wersjach konserwacyjnych naszego produktu, więc naprawdę trudno jest go refaktoryzować. Gdybym miał „po prostu” podzielić to, powiedzmy na początek, na 3 pliki, to scalenie zmian z wersji serwisowych stanie się koszmarem. A także, jeśli podzielisz plik o tak długiej i bogatej historii, śledzenie i sprawdzanie starych zmian w SCChistorii nagle staje się znacznie trudniejsze.

Plik zawiera w zasadzie „klasę główną” (główne wysyłanie i koordynację pracy wewnętrznej) naszego programu, więc za każdym razem, gdy dodawana jest funkcja, wpływa ona również na ten plik i za każdym razem, gdy rośnie. :-(

Co byś zrobił w tej sytuacji? Jakieś pomysły na przeniesienie nowych funkcji do osobnego pliku źródłowego bez zakłócania SCCprzepływu pracy?

(Uwaga na temat narzędzi: używamy C ++ z Visual Studio; Używamy AccuRevjako, SCCale myślę, że rodzaj SCCnie ma tu tak naprawdę znaczenia; Używamy Araxis Mergedo faktycznego porównywania i łączenia plików)

Martin Ba
źródło
15
@BoltClock: W rzeczywistości Vim otworzy go dość szybko.
ereOn
58
69305 linii i wciąż rośnie. Plik w naszej aplikacji, do którego mój kolega zrzuca większość swojego kodu. Nie mogłem się powstrzymać przed opublikowaniem tego tutaj. W mojej firmie nie ma nikogo, kto mógłby to zgłosić.
Agnel Kurian
204
Nie rozumiem Jak komentarz „odejść z pracy” może uzyskać tak wiele pozytywnych opinii? Wydaje się, że niektórzy mieszkają w bajkowej krainie, w której wszystkie projekty są pisane od zera i / lub używają 100% zwinności, TDD, ... (wstaw tutaj jedno ze swoich modnych słów).
Stefan
39
@Stefan: W obliczu podobnej bazy kodu zrobiłem dokładnie to. Nie miałem ochoty spędzać 95% mojego czasu na pracy w środowisku opartym na 10-letniej bazie kodu, a 5% na pisaniu kodu. W rzeczywistości niemożliwe było przetestowanie niektórych aspektów systemu (i nie mam na myśli testu jednostkowego, mam na myśli uruchomienie kodu, aby sprawdzić, czy zadziałał). Nie przetrwałem 6-miesięcznego okresu próbnego, zmęczyłem się walkami polegającymi na przegrywaniu bitew i pisaniem kodu, którego nie mogłem oprzeć.
Binary Worrier
50
w odniesieniu do aspektu śledzenia historii podziału pliku: użyj polecenia kopiowania systemu kontroli wersji, aby skopiować cały plik, jednak wiele razy chcesz go podzielić, a następnie usuń cały kod z każdej kopii, której nie chcesz w tym pliku. Zachowuje to całą historię, ponieważ każdy z podzielonych plików może prześledzić swoją historię z powrotem przez podział (który będzie wyglądał jak gigantyczne usunięcie większości zawartości pliku).
rmeador

Odpowiedzi:

86
  1. Znajdź w pliku pewien kod, który jest względnie stabilny (nie zmienia się szybko i nie różni się znacznie między gałęziami) i może stać się niezależną jednostką. Przenieś to do własnego pliku, a co za tym idzie, do jego własnej klasy we wszystkich gałęziach. Ponieważ jest stabilny, nie spowoduje to (wielu) „niezręcznych” połączeń, które muszą zostać zastosowane do innego pliku niż ten, w którym zostały pierwotnie utworzone, podczas scalania zmiany z jednej gałęzi do drugiej. Powtarzać.

  2. Znajdź w pliku jakiś kod, który zasadniczo dotyczy tylko niewielkiej liczby gałęzi i może być samodzielny. Nie ma znaczenia, czy zmienia się szybko, czy nie, z powodu małej liczby gałęzi. Przenieś to do własnych klas i plików. Powtarzać.

Więc pozbyliśmy się kodu, który jest wszędzie taki sam, i kodu specyficznego dla niektórych gałęzi.

To pozostawia jądro źle zarządzanego kodu - jest potrzebny wszędzie, ale jest inny w każdej gałęzi (i / lub zmienia się stale, tak że niektóre gałęzie działają za innymi), a jednak jest w jednym pliku, w którym jesteś bezskutecznie próbuje połączyć się między oddziałami. Przestań to robić. Plik należy rozgałęzić na stałe , być może przez zmianę nazwy w każdej gałęzi. To już nie jest „główne”, to „główne dla konfiguracji X”. OK, więc tracisz możliwość zastosowania tej samej zmiany do wielu gałęzi przez scalanie, ale w każdym razie jest to rdzeń kodu, w którym scalanie nie działa zbyt dobrze. Jeśli i tak musisz ręcznie zarządzać połączeniami, aby poradzić sobie z konfliktami, nie jest stratą, aby zastosować je niezależnie w każdym oddziale.

Myślę, że mylicie się, twierdząc, że rodzaj SCC nie ma znaczenia, ponieważ na przykład możliwości scalania gita są prawdopodobnie lepsze niż narzędzie scalania, którego używasz. Zatem główny problem, „łączenie jest trudne” występuje w różnych momentach dla różnych SCC. Jednak prawdopodobnie nie będziesz w stanie zmienić SCC, więc problem jest prawdopodobnie nieistotny.

Steve Jessop
źródło
Jeśli chodzi o łączenie: spojrzałem na GIT i SVN, spojrzałem na Perforce i pozwolę sobie powiedzieć, że nic, czego nigdzie nie widziałem, nie przebije AccuRev + Araxis w tym, co robimy. :-) (Chociaż GIT może to zrobić [ stackoverflow.com/questions/1728922/… ], a AccuRev nie może - każdy musi sam zdecydować, czy jest to część scalania czy analizy historii.)
Martin Ba
W porządku - być może masz już najlepsze dostępne narzędzie. Zdolność Gita do scalenia zmiany, która nastąpiła w pliku A w gałęzi X, w pliku B w gałęzi Y, powinna ułatwić dzielenie rozgałęzionych plików, ale prawdopodobnie używany system ma zalety, które lubisz. W każdym razie nie proponuję przejścia na git, mówiąc tylko, że SCC robi tutaj różnicę, ale mimo to zgadzam się z tobą, że można to zdyskontować :-)
Steve Jessop
129

Scalanie nie będzie tak wielkim koszmarem, jak będzie, gdy w przyszłości otrzymasz plik 30000 LOC. Więc:

  1. Przestań dodawać więcej kodu do tego pliku.
  2. Podziel To.

Jeśli nie możesz po prostu przestać kodować podczas procesu refaktoryzacji, możesz zostawić ten duży plik tak, jak jest , przynajmniej na chwilę, bez dodawania do niego więcej kodu: ponieważ zawiera on jedną „klasę główną”, możesz ją odziedziczyć i zachować odziedziczoną klasę ( es) z przeciążonymi funkcjami w kilku nowych małych i dobrze zaprojektowanych plikach.

Kirill V. Lyadvinsky
źródło
@Martin: na szczęście nie wkleiłeś tutaj swojego pliku, więc nie mam pojęcia o jego strukturze. Ale ogólną ideą jest podzielenie go na logiczne części. Takie logiczne części mogą zawierać grupy funkcji z „klasy głównej” lub można je podzielić na kilka klas pomocniczych.
Kirill V. Lyadvinsky
3
Dzięki 10 wersjom serwisowym i wielu aktywnym programistom jest mało prawdopodobne, aby plik mógł zostać zamrożony na wystarczająco długi czas.
Kobi
9
@ Martin, masz kilka wzorców GOF, które by załatwiły sprawę, jedną fasadę, która odwzorowuje funkcje mainmodule.cpp, alternatywnie (zaleciłem poniżej) stworzyć zestaw klas poleceń , z których każda odwzorowuje na funkcję / funkcja mainmodule.app. (Rozszerzyłem to na mojej odpowiedzi.)
ocodo
2
Tak, całkowicie się zgadzam, w pewnym momencie musisz przestać dodawać do niego kod, albo w końcu będzie to 30k, 40k, 50k, moduł główny kaboom właśnie został uszkodzony. :-)
Chris
67

Wydaje mi się, że napotykasz tutaj szereg zapachów kodu. Przede wszystkim wydaje się, że główna klasa narusza zasadę otwartego / zamkniętego . Wygląda również na to, że wykonuje zbyt wiele obowiązków . Z tego powodu zakładam, że kod jest bardziej kruchy niż powinien.

Chociaż rozumiem twoje obawy dotyczące identyfikowalności po refaktoryzacji, spodziewałbym się, że ta klasa jest raczej trudna w utrzymaniu i ulepszeniu oraz że wszelkie wprowadzane przez ciebie zmiany mogą powodować skutki uboczne. Zakładam, że ich koszt przewyższa koszt refaktoryzacji klasy.

W każdym razie, ponieważ zapachy kodu pogarszają się z czasem, przynajmniej w pewnym momencie ich koszt przewyższy koszt refaktoryzacji. Z twojego opisu zakładam, że przekroczyłeś punkt krytyczny.

Refaktoryzację należy wykonać małymi krokami. Jeśli to możliwe, dodaj automatyczne testy, aby zweryfikować bieżące zachowanie przed refaktoryzacją czegokolwiek. Następnie wybierz małe obszary izolowanej funkcjonalności i wyodrębnij je jako typy, aby przekazać odpowiedzialność.

W każdym razie to brzmi jak duży projekt, więc powodzenia :)

Brian Rasmussen
źródło
18
Pachnie dużo: pachnie, jakby anty-wzór Bloba jest w domu ... en.wikipedia.org/wiki/God_object . Jego ulubionym posiłkiem jest kod spaghetti: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan 1'10
@jdehaan: Starałem się o to dyplomatycznie :)
Brian Rasmussen
+1 Ode mnie też nie odważę się dotknąć nawet złożonego kodu, który napisałem bez testów na jego pokrycie.
Danny Thomas
49

Jedyne rozwiązanie, jakie kiedykolwiek wyobrażałem sobie w przypadku takich problemów, jest następujące. Rzeczywistym zyskiem opisywanej metody jest postęp ewolucji. Żadnych obrotów tutaj, w przeciwnym razie bardzo szybko będziesz miał kłopoty.

Wstaw nową klasę CPP powyżej oryginalnej klasy głównej. Na razie zasadniczo przekierowałby wszystkie wywołania do bieżącej klasy głównej, ale miał na celu uczynienie interfejsu API tej nowej klasy tak przejrzystym i zwięzłym, jak to możliwe.

Po wykonaniu tej czynności masz możliwość dodania nowych funkcjonalności w nowych klasach.

Jeśli chodzi o istniejące funkcjonalności, musisz je stopniowo przenosić w nowych klasach, gdy stają się wystarczająco stabilne. Utracisz pomoc SCC dla tego fragmentu kodu, ale niewiele można na to poradzić. Po prostu wybierz odpowiedni czas.

Wiem, że to nie jest idealne, ale mam nadzieję, że może pomóc, a proces musi być dostosowany do twoich potrzeb!

Dodatkowe informacje

Zauważ, że Git to SCC, który może śledzić fragmenty kodu z jednego pliku do drugiego. Słyszałem o tym dobre rzeczy, więc może to pomóc, gdy stopniowo przenosisz swoją pracę.

Git jest zbudowany wokół pojęcia obiektów blob, które, jeśli dobrze rozumiem, reprezentują fragmenty plików kodu. Przenieś te elementy w różnych plikach, a Git je znajdzie, nawet jeśli je zmodyfikujesz. Oprócz wideo Linusa Torvaldsa wspomnianego w komentarzach poniżej, nie byłem w stanie znaleźć czegoś jasnego na ten temat.

Benoît
źródło
Bardzo mile widziane byłoby odniesienie do tego, jak GIT to robi / jak to robisz za pomocą GIT.
Martin Ba
@Martin Git robi to automatycznie.
Matthew
4
@Martin: Git robi to automatycznie - ponieważ nie śledzi plików, śledzi zawartość. W rzeczywistości trudniej jest po prostu „pobrać historię jednego pliku”.
Arafangion
1
@Martin youtube.com/watch?v=4XpnKHJAok8 to dyskusja, w której Torvalds mówi o git. Wspomina o tym w dalszej części rozmowy.
Matthew
6
@Martin, spójrz na to pytanie: stackoverflow.com/questions/1728922/...
Benjol
30

Konfucjusz mówi: „pierwszym krokiem do wyjścia z dziury jest zaprzestanie kopania dziury”.

fdasfasdfdas
źródło
25

Niech zgadnę: dziesięciu klientów z rozbieżnymi zestawami funkcji i menedżerem sprzedaży, który promuje „personalizację”? Wcześniej pracowałem nad takimi produktami. Mieliśmy zasadniczo ten sam problem.

Zdajesz sobie sprawę, że posiadanie ogromnego pliku to kłopot, ale jeszcze większy problem to dziesięć wersji, które musisz utrzymywać na bieżąco. To wielokrotna konserwacja. SCC może to ułatwić, ale nie może to naprawić.

Zanim spróbujesz rozbić plik na części, musisz ponownie zsynchronizować dziesięć gałęzi, abyś mógł zobaczyć i ukształtować cały kod na raz. Możesz wykonać tę gałąź pojedynczo, testując obie gałęzie na tym samym głównym pliku kodu. Aby wymusić niestandardowe zachowanie, możesz użyć #ifdef i znajomych, ale lepiej, o ile to możliwe, użyć zwykłego if / else wobec zdefiniowanych stałych. W ten sposób kompilator zweryfikuje wszystkie typy i najprawdopodobniej i tak wyeliminuje „martwy” kod obiektu. (Możesz jednak wyłączyć ostrzeżenie o martwym kodzie).

Gdy istnieje tylko jedna wersja tego pliku udostępniana niejawnie przez wszystkie gałęzie, łatwiej jest rozpocząć tradycyjne metody refaktoryzacji.

#Ifdefs są przede wszystkim lepsze dla sekcji, w których kod, którego dotyczy problem, ma sens tylko w kontekście innych dostosowań dla poszczególnych gałęzi. Można argumentować, że stanowią one również okazję do tego samego schematu łączenia oddziałów, ale nie szaleją. Poproszę jeden kolosalny projekt na raz.

W krótkim okresie plik będzie się powiększał. To jest wporządku. To, co robisz, to łączenie rzeczy, które muszą być razem. Następnie zaczniesz widzieć obszary, które są wyraźnie takie same, niezależnie od wersji; można je pozostawić w spokoju lub zreformować do woli. Inne obszary będą się wyraźnie różnić w zależności od wersji. W tym przypadku masz wiele opcji. Jedną z metod jest delegowanie różnic do obiektów strategii dla poszczególnych wersji. Innym jest uzyskanie wersji klienckich ze wspólnej klasy abstrakcyjnej. Ale żadna z tych transformacji nie jest możliwa, o ile masz dziesięć „wskazówek” rozwoju w różnych gałęziach.

Ian
źródło
2
Zgadzam się, że celem powinna być jedna wersja oprogramowania, ale czy nie lepiej byłoby używać plików konfiguracyjnych (środowiska wykonawczego) i nie kompilować przechowywania danych
Esben Skov Pedersen
Lub nawet „klasy konfiguracji” dla kompilacji każdego klienta.
tc.
Uważam, że konfiguracja w czasie kompilacji lub w czasie wykonywania jest funkcjonalnie nieistotna, ale nie chcę ograniczać możliwości. Zaletą konfiguracji w czasie kompilacji jest to, że klient nie może się włamać przy użyciu pliku konfiguracyjnego w celu aktywacji dodatkowych funkcji, ponieważ umieszcza całą konfigurację w drzewie źródłowym zamiast jako kod „obiektu tekstowego do wdrożenia”. Drugą stroną jest to, że masz tendencję do AlternateHardAndSoftLayers, jeśli jest to czas działania.
Ian
22

Nie wiem, czy to rozwiąże twój problem, ale myślę, że chcesz zrobić migrację zawartości pliku do mniejszych plików niezależnych od siebie (podsumowane). Dostaję też, że masz około 10 różnych wersji oprogramowania i musisz je wszystkie wspierać bez bałaganu.

Przede wszystkim nie ma sposobu, aby było to łatwe i rozwiąże się w ciągu kilku minut burzy mózgów. Funkcje połączone w pliku są niezbędne dla twojej aplikacji, a samo ich odcięcie i migracja do innych plików nie uratuje twojego problemu.

Myślę, że masz tylko te opcje:

  1. Nie migruj i pozostań przy tym, co masz. Ewentualnie rzuć pracę i zacznij pracę nad poważnym oprogramowaniem z dobrym wzornictwem. Ekstremalne programowanie nie zawsze jest najlepszym rozwiązaniem, jeśli pracujesz nad długim projektem z wystarczającymi środkami, aby przetrwać awarię lub dwie.

  2. Opracuj układ, w którym chciałbyś, aby Twój plik wyglądał po podzieleniu. Utwórz niezbędne pliki i zintegruj je z aplikacją. Zmień nazwę funkcji lub przeładuj je, aby wziąć dodatkowy parametr (może po prostu zwykłą wartość logiczną?). Gdy będziesz musiał popracować nad kodem, przenieś funkcje, nad którymi musisz pracować, do nowego pliku i zamapuj wywołania funkcji starych funkcji na nowe funkcje. Powinieneś nadal mieć swój główny plik w ten sposób i nadal być w stanie zobaczyć zmiany, które zostały w nim wprowadzone, gdy dojdzie do konkretnej funkcji, wiesz dokładnie, kiedy został zlecony na zewnątrz i tak dalej.

  3. Spróbuj przekonać współpracowników dobrym ciastem, że przepływ pracy jest przereklamowany i że musisz przerobić niektóre części aplikacji, aby robić poważne interesy.

Rudzik
źródło
19

Dokładnie ten problem został rozwiązany w jednym z rozdziałów książki „Skutecznie współpracując ze starszym kodem” ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).

Patrick
źródło
informit.com/store/product.aspx?isbn=0131177052 umożliwia sprawdzenie spisu treści tej książki (i 2 przykładowych rozdziałów). Jak długi jest rozdział 20? (Tylko po to, aby przekonać się, jak użyteczne może być.)
Martin Ba
17
rozdział 20 ma długość 10 000 linii, ale autor pracuje nad tym, jak podzielić go na strawne części ... 8)
Tony Delroy
1
Ma około 23 stron, ale zawiera 14 zdjęć. Myślę, że powinieneś to zdobyć, poczujesz się bardziej pewny siebie, próbując zdecydować, co robić imho.
Emile Vrijdags
Doskonała książka na temat problemu, ale zalecenia, które czyni (i inne zalecenia w tym wątku) mają wspólny wymóg: jeśli chcesz przefaktoryzować ten plik dla wszystkich swoich oddziałów, to jedynym sposobem, aby to zrobić, jest zamrożenie plik dla wszystkich gałęzi i wprowadź początkowe zmiany strukturalne. Nie można tego obejść. Książka przedstawia iteracyjne podejście do bezpiecznego wyodrębniania podklas bez bezpiecznego refaktoryzacji, poprzez tworzenie duplikatów metod i delegowanie wywołań, ale wszystko to jest dyskusyjne, jeśli nie można modyfikować plików.
Dan Bryant
2
@Martin, książka jest doskonała, ale opiera się dość mocno na teście, refaktorze, cyklu testowym, który może być dość trudny z miejsca, w którym się teraz znajdujesz. Byłem w podobnej sytuacji i ta książka była najbardziej pomocna, jaką znalazłem. Ma dobre sugestie dotyczące twojego brzydkiego problemu. Ale jeśli nie możesz umieścić na zdjęciu jakiejś uprzęży testowej, wszystkie sugestie dotyczące refaktoryzacji na świecie ci nie pomogą.
14

Myślę, że najlepiej byłoby stworzyć zestaw klas poleceń , które będą mapowane na punkty API mainmodule.cpp.

Po ich wprowadzeniu będziesz musiał przebudować istniejącą bazę kodu, aby uzyskać dostęp do tych punktów API za pośrednictwem klas poleceń. Gdy to zrobisz, możesz dowolnie refaktoryzować implementację każdego polecenia do nowej struktury klas.

Oczywiście, z jedną klasą 11 KLOC, kod tam jest prawdopodobnie bardzo sprzężony i kruchy, ale tworzenie indywidualnych klas poleceń pomoże znacznie bardziej niż jakiejkolwiek innej strategii proxy / elewacji.

Nie zazdroszczę temu zadaniu, ale z biegiem czasu ten problem będzie się nasilał, jeśli nie zostanie rozwiązany.

Aktualizacja

Sugeruję, że wzór Dowodzenia jest lepszy niż Fasada.

Preferowane jest utrzymywanie / organizowanie wielu różnych klas dowodzenia na (względnie) monolitycznej fasadzie. Mapowanie pojedynczej elewacji na plik 11 KLOC prawdopodobnie będzie musiało zostać podzielone na kilka różnych grup.

Po co zawracać sobie głowę próbą znalezienia tych grup fasad? Dzięki wzorowi poleceń będziesz mógł grupować i organizować te małe klasy w sposób organiczny, dzięki czemu masz dużo większą elastyczność.

Oczywiście obie opcje są lepsze niż pojedynczy 11 KLOC i rosnący plik.

Slomojo
źródło
Daj +1 alternatywę dla rozwiązania, które zaproponowałem, z tym samym pomysłem: zmień interfejs API, aby podzielić duży problem na małe.
Benoît
13

Jedna ważna rada: nie mieszaj refaktoryzacji z poprawkami błędów. To, czego chcesz, to wersja Twojego programu, która jest identyczna z poprzednią wersją, z tym wyjątkiem, że kod źródłowy jest inny.

Jednym ze sposobów może być podzielenie najmniejszej funkcji / części na własny plik, a następnie dołączenie jej wraz z nagłówkiem (przekształcając main.cpp w listę #include, która sama w sobie brzmi jak kod * Nie jestem Guru C ++), ale przynajmniej teraz jest podzielony na pliki).

Następnie możesz spróbować przełączyć wszystkie wersje serwisowe na „nowy” plik main.cpp lub jakąkolwiek inną strukturę. Znowu: Żadnych innych zmian ani poprawek błędów, ponieważ śledzenie ich jest mylące jak diabli.

Inna sprawa: ile tylko możesz chcieć zrobić jedno wielkie przejście do refaktoryzacji całej rzeczy za jednym razem, możesz odgryźć więcej niż możesz przeżuć. Może po prostu wybierz jedną lub dwie „części”, weź je do wszystkich wydań, a następnie dodaj trochę wartości dla swojego klienta (w końcu Refaktoryzacja nie dodaje bezpośredniej wartości, więc jest to koszt, który musi być uzasadniony), a następnie wybierz inną jedna lub dwie części.

Oczywiście wymaga to pewnej dyscypliny w zespole, aby faktycznie używać podzielonych plików, a nie tylko dodawać nowe rzeczy do main.cpp przez cały czas, ale znowu, próba zrobienia jednego ogromnego refaktora może nie być najlepszym rozwiązaniem.

Michael Stum
źródło
1
+1 za faktoring i #include z powrotem. Gdybyś to zrobił dla wszystkich 10 oddziałów (trochę pracy, ale możliwe do zarządzania) nadal miałbyś inny problem, polegający na publikowaniu zmian we wszystkich swoich oddziałach, ale ten problem nie t zostały rozszerzone (koniecznie). Czy to jest brzydkie? Tak, nadal tak jest, ale może przynieść trochę racjonalności temu problemowi. Po kilku latach konserwacji i serwisowania naprawdę dużego produktu wiem, że konserwacja wiąże się z dużym bólem. Przynajmniej uczcie się z niego i służcie jako przestroga dla innych.
Jay
10

Rofl, to przypomina mi moją starą pracę. Wygląda na to, że zanim dołączyłem, wszystko było w jednym wielkim pliku (także C ++). Następnie podzielili go (w całkowicie losowych punktach za pomocą dołączeń) na około trzy (wciąż ogromne pliki). Jakość tego oprogramowania była, jak można się spodziewać, okropna. Projekt wyniósł około 40 tys. LOC. (nie zawiera prawie żadnych komentarzy, ale DUŻO duplikatu kodu)

W końcu zrobiłem kompletną przeróbkę projektu. Zacząłem od powtórzenia najgorszej części projektu od zera. Oczywiście miałem na myśli możliwy (mały) interfejs między tą nową częścią a resztą. Następnie wstawiłem tę część do starego projektu. Nie zmieniłem starego kodu, aby utworzyć niezbędny interfejs, ale po prostu go zastąpiłem. Potem zrobiłem małe kroki, przepisując stary kod.

Muszę powiedzieć, że zajęło to około pół roku i w tym czasie nie było rozwijania starej bazy kodu oprócz poprawek błędów.


edytować:

Rozmiar pozostawał na poziomie około 40 000 LOC, ale nowa aplikacja zawierała o wiele więcej funkcji i prawdopodobnie mniej błędów w swojej początkowej wersji niż 8-letnie oprogramowanie. Jednym z powodów przepisywania było również to, że potrzebowaliśmy nowych funkcji i wprowadzenie ich do starego kodu było prawie niemożliwe.

Oprogramowanie było dla systemu wbudowanego, drukarki etykiet.

Kolejną kwestią, którą powinienem dodać, jest teoretycznie projekt C ++. Ale to wcale nie był OO, mógł to być C. Nowa wersja była zorientowana obiektowo.

ziggystar
źródło
9
Za każdym razem, gdy słyszę „od zera” w temacie dotyczącym refaktoryzacji, zabijam kociaka!
Kugel
Byłem w bardzo podobnej sytuacji, choć główna pętla programu, z którą musiałem się zmierzyć, to tylko ~ 9000 LOC. I to było wystarczająco złe.
AndyUK,
8

OK, więc przeważnie przepisywanie API kodu produkcyjnego to zły pomysł na początek. Dwie rzeczy muszą się wydarzyć.

Po pierwsze, musisz faktycznie poprosić swój zespół o zawieszenie kodu w bieżącej produkcyjnej wersji tego pliku.

Po drugie, musisz wziąć tę wersję produkcyjną i utworzyć gałąź, która zarządza kompilacjami za pomocą dyrektyw przetwarzania wstępnego, aby podzielić duży plik. Podział kompilacji za pomocą dyrektyw preprocesora JUST (#ifdefs, #include, #endifs) jest łatwiejszy niż przekodowanie API. Jest to zdecydowanie łatwiejsze dla umów SLA i ciągłego wsparcia.

Tutaj możesz po prostu wyciąć funkcje odnoszące się do konkretnego podsystemu w klasie i umieścić je w pliku powiedzmy mainloop_foostuff.cpp i dołączyć go do mainloop.cpp w odpowiednim miejscu.

LUB

Bardziej czasochłonnym, ale solidnym sposobem byłoby opracowanie wewnętrznej struktury zależności z podwójną pośrednią reakcją na uwzględnienie rzeczy. Umożliwi to podzielenie rzeczy i zadbanie o współzależności. Należy zauważyć, że takie podejście wymaga kodowania pozycyjnego i dlatego powinno być połączone z odpowiednimi komentarzami.

Takie podejście obejmowałoby komponenty, które są używane na podstawie kompilowanego wariantu.

Podstawowa struktura polega na tym, że plik mainclass.cpp będzie zawierał nowy plik o nazwie MainClassComponents.cpp po bloku instrukcji, takich jak:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

Podstawowa struktura pliku MainClassComponents.cpp byłaby tam, aby opracować zależności w ramach podskładników w następujący sposób:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

A teraz dla każdego komponentu tworzysz plik component_xx.cpp.

Oczywiście używam liczb, ale powinieneś użyć czegoś bardziej logicznego na podstawie twojego kodu.

Korzystanie z preprocesora pozwala dzielić rzeczy bez martwienia się o zmiany API, co jest koszmarem w produkcji.

Po ustabilizowaniu produkcji możesz rozpocząć pracę nad przeprojektowaniem.

Król Elfów
źródło
To wygląda jak początkowo bolesne rezultaty doświadczenia.
JBRWilkinson
W rzeczywistości jest to technika zastosowana w kompilatorach Borland C ++ do emulacji stylów Pascal do zarządzania plikami nagłówkowymi. Zwłaszcza, gdy zrobili pierwszy port swojego systemu okienkowego opartego na tekście.
Elf King,
8

Rozumiem twój ból :) Byłem też w kilku takich projektach i to nie jest ładne. Nie ma na to łatwej odpowiedzi.

Jednym z podejść, które może Ci się przydać, jest rozpoczęcie dodawania bezpiecznych zabezpieczeń we wszystkich funkcjach, to znaczy sprawdzanie argumentów, warunków wstępnych / następczych w metodach, a następnie dodawanie testów jednostkowych wszystkich w celu uchwycenia bieżącej funkcjonalności źródeł. Gdy już to zrobisz, będziesz lepiej przygotowany do ponownego uwzględnienia kodu, ponieważ pojawią się komunikaty i błędy wyskakujące z ostrzeżeniem, jeśli coś zapomniałeś.

Czasami jednak czasami refaktoryzacja może przynieść więcej bólu niż korzyści. Wtedy może być lepiej pozostawić oryginalny projekt w stanie pseudoobsługowym i zacząć od zera, a następnie stopniowo dodawać funkcjonalność od bestii.

claptrap
źródło
4

Nie powinieneś zajmować się zmniejszaniem rozmiaru pliku, ale raczej zmniejszaniem rozmiaru klasy. Sprowadza się to do prawie tego samego, ale sprawia, że ​​patrzysz na problem z innej perspektywy (jak sugeruje @Brian Rasmussen , twoja klasa wydaje się mieć wiele obowiązków).

Björn Pollex
źródło
Jak zawsze chciałbym uzyskać wyjaśnienie opinii.
Björn Pollex
4

To, co masz, to klasyczny przykład znanego antipatternu projektowego o nazwie kropelka . Poświęć trochę czasu na przeczytanie artykułu, który tu wskazuję, a może znajdziesz coś przydatnego. Poza tym, jeśli ten projekt jest tak duży, jak wygląda, powinieneś rozważyć projekt, aby zapobiec rozrastaniu się kodu, którego nie można kontrolować.

David Conde
źródło
4

To nie jest odpowiedź na duży problem, ale teoretyczne rozwiązanie konkretnego jego fragmentu:

  • Dowiedz się, gdzie chcesz podzielić duży plik na podfile. Umieść komentarze w jakimś specjalnym formacie w każdym z tych punktów.

  • Napisz dość trywialny skrypt, który w tych punktach podzieli plik na podfile. (Być może specjalne komentarze mają osadzone nazwy plików, które skrypt może wykorzystać jako instrukcje dotyczące podziału). Powinien zachować komentarze w ramach podziału.

  • Uruchom skrypt. Usuń oryginalny plik.

  • Kiedy potrzebujesz scalić z gałęzi, najpierw odtwórz duży plik, łącząc elementy z powrotem, wykonaj scalenie, a następnie ponownie podziel.

Ponadto, jeśli chcesz zachować historię plików SCC, spodziewam się, że najlepszym sposobem, aby to zrobić, jest poinformowanie systemu kontroli źródła, że ​​poszczególne pliki części są kopiami oryginału. Następnie zachowa historię sekcji, które były przechowywane w tym pliku, chociaż oczywiście zapisze również, że duże części zostały „usunięte”.

Brooks Moses
źródło
4

Jednym ze sposobów na podzielenie go bez zbytniego niebezpieczeństwa byłoby historyczne spojrzenie na wszystkie zmiany linii. Czy niektóre funkcje są bardziej stabilne niż inne? Gorące punkty zmian, jeśli chcesz.

Jeśli linia nie została zmieniona przez kilka lat, możesz prawdopodobnie przenieść ją do innego pliku bez większego zmartwienia. Rzuciłbym okiem na źródło opatrzone adnotacją ostatniej wersji, która dotknęła daną linię, i sprawdziłam, czy są jakieś funkcje, które można wyciągnąć.

Paul Rubel
źródło
Myślę, że inni zaproponowali podobne rzeczy. Jest to krótkie i na temat i myślę, że może to być prawidłowy punkt wyjścia dla pierwotnego problemu.
Martin Ba
3

Wow, brzmi świetnie. Myślę, że warto wyjaśnić swojemu szefowi, że potrzeba dużo czasu na refaktoryzację bestii. Jeśli się nie zgadza, rezygnacja jest opcją.

W każdym razie to, co sugeruję, to w zasadzie wyrzucenie całej implementacji i zgrupowanie jej w nowe moduły, nazwijmy te „usługi globalne”. „Moduł główny” przekaże tylko te usługi i ŻADNY nowy kod, który napiszesz, użyje ich zamiast „modułu głównego”. Powinno to być wykonalne w rozsądnym czasie (ponieważ jest to głównie kopiowanie i wklejanie), nie psujesz istniejącego kodu i możesz to zrobić po jednej wersji serwisowej na raz. A jeśli nadal masz czas, możesz poświęcić go na refaktoryzację wszystkich starych, zależnych modułów, aby korzystać z usług globalnych.

back2dos
źródło
3

Moje współczucia - w mojej poprzedniej pracy spotkałem się z podobną sytuacją z plikiem, który był kilka razy większy niż ten, z którym masz do czynienia. Rozwiązaniem było:

  1. Napisz kod, aby dokładnie przetestować funkcję w danym programie. Wygląda na to, że nie będziesz mieć tego pod ręką ...
  2. Zidentyfikuj kod, który można wyodrębnić do klasy helper / utilities. Nie musisz być duży, po prostu coś, co nie jest tak naprawdę częścią twojej „głównej” klasy.
  3. Przekoduj kod wskazany w 2. na osobną klasę.
  4. Uruchom ponownie testy, aby upewnić się, że nic się nie zepsuło.
  5. Kiedy masz czas, przejdź do 2. i powtórz w razie potrzeby, aby kod był zarządzalny.

Klasy zbudowane w kroku 3. iteracje prawdopodobnie wzrosną, aby pochłonąć więcej kodu, który jest odpowiedni dla ich nowo wyczyszczonej funkcji.

Mógłbym również dodać:

0: kup książkę Michaela Feathersa na temat pracy ze starszym kodem

Niestety ten rodzaj pracy jest zbyt powszechny, ale z mojego doświadczenia wynika, że ​​wielką zaletą jest sprawienie, aby działający, ale okropny kod był coraz mniej okropny przy jednoczesnym utrzymaniu jego działania.

Steve Townsend
źródło
2

Zastanów się, w jaki sposób przepisać całą aplikację w bardziej rozsądny sposób. Może przepisać małą jego część jako prototyp, aby sprawdzić, czy Twój pomysł jest wykonalny.

Jeśli udało Ci się znaleźć realne rozwiązanie, odpowiednio zmodyfikuj aplikację.

Jeśli wszystkie próby stworzenia bardziej racjonalnej architektury zawiodą, to przynajmniej wiesz, że rozwiązaniem jest prawdopodobnie przedefiniowanie funkcjonalności programu.

wallyk
źródło
+1 - przepisz go w swoim czasie, ale w przeciwnym razie ktoś może pluć jego manekinie.
Jon Black
2

Moje 0,05 eurocenta:

Przeprojektuj cały bałagan, podziel go na podsystemy, biorąc pod uwagę wymagania techniczne i biznesowe (= wiele równoległych ścieżek konserwacji z potencjalnie różną bazą kodu dla każdego, oczywiście istnieje potrzeba dużej modyfikacji itp.).

Przy podziale na podsystemy przeanalizuj miejsca, które uległy największej zmianie, i oddziel je od niezmiennych części. Powinno to pokazać problemy. Oddziel najbardziej zmieniające się części do ich własnych modułów (np. Dll) w taki sposób, aby interfejs API modułu mógł być nienaruszony i nie trzeba cały czas łamać BC. W ten sposób możesz wdrożyć różne wersje modułu dla różnych gałęzi konserwacji, jeśli to konieczne, bez zmiany rdzenia.

Przeprojektowanie prawdopodobnie będzie musiało być osobnym projektem, próba zrobienia tego z ruchomym celem nie będzie działać.

Co do historii kodu źródłowego, moim zdaniem: zapomnij o nowym kodzie. Ale przechowuj gdzieś historię, aby w razie potrzeby ją sprawdzić. Założę się, że na początku nie będziesz go potrzebować.

Najprawdopodobniej musisz uzyskać wpisowe do zarządzania dla tego projektu. Być może możesz kłócić się z szybszym czasem programowania, mniej błędów, łatwiejszym utrzymaniem i mniejszym chaosem. Coś w stylu „Proaktywnie zapewniamy odporność i żywotność naszych kluczowych zasobów oprogramowania” :)

W ten sposób zacznę przynajmniej rozwiązywać ten problem.

Slinky
źródło
2

Zacznij od dodania do niego komentarzy. W odniesieniu do tego, gdzie wywoływane są funkcje i czy można przenosić różne rzeczy. To może wprawić sprawy w ruch. Naprawdę musisz ocenić, jak kruchy jest jego kod. Następnie przenieś wspólne elementy funkcjonalności. Małe zmiany na raz.

Jesper Smith
źródło
2

Coś, co uważam za przydatne do zrobienia (i robię to teraz, chociaż nie na skalę, z którą się zmierzysz), to wyodrębnienie metod jako klas (refaktoryzacja obiektów metod). Metody różniące się w zależności od wersji staną się różnymi klasami, które można wstrzyknąć do wspólnej bazy w celu zapewnienia różnych potrzebnych zachowań.

Channing Walton
źródło
2

Uznałem to zdanie za najciekawszą część twojego postu:

> Plik jest używany i aktywnie zmieniany w kilku (> 10) wersjach konserwacyjnych naszego produktu, więc naprawdę trudno jest go refaktoryzować

Po pierwsze, zalecałbym użycie systemu kontroli źródła do opracowania tych wersji serwisowych 10+, które obsługują rozgałęzianie.

Po drugie, stworzyłbym dziesięć oddziałów (po jednym dla każdej wersji konserwacji).

Czuję, że już się kulisz! Ale albo kontrola źródła nie działa w twojej sytuacji z powodu braku funkcji lub nie jest używana poprawnie.

Teraz przejdź do gałęzi, nad którą pracujesz - przerób ją według własnego uznania, mając pewność, że nie zakłócisz pozostałych dziewięciu gałęzi produktu.

Byłbym trochę zaniepokojony, że masz tak wiele w swojej funkcji main ().

We wszystkich projektach, które piszę, używałbym main () tylko do inicjowania podstawowych obiektów - takich jak obiekt symulacji lub aplikacji - te klasy powinny być kontynuowane.

Zainicjowałbym również obiekt rejestrowania aplikacji, który będzie używany globalnie w całym programie.

Wreszcie, w głównej części dodaję także kod wykrywania wycieków w blokach preprocesora, który zapewnia, że ​​jest włączony tylko w kompilacjach DEBUG. To wszystko, co dodałbym do main (). Main () powinien być krótki!

Mówisz tak

> Plik zawiera w zasadzie „główną klasę” (główne wysyłanie i koordynowanie pracy wewnętrznej) naszego programu

Wygląda na to, że te dwa zadania można podzielić na dwa osobne obiekty - koordynatora i dyspozytora pracy.

Po ich podzieleniu możesz zepsuć „przepływ pracy SCC”, ale wygląda na to, że ścisłe przestrzeganie przepływu pracy SCC powoduje problemy z konserwacją oprogramowania. Porzuć to teraz i nie oglądaj się za siebie, bo jak tylko to naprawisz, zaczniesz spać spokojnie.

Jeśli nie jesteś w stanie podjąć decyzji, walcz o nią z menedżerem - z tego powodu aplikacja musi zostać ponownie rozpatrzona - i to źle! Nie przejmuj się odpowiedzią!

user206705
źródło
Jak rozumiem, problem jest następujący: jeśli ugryziesz pocisk i refaktor, nie będziesz już mógł przenosić łat między wersjami. SCC może być idealnie skonfigurowane.
peterchen
@peterchen - dokładnie problem. SCC scalają się na poziomie plików. (Scalanie 3-kierunkowe) Jeśli przenosisz kod między plikami, będziesz musiał ręcznie ręcznie modyfikować zmodyfikowane bloki kodu z jednego pliku do drugiego. (Funkcja GIT, o której wspominał ktoś inny w innym komentarzu, jest po prostu dobra dla historii, a nie dla scalenia, o ile mogę powiedzieć)
Martin Ba
2

Jak już to opisałeś, głównym problemem jest różnica między podziałem przed i po podziale, łączenie poprawek błędów itp. Narzędzie wokół niego. Nie zajmie to dużo czasu, aby na stałe zakodować skrypt w Perlu, Ruby itp., Aby wydrzeć większość szumu wynikającego z różnic przed podziałem przed połączeniem po podziale. Rób wszystko, co jest najłatwiejsze pod względem obsługi hałasu:

  • usuń niektóre linie przed / podczas konkatenacji (np. uwzględnij strażników)
  • w razie potrzeby usuń inne rzeczy z wyjścia różnicowego

Możesz nawet sprawić, że za każdym razem, gdy nastąpi zameldowanie, rozpocznie się konkatenacja i masz coś przygotowanego do odróżnienia się od wersji z jednym plikiem.

Tony D.
źródło
2
  1. Nigdy więcej nie dotykaj tego pliku i kodu!
  2. Treat jest jak coś, z czym utknąłeś. Zacznij pisać adaptery dla zakodowanej tam funkcji.
  3. Napisz nowy kod w różnych jednostkach i rozmawiaj tylko z adapterami, które zawierają funkcjonalność potwora.
  4. ... jeśli tylko jedno z powyższych nie jest możliwe, rzuć pracę i zdobądź nową.
paul_71
źródło
2
+/- 0 - poważnie, gdzie mieszkają ludzie, którzy zalecilibyście rezygnację z pracy w oparciu o takie szczegóły techniczne?
Martin Ba
1

„Plik zawiera w zasadzie„ główną klasę ”(główne wysyłanie i koordynowanie pracy wewnętrznej) naszego programu, więc za każdym razem, gdy dodawana jest funkcja, wpływa ona również na ten plik i za każdym razem, gdy rośnie.”

Jeśli ten duży PRZEŁĄCZNIK (który moim zdaniem istnieje) stanie się głównym problemem związanym z konserwacją, możesz go przeredagować, aby używał słownika i wzorca poleceń, i usunąć całą logikę przełączania z istniejącego kodu do modułu ładującego, który wypełnia tę mapę, tj .:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();
Grozz
źródło
2
Nie, tak naprawdę nie ma dużego przełącznika. To zdanie jest najbliższe opisaniu tego bałaganu :)
Martin Ba
1

Myślę, że najłatwiejszym sposobem śledzenia historii źródła podczas dzielenia pliku byłoby coś takiego:

  1. Wykonuj kopie oryginalnego kodu źródłowego, korzystając z wszelkich zachowanych w historii poleceń kopiowania dostępnych w systemie SCM. Prawdopodobnie będziesz musiał przesłać w tym momencie, ale nie musisz jeszcze informować systemu kompilacji o nowych plikach, więc powinno być w porządku.
  2. Usuń kod z tych kopii. To nie powinno przełamać historii linii, które trzymasz.
Christopher Creutzig
źródło
„przy użyciu dowolnych zachowanych historii poleceń kopiowania, które zapewnia system SCM” ... zła rzecz, której nie zapewnia
Martin Ba
Szkoda Już samo to wydaje się dobrym powodem do przejścia na coś bardziej nowoczesnego. :-)
Christopher Creutzig
1

Myślę, że to, co zrobiłbym w tej sytuacji, to ugryzienie i:

  1. Dowiedz się, jak chciałem podzielić plik (na podstawie bieżącej wersji programistycznej)
  2. Umieść blokadę administracyjną pliku („Nikt nie dotyka mainmodule.cpp po 17:00 w piątek !!!”
  3. Spędź długi weekend, stosując tę ​​zmianę w> 10 wersjach konserwacyjnych (od najstarszej do najnowszej), aż do bieżącej wersji włącznie.
  4. Usuń mainmodule.cpp ze wszystkich obsługiwanych wersji oprogramowania. To nowy wiek - nie ma już mainmodule.cpp.
  5. Przekonaj kierownictwo, że nie powinieneś obsługiwać więcej niż jednej wersji oprogramowania (przynajmniej bez dużej umowy na obsługę $$$). Jeśli każdy z twoich klientów ma swoją unikalną wersję ... yeeeeeshhhh. Dodałbym dyrektywy kompilatora, zamiast starać się utrzymać 10+ rozwidleń.

Śledzenie starych zmian w pliku jest po prostu rozwiązywane przez pierwszy komentarz do odprawy, mówiąc coś w stylu „split from mainmodule.cpp”. Jeśli musisz wrócić do czegoś nowego, większość ludzi pamięta zmianę, jeśli minie 2 lata, komentarz powie im, gdzie szukać. Oczywiście, jak cenne będzie cofnięcie się o ponad 2 lata i sprawdzenie, kto zmienił kod i dlaczego?

BIBD
źródło