Techniki Refaktoryzacji Kodu Które Naprawdę Działają
Summary
Refaktoryzacja kodu to proces czyszczenia kodu bez dodawania nowych funkcji. Ten poradnik obejmuje techniki, które naprawdę działają w praktyce: extract method, composing methods, branch-by-abstraction i upraszczanie logiki warunkowej. Dowiesz się też, kiedy refaktoryzacja jest rzeczywiście potrzebna, a kiedy lepiej zostawić kod w spokoju.
Większość deweloperów wie, że refaktoryzacja kodu ma znaczenie. Mniej jasne jest, które techniki refaktoryzacji wybrać jako pierwsze, kiedy przestać, oraz jakie sygnały mówią o tym, że kod potrzebuje uporządkowania, a nie pełnego przepisania. Oto, czego nauczyliśmy się przez sześć miesięcy pracy z systemem rekomendacji, i co zrobiliśmy inaczej.

Sygnały mówiące, że refaktoryzacja jest potrzebna
Nie każda brzydka funkcja warta jest zmian. Trzy sygnały, które warto bać się poważnie:
Duplikacja w wielu miejscach. Jeśli piszesz tę samą transformację danych w trzech oddzielnych controllerach, to nie jest problem stylu. To bomba czasu. Jedna zmiana upstream oznacza trzy miejsca do aktualizacji, a jedno zapomnisz.
Jedna zmiana psuje niezwiązane funkcje. Jeśli naprawianie buga w flow płatności wpływa na to, jak renderują się profile użytkowników, sprzężenie jest zbyt mocne. System traktuje oddzielne zagadnienia jako jedno.
Nowy inżynier nie orientuje się przez pół godziny. To niedoceniany sygnał. Jeśli nie możesz wyjaśnić, co robi plik, bez przeczytania całego kodu, prawdopodobnie robi zbyt wiele rzeczy. W Appier mierzyliśmy czas onboardingu. Pliki, które zajmowały więcej niż 15 minut wyjaśniania, trafiały na backlog refaktoryzacji.
Asymetria zmian w kodzie. Uruchom git log --stat i spójrz, które pliki są dotykane najczęściej. Jeśli te same 3 pliki stanowią 60% twoich commitów, ta koncentracja jest sygnałem. Wysokie zmiany plus wysoka złożoność to 80/20 hotspot wart ataku.
Omijaj refaktoryzację, jeśli: kod działa, nikt go nie dotyka, i nie masz powodu, aby go rozszerzać. Reguła Skautów (zostaw kod czystszy, niż go zastałeś) to dobra zasada, ale nie sprzątaj terenu biwaku, którego nie używasz.
Extract Method: technika, którą używasz 80% czasu
Extract Method to wyciąganie znaczącej porcji logiki z długiej funkcji i nadanie jej nazwy. Ta nazwa to dokumentacja.
Przed:
def process_order(order):
# validate
if not order.get('user_id'):
raise ValueError('Missing user_id')
if order.get('amount', 0) <= 0:
raise ValueError('Invalid amount')
# apply discount
if order.get('coupon') == 'LAUNCH20':
order['amount'] = order['amount'] * 0.8
# save
db.save(order)Po:
def process_order(order):
validate_order(order)
apply_discount(order)
db.save(order)
def validate_order(order):
if not order.get('user_id'):
raise ValueError('Missing user_id')
if order.get('amount', 0) <= 0:
raise ValueError('Invalid amount')
def apply_discount(order):
if order.get('coupon') == 'LAUNCH20':
order['amount'] = order['amount'] * 0.8Wersja "po" jest dłuższa. To w porządku. Każda funkcja ma teraz jeden powód do istnienia, i gdy logika rabatu się zmieni (a zmieni się), wiesz dokładnie gdzie szukać.
Warto inwestować, jeśli: oryginalna funkcja ma więcej niż około 20-25 linii, lub dodajesz komentarz typu # validate aby wyjaśnić, co robi blok kodu. Ten komentarz to nazwa funkcji czekająca na ekstrakcję. Gdy piszesz # apply discount nad blokiem kodu, już nazwałeś metodę; po prostu jej jeszcze nie wyodrębniłeś.
Praktyczna reguła: jeśli masz ochotę dodać wbudowany komentarz wyjaśniający, co robi kolejnych pięć linii, to kandydat do ekstrakcji.
Composing Methods: gdy Extract Method to za mało
Composing idzie dalej. Zamiast wyciągania jednej metody, rozbijasz całą klasę lub moduł na mniejsze, ukierunkowane komponenty. To, czego potrzebujesz, gdy plik urósł do 600 linii i służy pięciu niezwiązanym celom.
Wzorzec: identyfikuj różne odpowiedzialności, daj każdej własną klasę lub moduł, złącz je na wyższym poziomie. Martin Fowler opisuje to jako przekształcanie jednej nabrojonej klasy w skupisko współpracujących elementów.
W praktyce trudna część to nie mechanika. To ustalenie, gdzie kończy się jedna odpowiedzialność, a zaczyna druga. Przydatna heurystyka: jeśli możesz opisać, co robi funkcja lub klasa, bez słowa "i", prawdopodobnie ma jedną odpowiedzialność. Jeśli potrzebujesz "i", rozdziel.
Inny sposób znalezienia szewów: spójrz na importy na górze pliku. Jeśli jeden moduł importuje z sześciu niezwiązanych domen (warstwa bazy danych, usługa email, logging, klient płatności, reporting), robi zbyt wiele. Każde skupisko importów to kandydat na własny moduł.
Composing methods również pomaga z testowalności. Klasa 400-liniowa jest trudna do testowania jednostkowego. Cztery klasy 100-liniowe z jasnym interfejsem są znacznie łatwiejsze do mockowania i izolacji.

