Refaktoryzacja do celu

09 Feb 2018

Refaktoryzacja do celu

1. Zakres artykułu

1.1. Rozpatrywane narzędzia

  1. Podejście ‘od problemu do rozwiązania’ wykorzystane przy refaktoryzacji
  2. Podejście systemowe - analiza części, większej całości oraz otoczenia
  3. Pętla ‘koncepcja-cele-implementacja-weryfikacja’ w praktyce, w sposób możliwy do powtórzenia.
  4. Celowość, iteracyjność, kryterium końca

1.2. Korzyści dla Was

Kiedy warto to przeczytać?

  1. Coraz trudniej Wam poruszać się po istniejącym kodzie i chcielibyście go przebudować - ale boicie się czegoś zepsuć.
  2. Macie kod i chcecie go przebudować - ale od czego zacząć? Co zmienić najpierw? Jak to zrobić, by się udało?
  3. Macie kilka potencjalnych wariantów zmiany kodu i chcecie móc porównać, która byłaby “lepsza”. I obronić swój wybór w zespole, by nie wyjść na łosia.

Co ja chcę Wam dać?

  1. Recepturę na refaktoryzację, która - jak na razie - pozwala mi na szybkie i skuteczne refaktoryzowanie (lub podjęcie decyzji o nie rozpoczynaniu refaktoryzacji).
  2. Chcę Wam pokazać, że refaktoryzacja nie jest czymś bardzo trudnym ani szczególnie strasznym. Jest trudna, ale nie aż TAK trudna jak się wydaje.
  3. Chcę to wszystko pokazać na pewnym studium przypadku (case study) - oraz przygotowałem małą solucję, na której możecie sami poćwiczyć.

2. Streszczenie myśli przewodniej artykułu

Jeżeli nie macie czasu przeczytać tego artykułu, lub jeśli chcecie sobie przypomnieć najważniejsze fragmenty artykułu, przeczytajcie ten punkt. To nie jest spis treści; to ekstrakt reszty artykułu (który stanowi opis techniki, po czym pojawia się case study):

  1. Refaktoryzacja to przebudowa nie zmieniająca wymagań funkcjonalnych.
  2. Refaktoryzacja tak “sama w sobie” nie ma sensu. Musi być “refaktoryzacja do czegoś”, pod kątem celu.
  3. Przebudowa istniejącego kodu wymaga przeanalizowania całości systemu, nie tylko przebudowywanego elementu systemu.
  4. Sukces refaktoryzacji zależy w większym stopniu od właściwego zidentyfikowania problemu i zaplanowania działań niż od umiejętności technicznych.
  5. Proponuję podejście: “Kontekst -> problem -> ideał -> (strategia <-> weryfikacja strategii) -> (działanie -> weryfikacja wyniku)”
  6. Działające techniki: iteracje, ewolucja a nie rewolucja, ustawienie warunków sukcesu.
  7. Bez zrozumienia problemu i domeny refaktoryzacja raczej nie przyniesie maksymalnego zwrotu - może nawet zaszkodzić.
  8. Jeśli nie wiesz, po czym poznasz sukces - nie wiesz kiedy przerwać i raczej nie obronisz rozwiązania przed krytyką.

Jeśli jeszcze jesteście zainteresowani, czas przejść do Bardzo Poważnego Wstępu.

3. Bardzo Poważny Wstęp

3.1. Refaktoryzacja?

Warto może tu zacząć od omówienia słowa “refaktoryzacja”. Zwykle refaktoryzacja rozumiana jest jako “zmiana struktury kodu bez wpływania na działanie samego programu - program ma robić to, co robił wcześniej”. Kryją się w tym trzy pułapki:

Zwykle zaczynamy coś refaktoryzować, bo kod staje się “nieczytelny” czy “trudno się z nim pracuje”. Sęk w tym, że gdzie dwóch programistów tam trzy style programowania…

Bardzo łatwo przez to wszystko ugrzęznąć w bezcelowych kłótniach typu “ale Ty nie masz przecinka w komentarzu!”. Mieliście tak, że spłonęło Wam mnóstwo czasu przy dyskusji o to, czy “piszemy komentarze w kodzie” czy “nie piszemy komentarzy w kodzie - kod ma być samokomentujący”?

Teraz wyobraźcie sobie jak piękne i bezużyteczne wojny mogą wybuchać o wszystko podczas przeprowadzania refaktoryzacji. Aż chce się wziąć popcorn.

Nic dziwnego, że refaktoryzacja cieszy się ogólnie złą opinią:

Rysunek: Do lekko przestraszonego mężczyzny z miną wskazującą na obrzydzenie zbliża się macka ośmiornicy. Mężczyzna wykonuje ruchy wskazujące na to, że zaraz będzie uciekał.

