Finalna praca nad kodem i jego implementacją to żmudny proces przechodzenia przez wszystkie zmiany oraz ich weryfikacji. Często jest to męczący moment, w którym frustracja jest wprost proporcjonalna do czasu spędzonego nad kodem. Co zrobić, aby uporządkować swoją pracę i unikać łez pod prysznicem na myśl o kolejnej implementacji? Proponuję metodę małych kroków – przemyślanych i działających.
Z czym miałem problem?
Wyobraź sobie, iż po kilkudziesięciu minutach (lub godzinach, ups!) pracy na dużym flow, otrząsasz się z transu, a Twój edytor kodu sygnalizuje 50 wprowadzonych zmian w totalnie różnych miejscach aplikacji.
Niczym Ant-Man patrzysz na pobojowisko kodu i zadajesz sobie pytanie „co tu do cholery się stało?”. Okazuje się, iż nie wszystko działa i sporo jeszcze pracy, bo rozgrzebałeś zdecydowanie za dużo. Część zmian musi jednak zostać ponownie zmodyfikowana, a to pociąga za sobą kolejne zależne elementy. Na domiar złego, nie poprawiłeś ani jednego testu.
Wiesz, iż oznacza to jedno – potrzebę przejścia przez wszystkie zmiany, ich weryfikacji, usuwania zbędnego kodu i poprawiania testów. W sumie można powiedzieć, iż zaczynamy od początku iterację numer dwa. Tylko siły już nie te i w głowie kiełkuje myśl, aby zostawić tę pracę na jutro – w końcu dziś już tyle zmieniłeś…
Finalnie takie podejście, choćby następnego dnia, doprowadzało mnie do stanu, w którym nie chciało mi się choćby kiwnąć palcem. Narzekałem na swoich poprzedników, a tak naprawdę, wczoraj zostawiłem większy bałagan.
Udało mi się jednak z podejścia “robię wszystko na raz”, przejść na metodę małych kroków – przemyślanych i działających.
Pokażę i przeprowadzę Cię przez mój aktualny proces wprowadzania zmiany w oprogramowaniu. Zobaczysz, w jaki sposób dzielę sobie etapy pracy oraz na co szczególnie warto zwrócić uwagę.
Zaczynamy od dobrego planu
Pomyśl jak podejść do problemu, aby całościowo ukończyć aktualnie wykonywane zadanie. Skup się na celu i tylko tych zmianach, które do niego prowadzą.
Ważne! Rzadko kiedy zwracam uwagę na zadania, które będę realizował za tydzień, Sprint czy miesiąc. Ich czas nadejdzie, gdy rozpocznie się praca nad nimi. Nie raz przygotowywałem kod podczas implementacji pod najbliższe zmiany, a finalnie nie było takiej potrzeby, ponieważ zmieniły się wymagania i wykonana praca okazywała się stratą czasu. To ciężkie zadanie, bo z każdej strony atakują Cię zasadami SOLID, DRY i resztą akronimów, które zamiast pomagać pisać zwięzły kod, rozwlekają go w zbędną na ten moment abstrakcje.
Przykładowo, musisz podjąć decyzję czy wdrożyć wzorzec strategii, czy jeszcze nie. Jednak od tego wszystkiego ważniejsze jest czy to co teraz robię, będzie można stosunkowo gwałtownie zmienić / podmienić lub usunąć w przyszłości, bez zaorania połowy kodu aplikacji. Dbam o sprzężenie z resztą kodu i luźne powiązanie z nim.
Często rozpisuje sobie etapy na kwadratowej karteczce, aby po wykonaniu poszczególnych kroków skreślać je z listy, a po wykonaniu zadania wyrzucić ją do kosza. To taki krótki moment satysfakcjonującego triumfu.
Oczywiście możesz skorzystać z wirtualnych sticky notes.
Ten plan nie jest jednoznaczny ze zmianami, które wprowadzam, ponieważ można rozbić go na zdecydowanie więcej małych kroków.
To mój wysokopoziomowy plan na pracę, ale nie na poszczególne zmiany.
Wprowadź jedną zmianę
Po przygotowaniu planu przychodzi czas walki z najtrudniejszą rzeczą – dekompozycją na mniejsze.
Badanie i wyznaczanie granicy gdzie zaczynam pracę, a gdzie ją kończę to pewnego rodzaju heurystyka. Ja opieram ją o dwa czynniki.
Czynnik pierwszy. To rodzaj wprowadzanej zmiany, a zdefiniowałem ich pięć różnych:
- Zmiany w strukturze plików i katalogów – zmiany nazw, przenoszenie w inną lokalizację,
- Refaktoryzacja – przygotowanie podłoża w myśl Zasady Skautów,
- Implementacja – najbardziej atomowej, niezależnej część funkcji, nad którą pracujesz,
- Naprawienie błędu,
- Usunięcie niepotrzebnego, nieużywanego fragmentu kodu.
Zawsze staram się wykonywać jedną zmianę na raz i nigdy nie łączyć ich razem. Wyjątkiem są zmiany wykluczające np. zmiana nazwy pliku, która rozwiązuje błąd.
Tutaj pułapka się nie kończy, bo dekompozycji nie da się opisać w pięciu prostych punktach.
Czynnik drugi. Weryfikuję jeszcze czy zmiana spełnia założenia, tak aby:
- Była atomowa,
- Była maksymalnie niezależna od rzeczy, które dodam w przyszłości – czyli nie wymaga czegoś, czego jeszcze nie ma,
- Może być zależna od rzeczy, które dodałem wcześniej – czyli może wykorzystywać to co przygotowałem wcześniej,
- Zmieniała jedno konkretne zachowanie,
- Najlepiej, gdy dotyczy tylko jednego pliku,
- Najlepiej, gdy paroma słowami jesteśmy w stanie opisać zaistniałe zmiany – to ułatwia tworzenie wiadomości w commicie,
- Zaczynam od najgłębszych zmian – tych najbardziej zaszytych i na końcu łańcucha wywołań.
Przykładem może być:
- Dodaję funkcję do przeliczania X. Nigdzie natomiast nie pozostało używana (nowa funkcja w klasie MathCalculations),
- Dodaję nowy endpoint w którym będę wywoływał operacje (tylko akcja w Controller wraz ze ścieżką),
- Dodaję walidację na dane przychodzące do endpointa,
- Dopisuję logikę biznesową (np. w UseCase),
- Łączę wywołanie logiki z endpointem.
Teoretycznie i praktycznie mogę w trakcie pracy, scalać moje zmiany do głównej gałęzi rozwojowej. Nic się nie powinno stać, ponieważ poszczególne elementy są niezależne od następnych kroków.
To oczywiście przykład i może się okazać, iż w przypadku punktu 4 musimy jeszcze dodać nową encję, repozytorium czy wygenerować nową tabelę w bazie danych. To także traktowałbym jako pojedyncze i osobne zmiany.
Tutaj bardzo ważnym aspektem jest pamiętanie, iż teoretycznie jedna zmiana, może wymagać podzielenia jej na wiele mniejszych, które przechodzą dokładnie ten sam proces. Myślałem o trzech zmianach, a finalnie jest ich siedem
Dekompozycja staje się tutaj właśnie tą kluczową umiejętnością, bez której proces, sam w sobie, nic szczególnego nam nie da.
Wypracowanie tej umiejętności nie oznacza, iż zawsze będzie łatwo, ale tego też nikt nie obiecywał. Często musisz znać system, który modyfikujesz – jego architekturę, strukturę kodu oraz zasady, które panują. Bez tego, praca z małymi krokami, jest zdecydowanie dużo trudniejsza. Ale każde kolejne zadanie to poznawanie kodu oraz sposobu, w jaki zrealizowane zostało oprogramowanie. Właśnie to finalnie sprawia, iż poruszamy się po nim szybciej i pewniej.
W tym kroku nie boję się stosować komentarzy // @TODO. To wiadomości do mnie z przyszłości są po to, aby ułatwić pracę mi w kolejnym kroku i finalnie są zastępowane odpowiednią implementacją.
Wszystko ma działać
Po dokonanych zmianach wszystko ma działać. Każde usunięcie linijki, dodanie nowej czy modyfikacja istniejącej nie ma efektu ubocznego w postaci: niedziałających testów automatycznych, wysypanego SCA czy po prostu niedziałającego oprogramowania. Albo co gorsza, choćby takiego, którego nie da się skompilować.
Jaki jest kolejny krok? Sprawdzenie, czy wszystko działa. Weryfikuję nie tylko aplikację u siebie, ale także wszystko, co jest niezbędne do wdrożenia na najbliższe / kolejne środowisko wdrożeniowe.
Więcej o samym podejściu do sprawdzania zmian opisałem w ramach artykułu “Programisto. Testuj!”. Znajdziesz tam listę punktów, o których warto pamiętać podczas weryfikacji zmian.
Zapisz to!
Ostatnim krokiem jest przygotowanie w głowie odpowiedniej historii, która wyjaśnia w jednym zdaniu, jakie zmiany zostały wprowadzone i dlaczego.
- Dodaję pliki do Staging Area (robię git add);
- Wykonuję Git Commit, a stworzoną historię wpisuje w Commit Message;
Teraz mogę przejść dalej. Ta część pracy, którą uważam za zrealizowaną, została zabezpieczona i pozwala mi wykonać następny krok.
Co zyskałem?
Przede wszystkim dzięki jednego narzędzia (Git) mogę monitorować przebieg mojej pracy, a po weekendzie, choćby gdyby w jego trakcie odbywał się wieczór kawalerski kumpla, przypominać sobie co zrobiłem w piątek i od czego dzisiaj zacząć.
Łatwo także cofać swoje eksperymenty, wystarczy wycofać się do danego commita.
Jeśli zadbasz o odpowiednie tworzenie historii wykorzystywanych do Commit Message, jesteś w domu, bo wiesz, co zmieniłeś i dlaczego.
Jeśli o to nie dbasz, to warto zastosować Git Squash, czyli scalić kilka commitów w jeden. Aby dać finalny wynik, a nie sposób dochodzenia do niego. Oczywiście podejście, które wybierzesz, może być zależne od projektu, w którym pracujesz lub Twoich decyzji.
Czy pushować każdy stworzony commit?
To zależy. Czasem pracujesz z kimś na tej samej gałęzi i na Twoje zmiany będzie czekać kolega z zespołu. Czasem po prostu wrzucisz je raz na jakiś czas.
Z czasem odpowiednie dzielenie pracy wejdzie Ci w nawyk i zauważysz, iż idzie ci to coraz szybciej, a zadania są coraz sprawniej realizowane. W końcu praktyka czyni mistrza, albo chociaż – praktyka czyni Cię “praktykiem”
Na koniec polecę jeszcze jeden materiał, warty przeczytania – Commit Often, Perfect Later, Publish Once: Git Best Practices. To w sumie od niego zaczęła się moja przygoda nie tylko z procesem małych kroków, ale całościowym podejściem do pracy z Gitem.