wSołtysiak BlogPraktycznie o tworzeniu oprogramowania i nie tylko
12 czerwca 2024

Epic branch to antywzorzec

Podczas pracy przy większych zadaniach pojawia się czasami pomysł, żeby prace były realizowane na osobnej, długo żyjącej gałęzi kodu zwanej potocznie epic branchem. Taka decyzja często mści się później na projekcie w wielu różnych aspektach. W tym wpisie przedstawię, dlaczego epic branch to antywzorzec oraz w jaki sposób można osiągnąć te same korzyści, ale bez negatywnych skutków ubocznych.

Epic branch w repozytorium

Czym jest epic branch?

Zanim odpowiemy sobie na to pytanie, należy przypomnieć sobie, czym jest feature branch. Feature branch to osobna gałąź kodu, na której programista wprowadza pojedyncze zmiany do projektu bez konfliktowania się z innymi. Po zakończeniu implementacji zmiany powinny zostać przejrzane w ramach dobrego Code Review, a następnie zmergowane do mastera. W idealnym świecie, gdzie zadania są jasno zdefiniowane i odpowiednio podzielone, cały proces zabiera od kilku godzin do kilku dni w zależności od poziomu skomplikowania zmian.

Epic branch to większy brat feature brancha. Idea jest prosta. Skoro małe zadania realizujemy na osobnym branchu to i duże zadanie, które nie może być dostarczone w częściach, możemy zrealizować w dokładnie ten sam sposób. Epic branch jest więc długo żyjącą (tygodnie, miesiące, kwartały) gałęzią kodu z bardzo dużą liczbą zmian.

Podczas pracy natknąłem się również na zmodyfikowany wariant, który nazywam master epic branchem. Commity nie są wprowadzane bezpośrednio do gałęzi, lecz analogicznie do mastera, za pomocą mniejszych feature branchy przechodząc po drodze proces sprawdzania kodu.

Kiedy tworzymy epic branch?

Pomysł na zastosowanie epic brancha pojawia się zwykle w sytuacjach gdy chcemy zaimplementować duże funkcjonalności, których na pierwszy rzut oka nie ma jak sensownie podzielić na mniejsze części. Czasami jest to typowa logika biznesowa. Zdarzają się także zadnia czysto techniczne np. przebudowanie architektury aplikacji lub zastąpienie biblioteki.

Ciekawym przypadkiem, jest również utworzenie osobnej gałęzi kodu, żeby mieć gdzie „wygrzać” nową funkcjonalność. Mamy wtedy sytuacje, gdzie dwie wersje aplikacji (albo i więcej) są rozwijane równolegle. Daje to pewność, że zmiany z jednej wersji nie zepsują nic w drugiej. Dopiero po wnikliwych testach (często na produkcji) takie wersje są scalane.

Problemy epic brancha

Mimo iż z początku wszystko wygląda naprawdę dobrze, to czym dłużej żyje epic branch, tym więcej problemów się pojawia. Od drobnych, ale frustrujących, aż do poważnych, które zagrażają płynności wprowadzania zmian do projektu. W najgorszym przypadku skutkuje to dużymi utrudnieniami i opóźnieniami w pracach.

Syzyfowe konflikty

Równolegle do epic brancha w repozytorium prowadzone są inne prace, które czasami powodują konflikty. Odpowiedzialni programiści będą je rozwiązywać na bieżąco. Prowadzi to do dwóch problemów: przepalaniu czasu dewelopera oraz poczucia wykonywania syzyfowej pracy.

Osoby pracujące poza feature branchem, zwykle nie mają dokładnego pojęcia, jakie zmiany zostały wprowadzone na branchu. Cała odpowiedzialność za dostosowanie wytworzonego wcześniej kodu do zmian spada więc na osobę opiekującą się epic branchem. Gdyby zmiany z epic brancha pojawiały się stopniowo w masterze, to ta odpowiedzialność zostałaby odwrócona i podzielona na wiele osób.

Rozwiązywanie konfliktów wymaga zapoznania się ze zmianami innych osób oraz zrozumienia, w jaki sposób działa nowy kod. Dopiero wtedy możemy odpowiednio dostosować swoje rozwiązanie. W przypadku gdy zmiany w masterze nie są konsultowane na bieżąco, mogą one wywrócić całą koncepcję zmian z epic brancha do góry nogami. Dodatkowa praca z tym związana opóźni projekt, a programiści będą coraz bardziej zniechęceni.

Wszystkie te problemy mogą prowadzić do sytuacji, gdzie zespół pracujący na epic branchu będzie chciał opóźnić wdrożenie innych zmian do głównej gałęzi. Zgadzając się na taką propozycję, budujemy kolejkę zmian oczekujących na finalizacje. Jeżeli pierwsza z tych zmian będzie miała poślizg, to wszystkie inne także zostaną przesunięte w czasie. Należy unikać takich zależności, ponieważ nikt nie docenia skończonych funkcjonalności, których nie można wydać na produkcję.