Niestety, refaktoryzacja jest czymś niemożliwym do uniknięcia. Wraz ze zmianą potrzeb biznesu oraz z powiększaniem się aplikacji będzie pojawiała się konieczność refaktoryzacji.

Postawię tu hipotezę, że program który nie jest przebudowywany jest już martwy - pytanie nie brzmi “czy umrze” tylko “kiedy umrze”.

3.2. Ale… dlaczego?! Po co to robić?!

Z uwagi na Stożek Nieoznaczoności.

Podejrzewam, że świetnie kojarzycie poniższy rysunek (bo ciągle trąbię o tym samym):

Rysunek: Stożek nieoznaczoności - wykres przedstawiający linię czasu w poziomie, poziom zrozumienia co piszemy w pionie. Im bardziej w "prawo" tym lepiej rozumiemy co piszemy.

Z powyższego wynika, że najwięcej wiemy o aplikacji gdy kończymy nad nią pracować, a najmniej gdy zaczynamy. Tu pojawia się złośliwe pytanie: kiedy najczęściej podejmujemy kluczowe decyzje na temat aplikacji, takie jak na przykład… decyzje odnośnie architektury? React, Vue czy Angular? Baza grafowa czy SQLowa? Monolit czy mikroserwisy?

Niestety, wiele takich decyzji musimy podjąć na początku pracy. Wtedy, gdy najmniej wiemy.

Wraz z coraz lepszym zrozumieniem naszej aplikacji pojawia się myśl “gdybym tylko TO wiedział wcześniej, zrobiłbym inaczej”. Przez przypadek odcięliśmy sobie niektóre ścieżki. Zrobiliśmy niewłaściwe obiekty stanowe, nasz kod nie odpowiada naszej domenie…

Nie pociesza, że działaliśmy w dobrej wierze i zgodnie z zasadami sztuki. Nadal - pozmieniało się i teraz musimy się z tym męczyć. Albo przebudujemy kod tak, by dalej się z nim łatwo pracowało (oczywiście, bez psucia tego, co już działa), albo mamy trochę przechlapane.

Jak ze sprzątaniem - im dłużej to zaniedbujemy, tym więcej i trudniej. A jak nie sprzątamy, mamy coraz mniej miejsca i coraz trudniej się połapać.

Możemy oczywiście niczego nie robić…

Rysunek: Dyskusja o kawie. Programistka informuje nowo zatrudnionego programistę, że do zdobycia kawy trzeba złożyć podanie. Zaszłości historyczne. Jej jest głupio z tego powodu, on jest wstrząśnięty i zmieszany.

Lepiej jednak nauczyć się refaktoryzować, nie sądzicie? ;-).

3.3. To co to JEST refaktoryzacja?

Gdybym miał spróbować złożyć definicję refaktoryzacji, powiedziałbym coś takiego:

“Refaktoryzacja to przebudowa aplikacji, która ma za zadanie zmienić strukturę kodu pod kątem jasno określonych celów. Przy okazji, nie mogą zmienić się żadne działania tej aplikacji.”

Dlaczego tak:

Z perspektywy powyższej pseudo-definicji, optymalizacja (przebudowa kodu tak, by program działał szybciej lub wydajniej) jest formą refaktoryzacji (refaktoryzacja pod kątem prędkości / wydajności).

W klasycznej definicji jest inaczej, aczkolwiek nie rozumiem czemu ( każda zmiana kodu wpływa na prędkość działania kodu).

W ramach tego artykułu będę trzymał się własnej pseudo-definicji. U mnie działa ;-).

4. Pobieżnie - jak refaktoryzować?

4.1. Pobieżnie i szybko

Wiemy już “czemu”, czas dojść do “jak”. Z mojego punktu widzenia najważniejsze jest zapamiętanie kilku prostych zasad:

  1. Spojrzenie systemowe
  2. Kontekst -> problem -> ideał -> (strategia <-> weryfikacja strategii) -> (działanie -> weryfikacja wyniku)
  3. Iteracje, ewolucja a nie rewolucja
  4. Ustawienie warunków sukcesu

Koniec. To powinno wystarczyć. A teraz pozwólcie, że wyjaśnię dlaczego to wystarczy.

4.2. Spojrzenie systemowe

4.2.1. Co to jest system?

O tym powinienem napisać osobny artykuł. Jednak, w największym skrócie, “System to zbiór elementów i zachodzących między nimi relacji” (definicja cybernetyczna, za wikipedią).

Powyższe znaczy, że na system składa się:

Brzmi to dumnie i niezrozumiale, więc pokażę to na przykładzie:

Zamyślona dziewczyna w kasku patrzy na pojazd roweropodobny

Mamy zatem dziewczynę patrzącą na pojazd roweropodobny, ale napędzany paliwem. Powiedzmy, że chcemy usprawnić taki pojazd pod kątem prędkości - dostaliśmy polecenie usprawnienie silnika.