Branch-by-Abstraction: refaktoryzacja z systemem wciąż działającym
To bardziej architektoniczne, ale technika, która sprawia, że na dużą skalę refaktoryzacja jest do przeżycia w środowisku produkcyjnym.
Pomysł: zamiast wielkiego przełącznika z jednej implementacji na drugą, wprowadzasz warstwę abstrakcji (zwykle interfejs) wokół kodu, który chcesz zmienić. Stara i nowa implementacja zarówno spełniają interfejs. Możesz wysłać nową implementację za flagą funkcji, uruchamiać obie równolegle, i wycofać starą, gdy nowa jest stabilna.
Czemu to ważne dla side projectów: jeśli przebudujesz integrację płatności lub migrujesz ORM, branch-by-abstraction oznacza, że możesz utrzymać bieżącą wersję działającą w produkcji, gdy nowa się przygotowuje. Bez "gałęzi refaktoryzacji", która odbiega z main przez trzy tygodnie.
Koszt: więcej upfront pracy nad zdefiniowaniem czystego interfejsu. Warte, gdy rzecz, którą zastępujesz, jest ryzykowna (auth, billing, dostęp do danych) i nie możesz sobie pozwolić na pełne okno wyłączenia.
Konkretny przykład: mieliśmy starszy moduł śledzenia zdarzeń, który pisał bezpośrednio do tabeli PostgreSQL. Przejście do pipeline'u opartego na Kafce oznaczało dotknięcie 40+ miejsc wywołania. Z branch-by-abstraction wprowadziliśmy interfejs EventTracker, zamienialiśmy implementację za flagą, uruchamialiśmy oba cele równolegle przez dwa tygodnie, potem usuwaliśmy starego. Zero downtime, zero awaryjnych deployów.
Upraszczanie Logiki Warunkowej: niedoceniana oczyszczarka
Logika warunkowa to miejsce, gdzie ukrywa się złożoność. Funkcja z pięcioma zagnieżdżonymi if jest trudniej testowalna, trudniejsza do czytania i trudniejsza do rozszerzania niż pięć oddzielnych warunków wyrażonych jako guard clauses czy polimorfizm.
Zamień zagnieżdżone warunki na guard clauses:
Przed:
function getDiscount(user) {
if (user) {
if (user.isActive) {
if (user.plan === 'pro') {
return 0.20;
} else {
return 0.05;
}
} else {
return 0;
}
} else {
return 0;
}
}Po:
function getDiscount(user) {
if (!user) return 0;
if (!user.isActive) return 0;
if (user.plan === 'pro') return 0.20;
return 0.05;
}Ta sama logika. Znacznie łatwiej skanować. Wczesne returny eliminują piramidę wcięć i pozwalają czytać każdy warunek w izolacji.
Zamień warunki na polimorfizm gdy masz ten sam check if type === X powtarzany w wielu funkcjach. Zdefiniuj klasę bazową lub protokół, jedną implementację na typ, i pozwól dispatchowi dziać się w miejscu wywołania. Pomiń to jeśli: masz tylko dwa typy i trzeci jest mało prawdopodobny. Koszt abstrakcji nie jest tego warte dla binarnego wyboru, który nigdy się nie rozrośnie.
Konsoliduj duplikaty warunków. Jeśli ten sam warunek sprawdzasz w trzech oddzielnych miejscach przed zrobienie nieco innych rzeczy, wyodrębij warunek do nazwanej funkcji predykatu. if (isEligibleForDiscount(user)) czyta się lepiej niż powtarzanie trzyczęściowego sprawdzenia za każdym razem.

