Tuesday, February 19, 2013

Strategiczna refaktoryzacja

Dlaczego "strategiczna"? Ponieważ od kiedy po raz pierwszy zobowiązałem się do "Niech Pan coś zrobi z tym kodem", stało się całkowicie dla mnie jasne, że techniki refaktoryzacji to za mało. Refaktoryzacje refactoring.com, czy Refactoring to Patterns są genialne i precyzyjnie opisują jak transformować kod z jednej postaci w inną, lecz są to zbyt niskopoziomowe przekształcenia o zbyt małej "sile rażenia", aby mogły przynieść wymierny efekt w mocno zagmatwanym i wciąż zmieniającym się kodzie.

O czym dokładnie mówię? Mówię o projektach rozwijanych od dziesięciu lub więcej lat. Ilość wierszy kodu liczona jest wtedy w milionach. Wyobraź sobie ten projekt, a w nim plik *java (zawierajacy jedną klasę) o rozmiarze 2.8MB, co przekłada się na około 60k wierszy kodu w klasie. Dalej wyobraź sobie, że podobnych klas jest kilkaset w tym projekcie. W takim razie jaką konkretnie zmianę spowoduje chociażby Replace Conditional with Polymorphism? Albo inaczej: ile pieniędzy w kontekście najbliższego kwartału pozwoli to zaoszczędzić? Wydaje mi się, że takiego bilonu w Polsce nawet się nie bije. Wprowadzona zmiana utonie w odmętach wszechogarniającego spaghetti. A system wciąż się rozwija, kodu wciąż przybywa.

Kluczowe założenie: To ma sens

Szczegóły na ten temat umieściłem w poście Megasoftwarecraftsmanshipper. Moim zdaniem nie da się sensownie przeprowadzić znaczącej refaktoryzacji, jeśli w pierwszej kolejności nie zrozumiemy dokładnie przyczyn, dla których kod znajduje się obecnie w takiej a nie innej postaci. Bez takiej analizy najczęściej podejmowane są refaktoryzacje, które od dawana chodzą zespołowi po głowie, są ciekawe od strony technicznej, bywają czasochłonne i jednocześnie nie prowadzą do znaczącej poprawy.

Może jest to banalne, ale tego typu analizę najłatwiej przeprowadzić przy założeniu, że architektura kodu ma sens, że komuś kto ją tworzył, przyświecały racjonalne przesłanki do podjęcia decyzji, które podjął. Zamiast zadawać sobie pytania: co to $^&#@#$ jest!? , itp., stawiasz następujące: "co musiałbym myśleć, aby stworzyć taką architekturę? w jakiej sytuacji musiałbym się znajdować?". Ponoć indiańskie przysłowie mówi, że "możesz kogoś oceniać dopiero wtedy, gdy przejdziesz milę w jego mokasynach". Coś w tym jest.

Zidentyfikuj obszary refaktoryzacji

Nie wszystkie fragmenty systemu są jednakowo ważne. Refaktoryzuj te, które są najważniejsze. Jako atrybuty do klasyfikacji możesz wziąć pod uwagę:
  • najczęściej zmieniające się fragmenty
  • fragmenty, na które raportowanych jest najwięcej błędów
  • fragmenty, z którymi programiści mają najwięcej problemów
  • fragmenty najmniej/najbardziej pokryte testami

Pomocnym narzędziem do analizy w/w atrybutów są raporty z Twojego traca albo repozytorium. Kilka informacji znajdziesz w artykule Your VCS – the forgotten feedback loop

Najpierw zatrzymaj degradację kodu

Idę tę opisałem na na przykładzie radzenia sobie z workarounds w kodzie.
Innym, bardziej powszechnego użytku przykładem, może być skorzystanie z zasady Interface Paritioning.



Gdy mamy wielgachne (60k w pliku txt; rozmiar metod od 20 wierszy od 1k) klasy jak na rysunku, od których zależy wiele innych, to pierwszym problemem nie jest ich rozmiar, lecz to, że sytuacja wciąż się pogarsza ze względu na trwające prace, a więc stale przyrasta ilość klas zależnych od naszego potwora. Jeśli zabierzesz się za rozkładanie tej klasy na mniejsze kawałki, to może to zająć tyle czasu, że w końcu spotkasz się z Jeźdźcami Apokalipsy. Jeśli jednak na najpierw posegregujemy metody z potwora pomiędzy interfejsy, to spowoduje to zatrzymanie postępującego pogarszania się kodu.