Innymi słowy, by pojazd jako SYSTEM działał poprawnie, musimy zapewnić to, że:

I to właśnie jest spojrzenie systemowe. Wszystko wpływa na wszystko i każdy ruch ma tysiące efektów ubocznych.

4.2.2. Przykład spojrzenia systemowego przy refaktoryzacji

Podczas pracy nad naszą aplikacją spotkałem się z poniższym problemem:

Diagram przedstawiający pięć bloczków połączonych w łańcuch. Dane tekstowe -> Aplikacja Command "Refresh RD" -> Baza danych grafowa -> Aplikacja Command "Generate Profiles" -> Renderer Zapisywacz -> Dane Tekstowe. Jednocześnie jest połączenie człowieka do Danych Tekstowych (z prawem zapisu i odczytu) i człowieka z Bazą danych grafową (z prawem odczytu).

Gdy nasza aplikacja dochodziła do etapu “Generate Profiles”, rzucany był wyjątek. Po zbadaniu sprawy okazało się, że w naszej bazie grafowej część danych potrzebnych do zbudowania profili było nullami. Generacja profili nie była zabezpieczona przed nullami i stąd ten wyjątek.

Proste do naprawienia, prawda? Wystarczy ufortyfikować “Generate Profiles” i zapewnić, że jeśli pobierze nulla z bazy danych to… właśnie, co?

Nulle były wyrzucane na dość istotnym polu “uid”, czyli “unikalny identyfikator”. Nie było możliwości zbudowania prawidłowego profilu, bo nie dało się mu przypisać unikalnego identyfikatora (to takie coś w stylu PESELu). Uid powstaje przez połączenie “numeru” oraz “nazwiska” danej postaci (upraszczając).

Przeanalizujmy możliwe rozwiązania:

Naprawienie “Generate Profiles”:

Zmiana “Refresh DB”:

Oczywistym rozwiązaniem była w tym wypadku zmiana “Refresh DB” - upewnienie się, że nigdy do naszej bazy grafowej nie trafią dane mające “uid” jako nulla. Problem nie leżał w CZĘŚCI (Generate Profiles) a w CAŁOŚCI (Refresh DB podawał nulle do bazy). W CZĘŚCI jedynie ów problem się objawiał.

Problem rozwiązany, programista idzie na kawę.

Czy aby na pewno?

Stara, dobra zasada głosi “nigdy nie ufaj użytkownikowi”. Nasz użytkownik ma dostęp do dwóch punktów systemu - do danych tekstowych i do bazy grafowej. Przeanalizujmy OTOCZENIE.

Mam nadzieję, że ten prosty przykład pokaże Wam, dlaczego podejście systemowe jest potrzebne przy refaktoryzacji. Bez tego typu analizy refaktoryzacja bywa całkiem zabawna ;-).

4.3. Od problemu do weryfikacji wyniku.

Pętla:

Kontekst -> problem -> ideał -> (strategia <-> weryfikacja strategii) -> (działanie -> weryfikacja wyniku)

jest kolejną formą “od problemu do rozwiązania”, “top-down” i innych konceptów tego typu:

Różnica polega na tym, że refaktoryzacja wymaga spojrzenia systemowego (poprzedni punkt), więc:

Z uwagi na Otoczenie, nie możemy rozpatrywać samego Problemu w izolacji. Musimy spojrzeć na nasz Problem w kontekście całości systemu, w którym się poruszamy (pamiętacie przykład z nullami w bazie grafowej?).

Z uwagi na to, że dotykamy Całości a nie tylko Części, nie możemy rozpatrywać Problemu bez spojrzenia na to, co istnieje dzisiaj. Refaktoryzacja to jest przebudowa czegoś, co już istnieje - musimy więc rozumieć co dzisiaj istnieje. Stąd “Stan Idealny” - czego oczekujemy?

Z uwagi na to, że mamy “stan aktualny” i “stan idealny”, musimy móc wyprowadzić jakąś formę przejścia “od tego co mamy dziś do tego co chcemy mieć”. To jest wybór strategii dojścia do stanu idealnego. Już na tym etapie można na bazie kryteriów sukcesu wybrać, które podejścia są “lepsze” a które “gorsze”.

No i wszystkie te plany nie są wiele warte, póki faktycznie nie spróbujemy tego zrealizować. Pozostaje potem tylko udowodnić, że to co zrobiliśmy faktycznie pomogło.

Brzmi prosto, nie? ;-).

4.4. Podejście iteracyjne i ewolucyjne

W idealnym świecie moglibyśmy chwilę podumać i nagle - spadłoby na nas oświecenie. Wiem już, jak przeprowadzić tą refaktoryzację! Natchniony, siadam do klawiatury i piszę! Wszystkie testy - zielone! Wszystko działa! To było TAKIE proste!