Konflikt na raz

Część programistów nie chcąc wpaść we wcześniej opisaną pułapkę, postanawia przesunąć moment synchronizacji zmian aż do momentu zakończenia etapu implementacji. Na początku wydaje się to bardzo dobrym pomysłem. Środowisko jest stabilne, nie trzeba też tracić czasu na konflikty. Jest to jednak złudne i prowadzi do innych problemów, które pojawiają się w momencie, kiedy wszyscy są przekonani, że zadanie zostało ukończone.

Rozwiązanie konfliktów, które nawarstwiały się kilka tygodni albo i miesięcy jest nie lada wyzwaniem. Zwykle wymaga bardzo dużej dokładności i pomocy osób z zewnątrz, które wprowadzały konfliktujące zmiany. Istnieje spora szansa, że jakiś konflikt zostanie źle rozwiązany i wprowadzi regresje, która może wyjść w dowolnym miejscu systemu. Nie ma także pewności, że po tak dużych zmianach nasze rozwiązanie nadal będzie działać stabilnie. Dochodzi więc dodatkowy czas na ponowne testy i kolejne iteracje z poprawkami ewentualnych błędów.

Kolejnym problemem opóźnienia synchronizacji zmian jest obniżenie jakości kodu. Jeżeli w trakcie prac zmieniła się funkcjonalność lub zależność, na której opieramy nasze zmiany, to dopiero pod sam koniec prac można na to zareagować. Oznacza to zwykle o wiele większy nakład pracy, ponieważ trzeba dostosować kod całego rozwiązania. Często na poprawne przebudowanie funkcjonalności jest już zwyczajnie za późno. To właśnie w tym momencie najczęściej powstają „tymczasowe rozwiązania do poprawienia po wyjściu na produkcje”.

Zmiany za szybą

Podczas tworzenia funkcjonalności zdarza się przy okazji poprawić jakiś błąd lub zrefaktoryzować kawałek kłopotliwego kodu. Jest to bardzo pozytywny efekt uboczny, jednak z powodu długiego okresu życia epic brancha, efekty tych zmian są mocno odsunięte w czasie. Niestety, jeżeli taka zmiana jest nagle potrzebna w głównej gałęzi lub innym zadaniu nie możemy jej w prosty sposób użyć.

Epic brancha zmergować bezpośrednio nie można, ponieważ nie jest skończony. Cherry-pick nie zawsze daje radę, szczególnie jeżeli osoba implementująca nie pilnowała, żeby commity były atomowe. Pozostaje jedna opcja — ręczne przeniesienie zmian i ich sprawdzenie. Ponownie tracony jest czas na pracę, która została już wykonana. Dodatkowo istnieje jeszcze możliwość, że poprawka będzie się nieco różnić, co spowoduje kolejne konflikty.

Analogiczna sytuacja występuje wtedy, gdy kilka funkcjonalności jest ze sobą powiązana na epic branchu, a biznes chcę jedną konkretną funkcjonalność wykorzystać wcześniej. Prowadzi to oczywiście do tych samych problemów, a dodatkowo nie zawsze jest możliwe do zrealizowania w rozsądnym czasie.

Frustrujące Code Review1

Sprawdzanie zmian składających się z ponad tysiąca nowych linii kodu wymaga poświęcenia dużej ilości czasu. W przypadku dziesięciu tysięcy i więcej wierszy czas można liczyć już nie w godzinach, a w dniach. Takie Code Review jest bardzo wyniszczające dla obu stron: autorów oraz osób sprawdzających.

Autorzy kodu mogą czuć się przytłoczeni liczbą uwag. Przy takiej skali zmian, sto uwag nie powinno robić większego wrażenia. Istnieje więc ryzyko wypalenia. Praca jest przecież na samym końcu, a teraz nagle trzeba wrócić do dyskusji na temat rozwiązań opracowanych kilka tygodni temu. Presja czasu i chęć zamknięcia zadania powoduje, że następują próby odrzucenia wielu zasadnych uwag, a dostarczane poprawki nie zawsze są zadowalającej jakości.

Strona sprawdzająca kod w krótkim czasie musi zapoznać się ze zmianami, które były tworzone przez tygodnie lub miesiące. Nie jest to proste zadanie, wiele osób w połowie może się już zniechęcić i obniżyć standardy Code Review.

Ponadto przegląd kodu na tak późnym etapie traci mocno na wartości. Można posłużyć się tutaj analogią statku i rejsu. Lekka zmiana kursu na początku rejsu pozwoli dopłynąć w zupełnie inne miejsce praktycznie bez wysiłku, natomiast mocna zmiana kursu na samym końcu drogi już niewiele zmienia. Jeżeli wprowadzenie zmian do uwagi wymaga przeprojektowania większości wytworzonego kodu, to w najlepszym przypadku dzieje się to dopiero po zmergowaniu w ramach spłaty długu technologicznego. Ta sama zmiana byłaby wprowadzona bezproblemowo na początku prac i prawdopodobnie zaoszczędziłaby kilku kolejnych poprawek w innych miejscach.