Skupiamy się, na zatrzymaniu powstawania nowych zależności. Empirycznie sprawdziłem, że poszatkowanie na interfejsy klasy zawierającej ok 1k metod, dwóm osobom, które w miarę często z niej korzystają zajmuje jakieś 4-6 godzin. Po takim zabiegu można sobie już zaplanować spokojne wydzielanie mniejszych komponentów z potwora.

Drugim przykładem na powstrzymanie postępującego bałaganu w kodzie jest Replace Method with Method Object. Ten refaktoring kojarzy mi się z pączkowaniem drożdży.



Chodzi tu przede wszystkim o to, aby "wypchnąć" dużą ilość kodu poza wielgachną klasę i tam sobie już spokojnie rozkminiać dany fragment i go porządkować.

Te dwa sposoby działania bardzo dobrze ze sobą współpracują:
  1. Interface Partitioning - tworzy w okół potwora skorupę, dzięki której nie może on już wpływać na inne klasy
  2. Replace Method with Method Object - pomaga rozczłonkować potwora na kwałki



Zarządzaj refaktoryzacją również na poziomie mikro

Oczywiście umiejętności programistów są jednym z kluczowych blokad przeciw pogarszaniu się jakości kodu.



Mam tu na myśli powtarzalny proces pracy z kodem, który nazywam Naturalnym Porządkiem Refaktoryzacji. Szczegółowo możesz się zapoznać z tą koncepcją tutaj:

Hipoteza o pewnym Punkcie:)

Przez analogię do Promienia Schwarzschilda mam taką hipotezę, że dla każdego systemu istnieje taki poziom bałaganu w kodzie, po przekroczeniu którego, zespół nie jest już w stanie kompetencyjnie podołać refaktoryzowaniu systemu i każde podjęte działanie pogarsza tylko jakość kodu.

Żeby znaleźć rozwiązanie w tej sytuacji, należy zacząć inaczej myśleć o refaktoryzowaniu kodu oraz o jego jakości. Z mojego punktu widzenia architektura jest emanacją relacji pomiędzy biznesem a IT. Jeśli relacja ta jest nieekologiczna, to żadna magiczna sztuczka nie poprawi designu. Dalej, refaktoryzacja jest dla mnie problem organizacyjnym, a nie technicznym, czy kompetencyjnym.

Co z wynika z powyższych dywagacji. Bardzo prosta rzecz. Możliwe staje się zastosowanie wytycznej: nie szukaj rozwiązania problemu na poziomie, na którym się on rozgrywa, przejdź poziom wyżej. Zatem po przekroczeniu wymienionego w H3 Punktu, jedyne co może poprawić jakość kodu to refaktoryzacja na poziomie organizacyjnym. Właśnie tak, zmień sposób zorganizowania prac nad projektem, a przełoży się to na poprawę architektury i jakości kodu. Możesz zacząć od przyjrzenia się następującym relacjom:
  • współpraca między zespołami deweloperskimi
  • współpraca między zespołami deweloperskimi a zespołami testującymi
  • współpraca między zespołami deweloperskimi a biznesem

Podam przykład, w pewnej firmie postanowiono wprowadzić Zespół ds. Trudnych i Beznadziejnych. Zajmowali się oni naprawianiem problemów, których nikt inny nie potrafił naprawić. Przez pewien czas rozwiązanie sprawdzało się świetnie. Przez pewien czas, bo po ośmiu miesiącach zaczęły pojawiać się następujące objawy:
  • najlepsi programiści znaleźli się w Zespole
  • najlepsi programiści nie mieli czasu na uczenie innych jak rozwiązywać trudne problemy
  • zwykli programiści przykładali mniejszą wagę do jakości kodu, bo wiedzieli, że Najlepsi wychwycą każde niedociągnięcie
  • Zespół Najlepszych miał coraz więcej pracy
  • zwykli programiści coraz mniej dbali o jakość kodu
  • jakość kodu drastycznie spadła
  • Najlepsi programiści nie mieli czasu na refaktoryzację, zwykli programiści nie potrafili jej przeprowadzić
Paradoksalnie można stwierdzić, że kod był pisany po to, aby Zespół ds. Trudnych i Beznadziejnych miał co robić:)


Najważniejsze założenia przestawione w tym artykule

  • Pierwszym celem strategicznej refaktoryzacji jest powstrzymanie postępującej degradacji w kodzie; drugim celem jest poprawa jego jakości
  • Aby zaproponować sensowny plan refaktoryzacji trzeba w pierwszej kolejności zrozumieć przyczyny powstania legacy code
  • Umiejętności programisty to kluczowa rzecz, która poprawia jakość kodu
  • Refaktoryzację można prowadzić na poziomie technicznym tylko do pewnego momentu, potem należy przenieść się na poziom organizacyjny