…tyle, że to się nie zdarza.

Nawet najlepszy plan prędzej czy później musi być zweryfikowany. O czymś nie pomyśleliśmy. Czegoś nie sprawdziliśmy. Czegoś nie wiemy.

Jest OK.

Wszelkie próby refaktoryzacji powinny być podzielone na małe kroki, gdzie:

Najważniejsze parametry to te trzy pierwsze; czasem nie ma możliwości zbudowania kroku, który da się zamknąć w jednym dniu (acz to sprawia, że ryzyko refaktoryzacji jest naprawdę wysokie).

“Big Bang Refactoring” (refaktoryzacja typu “Wielki Wybuch” - z niczego powstaje rozwiązanie) jest bardzo rzadkim rozwiązaniem i najczęściej tak średnio działa. Jeżeli nie ma innej możliwości i trzeba zrobić to “jednym ruchem”, nie znamy domeny i ogólnie mamy przechlapane, polecam technikę Strangler Application, zaproponowaną przez Martina Fowlera. Dokładniejszy opis wraz z przykładami tutaj. Sprawdziłem; ta technika faktycznie działa.

4.5. Warunki sukcesu

Kluczem do udanej refaktoryzacji jest prawidłowe zdefiniowanie faktycznego problemu z jakim mamy do czynienia, jak i tego, co uznamy za sukces.

Bardzo łatwo jest powiedzieć “refaktoryzacja się udała, bo kod jest bardziej czytelny”. Jeszcze łatwiej jest powiedzieć “czytelny oznacza to, że główny programista powie, że jest bardziej czytelny”. Widziałem już takie przypadki.

Sęk w tym, że ów główny programista był przyzwyczajony do C, aplikacja była pisana w C# i “bardziej czytelny kod” oznaczał usunięcie połowy LINQ i generyków ORAZ przekształcenie kodu w kod imperatywny, drastycznie utrudniając przebudowę aplikacji. Tak więc, jak to mówił klasyk, “operacja się udała, pacjent zmarł”.

Co za niefart.

Trzeba pamiętać o tym, że mamy do czynienia z systemem. Każda zmiana kaskaduje na cały system - na CZĘŚĆ, CAŁOŚĆ i nasz system nadal musi być w stanie działać w określonym OTOCZENIU. To właśnie powoduje, że poważne refaktoryzacje są dość niebezpieczne; wydawałoby się, że jest lepiej - a jednak pogorszyliśmy sytuację.

Kryteria sukcesu muszą uwzględniać interesy wszystkich interesariuszy i spojrzenie na całość systemu. W innym wypadku będzie ciężko.

4.6. Ostatnie słowo przed przykładem

Sporo tego. Jednak jak długo metodycznie podchodzicie do problemu i pamiętacie o każdym kroku, powinno się bez większego problemu udać.

Rysunek: Od macki ośmiornicy odskakuje kobieta z bronią, ostrzeliwując się. Macka wije się, wyraźnie przegrywa.

5. Przykład z omówieniem

5.1. Opowieść o problemie

W styczniu 2018 roku poprosiłem swoją kochaną Żonę, by dopisała mi do pisanej przez nas aplikacji (rdbutler) parsowanie Planów dla Profili i Frakcji. Ku mojemu ogromnemu zdziwieniu, usłyszałem, że sam mogę sobie to pisać. Ona ma dość pracy z wyrażeniami regularnymi - a już zwłaszcza moimi ;-).

Gdy w 2016 roku jeszcze uczyłem się wyrażeń regularnych (czasem zwanych regexami) nigdy nie zakładałem, że będę miał aż tyle parsowania tekstu w tej aplikacji. To sprawiło, że aplikacja nie była przystosowana do wielu różnorodnych regexów.

Dla wyjaśnienia co to jest wyrażenie regularne: wyobraźcie sobie, że mamy następujący tekst:

# streszczenie:
summary data
## Different subheader
Text
# Different Header

Uruchamiam funkcję o nazwie “ExtractSummarySection” podając powyższy tekst. Dostaję w odpowiedzi:

summary data

A wszystko dzięki niewielkiemu dziwnemu ciągowi znaków - wyrażeniu regularnemu - w środku owej funkcji:

^#\s[Ss]treszczenie[:|\s]*$(.+?)(\Z|^#{1,2}\s\w+)

który oznacza mniej więcej coś takiego:

I wyciągnij wszystko co tam ważne było. Jak spojrzycie na tekst wyżej, jedynym takim tekstem było “summary data”.

(Dla zainteresowanych, wizualizacja tego wyrażenia regularnego pod linkiem poniżej; trzeba skopiować i wkleić ręcznie: https://regexper.com/#%5E%23%5Cs%5BSs%5Dtreszczenie%5B%3A%7C%5Cs%5D*%24(.%2B%3F)(%5CZ%7C%5E%23%7B1%2C2%7D%5Cs%5Cw%2B)

)