Przenoszenie funkcji między obiektami: niedoceniana technika
Czasem metoda jest w złej klasie. Nie dlatego, że kod jest zły, ale dlatego, że operuje na danych z innej klasy więcej niż na swoich. To sygnał, by ją przenieść.
Wzorzec Move Method jest prosty: identyfikuj metodę, sprawdzaj od której klasy zależy, przenieś tam, i aktualizuj wszystkich wywołujących. Lint z IDE wychwyci złamane odniesienia.
Gdzie to pokazuje się najbardziej w side projectach: klasy narzędziowe, które gromadzą wszystko, co nie pasuje oczywistym miejscom. Pliki helpers.js lub utils.py, które rosną do 400 linii. W pewnym momencie te funkcje należą do obiektów domeny, które są operowane. Funkcja, która bierze obiekt User i robi pięć rzeczy, należy do User, nie do utils.
Omijaj, jeśli jesteś na początku: jeśli projekt ma dwa tygodnie i wciąż się kształtuje, nie over-inżynieruj struktury. Pozwól designowi wyłonić się z rzeczywistego użycia, zanim zainwestujesz w czysty model domeny. Przenoszenie rzeczy zbyt wcześnie oznacza przenoszenie ich znów, gdy wymagania się zmienią.
Zanim refaktoryzujesz: linia bazowa testów
Refaktoryzacja bez testów to hazard. Myślisz, że sprzątasz; możesz psować.
Minimalna sieć bezpieczeństwa: testy charakteryzujące. Napisz testy, które przechwytują bieżące zachowanie kodu, który masz zmienić. Nie to, co powinno robić, ale to, co rzeczywiście robi teraz. Te testy istnieją, by łapać regresje, nie zatwierdzać design.
Mając linię bazową, refaktoryzuj małymi commitami. Jeden wzorzec na commit. Jeśli coś się psuje, wiesz dokładnie, która zmiana to spowodowała. Porównaj z alternatywą: trzydniowa sesja refaktoryzacji commitowana jako jeden ogromny diff. Gdy CI pada o 2 w nocy, nie wiesz, która z 40 zmian to zrobiła.
Praktyczna checklist zanim zaczniesz refaktoryzować:
Napisz testy charakteryzujące dla modułu, którego to dotyczy
Upewnij się, że pipeline CI jest zielony zanim zaczniesz
Identyfikuj hotspoty 80/20: kod, który jest zarówno złożony i często modyfikowany
Wybierz jedną technikę, zastosuj, przejdź na zielono, commituj, potem przejdź do następnej
Trzymaj commity refaktoryzacji osobno od commitów funkcji, aby diff pozostawał czytelny
Przydatny constraint: jeśli nie możesz wyjaśnić refaktoryzacji w jednym zdaniu w wiadomości commita, robisz zbyt wiele naraz.
Organizowanie danych: wzorzec, który poprawia wszystko inne
Wiele brudnej logiki warunkowej istnieje, bo model danych jest zły. Gdy przekazujesz surowe primitywne zamiast obiektów domeny, kończy się na warunkowych checkach wszędzie.
Zamiana primitywu na obiekt (email stringowy staje się EmailAddress, centy intem staje się Money) daje ci miejsce dla logiki walidacji. Zamiast sprawdzania if not re.match(EMAIL_REGEX, email) w 12 miejscach, konstruktor EmailAddress robi to raz.
To wzorzec "Replace Type Code with Class" z książki Fowlera. Nie jest spektakularne, ale redukuje rozrzedzenie warunkowe lepiej niż prawie wszystko inne w książce.
Ta sama zasada dotyczy magicznych liczb: if status == 3 jest nieczytelne. if status == OrderStatus.SHIPPED nie. Zamiana magicznych liczb na nazwane stałe zajmuje pięć minut i spłaca się za każdym razem, gdy ktoś czyta kod.
Kiedy NIE refaktoryzować: przypadki, które ludzie omijają
Tu większość poradników się kończy. Voilà ce qui coince en pratique: nie każdy kod zasługuje na refaktoryzację, a traktowanie oczyszczania jako zawsze-dobrego to po prostu dług techniczny innego rodzaju.
Nie refaktoryzuj tuż przed twardym deadline. Refaktoryzacja wprowadza zmianę. Zmiana wprowadza ryzyko. Jeśli masz demo dla klienta za 48 godzin, to nie jest czas.
Nie refaktoryzuj kodu, który masz usunąć. Jeśli funkcja jest cięta w następnym sprincie, czyszczenie jej implementacji to marnowanie pracy. Potwierdź usunięcie zanim zainwestujesz w oczyszczanie.
Nie refaktoryzuj dla stylu samego w sobie. Jeśli kod działa, ma testy, i nikt go nie dotyka, fakt, że napisałbyś to inaczej, to nie jest uzasadnienie do zmiany. Ryzyko regresji jest rzeczywiste; korzyść jest estetyczna.
Nie refaktoryzuj bez jasnego celu. "Posprzątaj to" nie jest zadaniem. "Wyodrębnij logikę walidacji z OrderProcessor do dedykowanej klasy Validator" to zadanie. Niewyraźny zakres prowadzi do scope creep, co prowadzi do gałęzi, która żyje przez trzy tygodnie i meruje się z konfliktami.
To nie jest doskonały zestaw reguł. To jest możliwy, a w kodzie produkcyjnym, możliwy pokonuje doskonały.
Co naprawdę wygląda jak sześć miesięcy przyrostowej refaktoryzacji
Zespół, który refaktoryzuje przyrostowo, trochę każdy sprint, zgodny z funkcjami, które wysyłają, kończy się z kodem, którego tanio pracować. Nie szybciej pisać początkowo, ale szybciej rozszerzać, szybciej debugować, szybciej onboardować nowych współpracowników.
Dla solo builderów: payoff jest osobisty. Ty jesteś nowym inżynierem za sześć miesięcy. Kod, który sprzątasz dzisiaj, to kod, który będziesz rozumieć bez czytania w Październiku.
Sześciomiesięczny obraz z projektu systemu rekomendacji: zaczęliśmy z plikiem usługi, który był 1200 linii długi i nie miał testów. Wyciągaliśmy metody podczas każdego sprintu funkcji, dodawaliśmy testy charakteryzujące, gdy dotykaliśmy każdej sekcji, i przenosiliśmy rzeczy do właściwych klas, gdy lepiej rozumieliśmy domenę. Po sześciu miesiącach ta sama logika żyła w dziewięciu plikach średnio 130 linii każdy, wszystkie testowane, wszystkie nazwane dla tego, co robią. Częstotliwość deploymentu poszła z tygodniowej na codzienną. Średni czas odzyskiwania z buga spadł o około 40%.
Te liczby nie pochodzą ze studium; pochodzą z jednego zespołu, jednego systemu. Ale kierunek się utrzymuje. Mniejsze, czystsze jednostki są szybsze do zmiany. O to chodzi.
FAQ
P: Od czego powinienem zacząć refaktoryzować w starej bazie kodu?
O: Zacznij od hotspotów 80/20 - kod, który jest zarówno złożony, jak i często modyfikowany. Uruchom git log --stat aby zobaczyć, które pliki są dotykane najczęściej. Te pliki są najlepszymi kandydatami.
P: Czy powinienem pisać testy przed refaktoryzacją? O: Tak. Napisz testy charakteryzujące, które przechwytują bieżące zachowanie. Nie idealne testy - po prostu testy, które mówią, co kod robi teraz. Te będą twoją siecią bezpieczeństwa.
P: Jak długo powinno trwać refaktoryzowanie jednego pliku? O: Zależy od rozmiaru, ale stara się czymać małymi krokami. Jeśli sesja trwa dłużej niż kilka godzin, dziel ją. Commity powinny być proste do zrozumienia.
P: Czy mogę refaktoryzować i dodawać funkcje w tym samym commicie? O: Nie. Trzymaj je osobno. Refaktoryzacja zmienia strukturę; nowe funkcje dodają zachowanie. Gdy coś pójdzie nie tak, jasne będzie, gdzie szukać.
P: Jak sobie radzić z presją, aby szybko dostarczać funkcje zamiast refaktoryzować? O: To rzeczywisty konflikt. Refaktoryzacja to inwestycja, której korzyści widać za miesiące. Taktyka: refaktoryzuj przyrostowo podczas tworzenia nowych funkcji.