Alternatywne podejście

Sposobem na wszystkie problemy jest podzielenie naszego zadania na kawałki, mimo że wydawać by się mogło, że nie da się tego zrobić. Mimo iż poniższe sposoby mogą brzmieć banalnie, to przynoszą świetne efekty. Dzięki nim możemy wrócić do zwykłych, krótko żyjących feature branchy i czerpać z korzyści ich zastosowania.

Feature flags

W przypadku gdy nie chcemy, żeby użytkownik widział częściowe efekty naszej pracy, możemy zastosować tzw. feature flags. Idea polega na tym, że jeżeli nie ustawimy pewnej wartości, to nigdy nie uruchomimy wybranej konkretnej części kodu. Możemy zrobić to trywialnie i wstawić w kodzie przełączniki na sztywno. Można do tego także użyć zmiennych środowiskowych. Jeżeli mamy aplikację webową to ustawienie ciastek, local storage lub parametru w URLu też wchodzi w grę. Jak widać, sposobów jest wiele i zawsze można tutaj coś ciekawego wymyślić.

Pamiętajmy jednak o usuwaniu przełączników, kiedy nie będą już potrzebne. Każda taki przełącznik to kolejna ścieżka w kodzie i dodatkowa komplikacja. Zbyt duża liczba przełączników niesie ze sobą problemy m.in.:

  • testowanie aplikacji staje się trudniejsze, ponieważ każdy przełącznik zmienia zachowanie programu
  • zmiany ukryte za przełącznikami mogą ze sobą kolidować
  • jeden przełącznik może być zależny od drugiego, trzeba więc pamiętać jak je ze sobą łączyć

Takich zagwozdek można jeszcze mnożyć i mnożyć. Tymczasowe przełączniki są jak najbardziej w porządku, ale utrzymywanie całego systemu przełączników to już katorga. Po więcej informacji na temat feature flags polecam zajrzeć tutaj.

Oznaczanie jako deprecated

Jeżeli potrzebujemy podmienić szeroko używany mechanizm (np. funkcję, klasę, komponent) i nie chcemy zamykać się w piwnicy na kilka miesięcy, to najlepszym rozwiązaniem będzie oznaczenie takiego elementu jako przestarzały.

Większość IDE potrafi odczytać stosowny komentarz/adnotację i wyróżnić definicje elementu wraz z użyciami (np. przez przekreślenie). Dzięki temu mamy duże szanse, że nikt więcej już go nie użyje.

Następnym krokiem jest utworzenie zamiennika. Informacje o nowym rozwiązaniu należy umieścić przy definicji starego kodu, tak aby każdy wiedział, co może wykorzystać w zamian. Zamiennik powinien być opracowany w taki sposób, żeby móc szybko i bez większego zastanowienia podmienić stare użycia. W przypadku gdy interfejs nowego rozwiązania znacznie różni się od poprzedniego, warto pomyśleć o wykonaniu dodatkowego adaptera.

Ostatni krok to przyjęcie zasady w projekcie, że jeżeli w trakcie prac zauważymy użycie przestarzałego kodu, to podmieniamy go na nowe rozwiązanie. To właśnie dlatego tak istotne jest maksymalne uproszczenie procesu podmiany rozwiązań. W przypadku usunięcia ostatniego wystąpienia należy usunąć także samą definicję. Takie postanowienie można egzekwować podczas Code Review.

Zaletami tego rozwiązania jest rozłożenie pracy na wiele osób oraz natychmiastowa możliwość używania nowego kodu. Jedynym kompromisem, na który musimy pójść, jest to, że stare rozwiązanie będzie żyło jeszcze bardzo długo, a w pesymistycznym wariancie zostanie z nami do końca. Nie zawsze jest to do zaakceptowania, ale w wielu przypadkach to dobre wyjście.

Podsumowanie

Moim zdaniem epic branch to antywzorzec, ponieważ przynosi zdecydowanie więcej problemów niż korzyści. Kluczem do uniknięcia pułapek jest podzielenie prac na mniejsze części. W przypadku gdy nie jest to łatwe, można wspomóc się praktykami opisanymi w tym wpisie.

Jeżeli jednak epic branch jest jedynym wyjściem, jakie widzisz — zastosuj master epic brancha. Wymusza on wprowadzanie nowego kodu w mniejszych częściach oraz regularne Code Review. Łącząc to z częstą synchronizacją zmian z główną gałęzią, można uniknąć większości problemów.


  1. Problem nie występuje w przypadku master epic branchy, których podstawą są zmiany sprawdzane na bieżąco.