Wyrażenie regularne to trochę jakby “język dopasowywania tekstu do wzoru” (przepraszam za uproszczenie).

Sęk w tym, że w naszej aplikacji takie coś trzeba było pisać z palca za każdym razem. I moja Żona zażądała, bym to zmienił, albo nie będzie dotykała parserów.

Zrobiłem to. Ostateczna refaktoryzacja dotknęła około 600-800 linijek kodu na przestrzeni 25 klas; w tym artykule skupimy się na pojedynczym wycinku zgodnie z naszą procedurą działania.

Jak zatem można to przebudować?

5.2. Kontekst oraz Problem

Aplikacja o której mówię pracuje z ogromną ilością plików tekstowych (zdecydowanie ponad 1000), dodawanych ręcznie przez ludzi. Ekstrakcja przy użyciu wyrażeń regularnych dotyka kilkunastu typów nagłówków. Realizowane jest to przez wyrażenia regularne takie jak:

^#\s[Ll]okalizacje[:|\s]*$(.+?)(\Z|^#\s\w+)
^#\s[Ss]treszczenie[:|\s]*$(.+?)(\Z|^#{1,2}\s\w+)
^##\s[Kk]ontynuacja:*\s*(.+)#{1,2}\s
###\s[Kk]ampanijna\s*(.+?)#{1,3}\s

Uproszczony problem w formie kodu C# tutaj

Jak zapewne widzicie, powyższe rozwiązanie posiada bardzo duży poziom duplikacji kodu. Co gorsza, gdybyśmy chcieli dodać nowy typ nagłówka, musimy napisać nowe wyrażenie regularne i naprawdę dużo kopiować i wklejać. Niewygodne; nic dziwnego, że moja Żona zaprotestowała. Miała rację - to trzeba było naprawić. W innym wypadku rozszerzalność parsera tekstu byłaby zagrożona.

Nazwijmy Problem:

Dobrze, mniej więcej to mamy. Czas przejść więc do kolejnego kroku.

5.3. Stan Idealny

Nie jest to szczególnie trudne w tym wypadku:

To powyżej wygląda dobrze. Zauważcie - wszystkie te rzeczy są dość mierzalne i pozwalają nam wartościować rozwiązania na lepsze i gorsze.

5.4. Strategia i Weryfikacja: próba 1

Stawiam hipotezę, że najlepszą strategią rozwiązania tego problemu będzie zrobienie funkcji (lub metod; jak wolicie). Niedoświadczony programista lubi pracować z funkcjami, acz niekoniecznie z wyrażeniami regularnymi. W tej chwili przykładowe funkcje ekstrakcji wyglądają tak:

string locationText = ExtractLocationSection(inputText);
string summaryText = ExtractSummarySection(inputText);

W moim świecie idealnym oczekuję, że to będzie wyglądało jakoś tak:

string locationText = ExtractSection(inputText, "#\s[Ll]okalizacje[:|\s]*$(.+?)(\Z|^#\s\w+)");
string summaryText = ExtractSection(inputText, "#\s[Ss]treszczenie[:|\s]*$(.+?)(\Z|^#{1,2}\s\w+)");

Hm… jakkolwiek pozbyłem się problemu z duplikacją kodu, muszę na poziomie funkcji podać jako parametr konkretne wyrażenie regularne. Moja Żona odmówiła pracy nie z funkcjami a właśnie z owymi wyrażeniami. To sprawia, że to rozwiązanie nie rozwiązuje “głównego” problemu. Nie jest ono głupie, ale nie jest wystarczające.

5.5. Strategia i Weryfikacja: próba 2

Stawiam inną hipotezę - najlepszą strategią rozwiązania tego problemu będzie parametryzacja wyrażeń regularnych. Niech istnieją funkcje - tak, jak zaproponowałem w poprzednim rozwiązaniu. Ale to, co tu ważniejsze - niech te wyrażenia regularne będą “składane” z komponentów.

Ale z jakich komponentów?

Jak przyjrzymy się tym wszystkim wyrażeniom regularnym, to są one bardzo podobne. Każda interesująca nas grupa zaczyna się pewną liczbą znaków ‘#’ i kończy się pewną liczbą znaków ‘#’. Każde ma jakiś “tekst”, jakąś nazwę nagłówka. Może da się je jakoś sensownie ujednolicić…

Chciałbym zobaczyć coś takiego:

string locationText = ExtractSection(inputText, "Lokalizacje", starting = 1, blocking = 1);
string summaryText = ExtractSection(inputText, "Streszczenie", starting = 1, blocking = 2);

Gdyby mi się udało, to rozwiązałbym ten problem:

Super. Pierwsze rozwiązanie nie zadziałało, ale drugie tak. Weryfikacja udana, mamy swoją strategię dojścia do stanu idealnego. Na tym etapie nie wiem jeszcze JAK to zrobić, ale wiem już przynajmniej gdzie chcę dojść.

5.6. Działanie i Weryfikacja

5.6.1. Ujednolicenie wyrażeń regularnych

Spójrzmy jeszcze raz na nasze cztery główne wyrażenia regularne:

^#\s[Ll]okalizacje[:|\s]*$(.+?)(\Z|^#\s\w+)
^#\s[Ss]treszczenie[:|\s]*$(.+?)(\Z|^#{1,2}\s\w+)
^##\s[Kk]ontynuacja:*\s*(.+)#{1,2}\s
###\s[Kk]ampanijna\s*(.+?)#{1,3}\s

Niezbyt to czytelne. Wiemy (z domeny), że wszystkie nagłówki zaczynają się od nowej linijki, ale trzy wyrażenia mają znak ‘^’ a jedno nie. Wiemy, że wszystkie powinny ignorować wielkie i małe znaki… to akurat łatwo rozwiązać. Zamiast:

Regex.Matches(text, "[Ll]okalizacje", RegexOptions.Singleline | RegexOptions.Multiline);

można użyć dodatkowej flagi:

Regex.Matches(text, "lokalizacje", RegexOptions.Singleline | RegexOptions.Multiline | RegexOptions.Ignorecase);

Tego typu zmiany pozwolą na uczytelnienie i ujednolicenie wyrażeń regularnych. Dzięki temu łatwiej uda się je nam parametryzować dalej.

5.6.2. Ekstrakcja nazwy nagłówka

Mając starą funkcję i nowe wyrażenie regularne, coś w stylu:

ExtractLocationSection(inputText)
# Lokalizacje[:|\s]*$(.+?)(\Z|^#{1,1} \w+)

Spróbuję wyciągnąć wszystkie nazwy nagłówków poza wyrażenie regularne. Zbudować coś takiego:

ExtractSection(inputText, "Lokalizacje")
# NAZWA_NAGŁÓWKA[:|\s]*$(.+?)(\Z|^#{1,1} \w+)

5.6.3. Ekstrakcja liczby zaczynających ‘#’

Mając starą funkcję i nowe wyrażenie regularne, coś w stylu:

ExtractSection(inputText, "Lokalizacje")
# NAZWA_NAGŁÓWKA[:|\s]*$(.+?)(\Z|^#{1,1} \w+)

Spróbuję wyciągnąć liczbę startowych ‘#’ tak, by działały zarówno:

# streszczenie[:|\s]*$(.+?)#{1,2}\s
## kontynuacja[:|\s]*$(.+?)#{1,2}\s

Jakoś taki wynik:

ExtractSection(inputText, "Lokalizacje", start = 1)
#{1, ILOŚĆ_START} NAZWA_NAGŁÓWKA[:|\s]*$(.+?)(\Z|^#{1,1} \w+)

5.6.4. Ekstrakcja liczby kończących ‘#’

Nie będę się powtarzał, jak powyżej.

5.7. Weryfikacja CAŁOŚCI

Mamy nowe funkcje. Są takie jak Stan Idealny. W teorii, wszystko powinno działać.

Pytanie: czy FAKTYCZNIE dostaliśmy takie korzyści jakich oczekiwaliśmy od przejścia Stan Aktualny -> Stan Idealny ? Czy nasz Stan Idealny rozwiązuje Problem w Kontekście?

Jeżeli tak, to commit i push. Refaktoryzacja zakończona powodzeniem. Jeżeli nie, to git reset –hard. Refaktoryzacja zakończona niepowodzeniem na poziomie koncepcyjnym. Musimy to opracować i zdefiniować problem jeszcze raz.

W naszym wypadku - to zadziałało. Czas dodawania nowego parsowanego bytu spadł z około 30 minut do niecałych 5 minut, nie licząc konieczności dodania testów w wypadku starego rozwiązania (w nowym testy są zbędne; testujemy funkcję techniczną jednorazowo).

W literaturze uzupełniającej znajdziecie linki do ćwiczeń i faktycznej refaktoryzacji, jak jesteście zainteresowani.

6. Podsumowanie

6.1. Podejście metodyczne

Jak - mam nadzieję - pokazałem, podejście metodyczne pozwala na w miarę spokojne podejście do refaktoryzacji. Zacznijmy od tego, że zamiast kłócić się o szczegóły możemy skupić się na tym, co ważne - po co to robimy, co chcemy osiągnąć i jak to zmierzymy. Samo to pozwala na dramatyczną oszczędność czasu.

Refaktoryzacja nigdy nie jest łatwa - warto więc sobie pomóc przy użyciu dowolnej techniki, która działa. Oczywiście, w tym artykule użyłem prostego przykładu - ale im większy i bardziej skomplikowany problem, tym cenniejsze jest metodyczne podejście.

6.2. Sprzeczne cele

Poważnym problemem przy refaktoryzacji jest to, że różne cele refaktoryzacji bardzo często są sprzeczne:

Często zdarza się, że podczas dyskusji o refaktoryzacji dwie osoby mają różne idee tego, DLACZEGO aplikacja jest refaktoryzowana; czym jest ów Stan Idealny. Najlepszym rozwiązaniem w tym wypadku jest usiąść i porozmawiać. Niestety, trzeba zaakceptować smutny fakt - nie ma rozwiązań idealnych. Dążenie do pierwszej korzyści często utrudnia dojście do drugiej korzyści. W takim wypadku ustalamy co ma wyższy priorytet - i działamy.

6.3. Musimy rozumieć domenę i kod

Z uwagi na Kontekst oraz Stan Idealny - musimy zrozumieć domenę, w której działa aplikacja. Refaktoryzując bez zrozumienia co ów program ma robić, możemy się nieźle wkopać z uwagi na Stożek Nieoznaczoności.

Z uwagi na Stan Aktualny, CAŁOŚĆ i OTOCZENIE - musimy rozumieć kod, który w chwili obecnej działa w refaktoryzowanej aplikacji. Tak jak z moim nieszczęsnym przykładem z nullami w bazie grafowej - bez znajomości istniejącego systemu nie byłbym w stanie znaleźć prawidłowego rozwiązania.

Tak naprawdę NIGDY nie refaktoryzujemy kodu. Zawsze dostosowujemy istniejący kod do tego, by lepiej rozwiązywał konkretny problem biznesowy. Dlatego ta domena jest tak ważna. A nie wiedząc, co mamy dziś - nie wiemy jak najmniejszym kosztem to przebudować.

6.4. Nie wszystko da się zrobić

Spotkałem się pewnego dnia z przypadkiem takim, jak poniżej:

Tak, wracamy do problemu Otoczenia. Czasami macie np. dwa dni na refaktoryzację a potrzebowalibyście tydzień. W takich okolicznościach także nie da się zwyciężyć. Czasem najlepszym rozwiązaniem jest stwierdzenie “nie, nie robimy refaktoryzacji”.

6.5. Refaktoryzacja jako problem polityczny

Bardzo często głównym problemem nie jest zmiana kodu a obronienie swojego rozwiązanie w Zespole. Lub sprzedanie swojego rozwiązania Biznesowi.

Jeżeli budujecie w prawidłowy sposób Koncepcję (w tym wypadku: Kontekst i Problem oraz Stan Idealny), powinniście być w stanie znaleźć rzeczowe argumenty w formie przystępnej dla odbiorcy. Nie daje Wam to gwarancji sukcesu, ale zwiększa prawdopodobieństwo.

Nie rozpatrywałem szczególnie w tym artykule refaktoryzacji jako problemu politycznego. Może kiedyś.

6.6. Jak zatem zacząć refaktoryzację?

Ja osobiście wykorzystuję następującą procedurę (omijam elementy związane ze zdobyciem wsparcia politycznego ze strony reszty zespołu i biznesu):

  1. Nazywam Problem w konkretnym Kontekście.
  2. Analizuję całość systemu (Część, Całość, Otoczenie) by określić zakres refaktoryzacji i czy ja jestem w stanie ją przeprowadzić w sensownym czasie.
  3. Sprawdzam z innymi członkami zespołu, czy oni TEŻ mają ten problem - co najbardziej ich boli. Doprecyzowuję Problem.
  4. Przechodzę przez pętlę: Kontekst -> problem -> ideał -> (strategia <-> weryfikacja strategii) -> (działanie -> weryfikacja wyniku). Upewniam się, że nie idę w perfekcję a w naprawienie tego, co jest największym problemem. Dopisuję testy tam, gdzie się da / ma to sens.
  5. Sprawdzam, czy nowa całość jest lepsza niż stara całość. Przeprowadzam wszystkie testy.
  6. Sprawdzam, czy rozwiązuje to problemy innych członków zespołu. Jak nie, upewniam się, że nie utrudniłem innym członkom zespołu pracy lub przyszłych zmian mających im pomóc.

Jeśli nie wiem jak coś przeprowadzić, pytam osobę po prawej stronie / szukam w wyszukiwarce czy Stack Overflow.

Jak długo pamiętacie o celowości i mierzalności każdego kroku, powinno się udać. Pamiętajcie, że refaktoryzacja to trochę eksploracja - trzeba szukać i eksperymentować z rozwiązaniami. Nie zawsze pierwsze rozwiązanie będzie najlepsze. Czasem wpadniecie w ślepą uliczkę i trzeba się wycofać (bo nie zauważyliście czegoś w Otoczeniu czy Całości).

Zupełnie jak sprzątanie ;-).

7. Wykazanie korzyści

7.1. Przypomnienie korzyści

Czy zatem dostaliście to, co Wam obiecałem na początku? Dla przypomnienia, obiecałem Wam to:

Kiedy warto to przeczytać?

  1. Coraz trudniej Wam poruszać się po istniejącym kodzie i chcielibyście go przebudować - ale boicie się czegoś zepsuć.
  2. Macie kod i chcecie go przebudować - ale od czego zacząć? Co zmienić najpierw? Jak to zrobić, by się udało?
  3. Macie kilka potencjalnych wariantów zmiany kodu i chcecie móc porównać, która byłaby “lepsza”. I obronić swój wybór w zespole, by nie wyjść na łosia.

Co ja chcę Wam dać?

  1. Recepturę na refaktoryzację, która - jak na razie - pozwala mi na szybkie i skuteczne refaktoryzowanie (lub podjęcie decyzji o nie rozpoczynaniu refaktoryzacji).
  2. Chcę Wam pokazać, że refaktoryzacja nie jest czymś bardzo trudnym ani szczególnie strasznym. Jest trudna, ale nie aż TAK trudna jak się wydaje.
  3. Chcę to wszystko pokazać na pewnym studium przypadku (case study) - oraz przygotowałem małą solucję, na której możecie sami poćwiczyć.

7.2. Dowody pośrednie

Receptura dostarczona, tak jak i case study. Solucja w materiałach uzupełniających. Przejdźmy do ciekawszego fragmentu:

Jeśli trudno Wam się poruszać po istniejącym kodzie i boicie się go zepsuć - receptura pozwoli podjąć decyzję, czy refaktoryzacja jest wskazana / konieczna czy też nie. Działając zgodnie z ową recepturą minimalizujecie szansę błędnej refaktoryzacji, bo wszystko opieracie na korzyściach i niewielkich ruchach. Oczywiście, jesteście w stanie podjąć błędne decyzje podczas refaktoryzacji, ale wprowadziłem tam tyle kroków weryfikacji, że to nie jest aż tak proste.

Dodajmy do tego myślenie systemowe: część, całość i otoczenie. Jeżeli faktycznie wykorzystujecie te wszystkie techniki, uważam, że nie macie się czego bać. Nie każda refaktoryzacja się udaje, ale zawsze zostaje git reset –hard ;-).

Od czego zacząć i jak w ogóle przeprowadzać refaktoryzację? Od Problemu do Rozwiązania. Nie pokazałem Wam konkretnych technik; osobiście nie znalazłem bardzo użytecznych i uniwersalnych technik. Ja podchodzę do każdej refaktoryzacji indywidualnie, acz zgodnie z zaproponowaną procedurą. Jeśli chcecie poczytać o pojedynczych transformacjach, polecam Wam katalog Martina Fowlera.

Jak porównać różne strategie refaktoryzacji i jak obronić swoją koncepcję? Przez posiadanie twardych, mierzalnych celów, faktów i argumentów. Cały fragment tekstu odnośnie definiowania stanu idealnego i korzyści z owej transformacji moim zdaniem spełnia tą obietnicę.

Jeżeli nie uważacie tych dowodów za wystarczające, zawiodłem. Nadal jednak pozostaję dość pewny siebie ;-).

8. Literatura uzupełniająca

Tym razem jedynie kod. Ćwiczenie w C# i przykład rzeczywisty w Pythonie.

  1. Solucja w C#, przy użyciu której możecie przeprowadzić prostszą wersję tej refaktoryzacji
    1. Potrzebujecie Visual Studio (może być Community Edition); solucja jest pisana w C# i plik *.sln znajduje się w folderze trainings_code_snippets\RefactoringWithRegex_1801\RefactoringWithRegex_1801
    2. Po ściągnięciu *.zip lub git pull, mstest powinien sam znaleźć 6 testów. Wszystkie testy powinny być zielone.
    3. Jeśli coś nie działa, ważne są tylko dwa pliki: TestExtractStoryData do testów i ExtractStoryDataFromText do kodu. Możecie zrobić nową solucję i dodać projekt zwykły i testowy jeśli trzeba. Do zwykłego dodajcie ExtractStoryDataFromText, do testowego - TestExtractStoryData. Zadziała.
    4. Ta solucja nie robi niczego. Przeprowadźcie refaktoryzację zgodnie z instrukcją w TestExtractStoryData. Niech testy są zielone ;-).
  2. Faktyczny commit po rebase opisywanej refaktoryzacji (uwaga: większej niż przykład)
  3. Branch, na którym jest cała przeprowadzona refaktoryzacja, łącznie z błędami: od 10 stycznia do końca

9. Metadane artykułu

Szkic, dla chętnych zobaczenia jak “robi się kiełbasę”, tutaj