Czy refaktoring to słowo tabu?

Refactoring – romantyczna wizja a rzeczywistość

Calendar, Date, Time, Month, Week, Planning, Paper

Poniedziałek

Zaparzasz poranną filiżankę kawy. Kenijskie ziarno nadaje jej niezwykle pobudzający, ale i rześki aromat. Mijając okno, rzucasz krótkie spojrzenie na zewnątrz. Cóż za cudowny, słoneczny poranek! Jakieś wewnętrzne ciepło mówi ci, że to będzie dobry dzień. Powoli zbliżasz się do biurka, zastanawiasz się co ciekawego dziś cię czeka, jakie wyzwania rzuci ci los. Po kilku minutach spędzonych na poszukiwaniu odpowiedniego zadania trafiasz na coś co nie wygląda tak źle. Nie jest to coś super trudnego, nie jest to coś banalnego. Idealne zadanie na poniedziałkowy poranek. Otwierasz kod projektu i już od początku czujesz to okropne uczucie obrzydzenia. Spod dziesiątek linijek kodu wydobywa się odór zastanego kodu. Tak, to śmierdzi kod legacy.

Wtorek

Wczorajszy dzień nie był zbyt udany. Może i wymagana zmiana nie wydaje się zbyt trudna, ale nasuwa się pytanie w którym miejscu należy jej dokonać. Pierwsze próby nie przynoszą spodziewanego efektu. Pytasz ludzi z zespołu o wskazówki lecz okazuje się, że wszyscy, którzy mieli do czynienia z tą funkcjonalnością już dawno odeszli. W myślach zastanawiasz się czy odeszli czy też uciekli w popłochu? Kolejny dzień samotnej walki przed tobą.

Środa

Udało się okiełznać potwora. W myślach co prawda już trzy razy składasz rezygnację i rzucasz papierami, ale być może obejdzie się tym razem bez tego? Kilka if-ów umiejętnie wstrzykniętych w odpowiednie miejsca wydaje się robić robotę. Może nadal nie masz do końca pojęcia co robisz, ale wyniki są dość dobre. Postanawiasz napisać testy. Unit testy to jest to! Po pierwsze pozwolą na lepsze zrozumienie funkcjonalności, a po drugi zapewnią pokrycie warunków brzegowych. Zderzenie z rzeczywistością jest jednak dość brutalne. Autor kodu nie był fanem testów jednostkowych. Albo inaczej, może i był fanem, ale akurat tutaj nie przewidział możliwości wstrzykiwania zależności. No cóż, następnym razem na pewno się uda. Tymczasem postanawiasz uporządkować kod i stworzyć Pull Request. Od razu znajdujesz kilka pomysłów jak niskim kosztem ulepszyć kod w okolicy twoich zmian lecz czy warto ponosić ryzyko bez testów jednostkowych? Racja, lepiej się nie wychylać, jest stanowczo zbyt wiele do stracenia. Wygrywa opcja „Pull request i fajrant”.

Czwartek

Na spotkaniu zespołu podnosisz temat czasu potrzebnego na refaktoring funkcjonalności z którą właśnie się ścierasz. Jak mityczni „oni” mogli stworzyć takie gówno? „To trzeba zaorać i przepisać”. Twój zespół potakująco kiwa głową, patrząc z boku ma się wrażenie, że jesteście na koncercie rapowym. Potrzebujesz tygodnia, dwóch, góra trzech i „będzie Pan Product Owner zadowolony”. Pan Product Owner przynajmniej na razie nie podziela jednak twojego entuzjazmu. Jak usłyszał o tym, że potrzebujesz nawet 3 tygodni na to by spędzić czas na czymś co nie przyniesie żadnego namacalnego efektu to o mało nie spadł z krzesła. Na szczęście dzięki poparciu innych członków zespołu udało się wynegocjować częściowy refaktoring. Co prawda ograniczony do tygodnia, i nie teraz tylko za sprint lub dwa. Uznajesz to za sukces. Szybko jednak zostajesz zawalony stertą innych zobowiązań, oczywiście wszystko na ASAP. Na szczęście Pan Product Owner zapewnia, że to tylko tymczasowe i że jak się już odkopiecie z ASAPów to będzie chwila na odetchnięcie i refaktoring. To będzie twoja chwila by pisać idealny, skalowalny, czysty kod.

Piątek

Zaczyna do ciebie docierać, że historia zaczyna zataczać koło. Że kiedyś już Pan Product Owner zbywał twoje sugestie o refaktoring. Piątek, piąteczek, piątunio to idealna okazja do refleksji. Tematem refleksji jest „co poszło nie tak?”, „czy refaktoring to słowo tabu?” oraz „jak mieć/zdobyć czas na refaktoring?”

No właśnie, odpowiedzmy sobie na po kolei na pytania.

Photo by Andrea Piacquadio on Pexels.com

Co poszło nie tak?

Odsuwanie niezbędnych zmian w kodzie, zaniedbania doprowadzające go do złego stanu i wrażenia „kodu legacy” to choroba która zaczyna się niewinnie. Tutaj magiczna liczba, tutaj dodatkowy „if”, tutaj przekopiowana metoda. Samo w sobie nic z tego nie jest problematyczne, problematyczne jest to, że każde takie małe uchybienie bez stałej pracy o to by poprawiać jakość kodu, prowadzi w tempie błyskawicznym to degradacji jakości kodu. Składają się na to 3 główne czynniki:

  • tzw. efekt zbitych okien, czyli łatwiej jest wprowadzić „brzydką” zmianę jeżeli kod już zawiera inny „brzydki” kod, w końcu to nie my zaczęliśmy tą karuzele degradacji, prawda?
  • złożoność kodu zależy od jakości ale też i od objętości kodu. Dodając nowe linijki łatwo jest skomplikować kod. Reguła kciuka w tym wypadku mówi nam by w miarę możliwości dodawać tego kodu jak najmniej. Każda nowa linijka to potencjalny bug, to kolejna linijka która musi być przeczytana przez osobę czytającą kod i starającą się go zrozumieć
  • im trudniejszy do zrozumienia kod, tym mniejsza szansa że osoba robiąca zmiany po nas będzie miała odwagę by go poprawić

Czy refaktoring to słowo tabu?

Refaktoring to może być dla naszego rozmówcy słowo-trigger. Warto przemyśleć czy to słowo nie ma negatywnych skojarzeń dla osoby z którą dyskutujemy. Ja staram się nie być uprzedzony, ale trudno nie oprzeć się wrażeniu, że o refaktoringu mówią często osoby, które niekoniecznie mają kompetencje by go umiejętnie przeprowadzić. Wg mnie warto tutaj po prostu używać pełnej definicji refaktoringu i mówić o tym jaki efekt chcemy uzysać, czyli np. „chcemy zmienić istniejący kod w taki sposób aby zredukować jego złożoność oraz zmieniejszyć koszt wprowadzania nowych zmian, bez zmiany istniejącej funkcjonalności”.

Jak mieć/zdobyć czas na refaktoring?

To zdecydowanie najważniejsze pytanie. Odpowiedź dla niektórych może wydawać się też zaskakująca. Może rzeczywiście w naszym wypadku to Product Owner ma rację ze swoją wstrzemięźliwością co do refaktoringu? Najłatwiej i najefektywniej, zmiany refaktoryzujące, wprowadzać jako część standardowego procesu implementacji nowych funkcjonalności czy też poprawiania błędów. Wprowadzanie osobnego czasu tylko i wyłącznie na refaktoring to ostateczność. Argumenty? proszę bardzo:

  • redukujemy ryzyko związane z wprowadzaniem wielkich zmian. Znacznie łatwiej coś popsuć jeżeli zmieniasz 500 linijek niż gdy zmieniasz 15 lub 50, prawda?
  • wprowadzamy zmiany wokół funkcjonalności, która w danym momencie jest przez nas analizowana. Oznacza to, że jest to kod który dokładnie zrozumiany i nasze zmiany będą tam najbardziej przemyślane i sensowne
  • nie chcemy być postawieni pod ścianą. Zdarzają się sytuacje w postaci krytycznych błędów, kiedy musimy poprawić coś jak najszybciej. Nie chcielibyśmy by taka sytuacja stała się w chwili tuż przed planowanym refaktoringiem kiedy mamy do czynienia z kumulacją niedoskonałości kodu nagromadzoną przez dłuższy czas
  • pokazujemy naszą incicjatywę, jako programista odpowiadamy za jakość tego co wytwarzamy. Jasne, że fajnie jest mieć wymówki, że „mogłem to zrobić lepiej ale mi nie pozwolili”, ale koniec końców to my będziemy musieli się męczyć z tym kodem.
  • kod legacy, który przebył już kilka cykli naprawiania w nim błędów, może zwyczajnie posiadać w sobie więcej wiedzy niż jesteś w stanie zrozumieć. Nawet jeżeli kod jest brzydki to często nie ma sensu go ruszać, zwłaszcza jeżeli działa dobrze i w najbliższej przyszłości nie planujemy wprowadzać tam nowej funkcjonalności
  • unikamy niepotrzebnych rozmów nt. tego czy potrzebujemy refaktoringu. Jeżeli jesteś pewien/pewna, że dany fragment kodu wymaga refaktoringu, to go robisz. Zakładając ciągłą troskę o kod, ta decyzja jest szybka (oszczędzamy czas potrzebny na osądy), zmiana jest skuteczna i mała (ograniczone ryzyko że coś pójdzie nie tak). Na pewnym poziomie doświadczenia jest to krytyczna umiejętność profesjonalnego programisty by zrobić dokonać zmiany popychającej kierunek projektu w dobrym kierunku, kawałek po kawałku.
https://icons8.com/

Podsumowanie

Refaktoring to na prawdę romantyczna wizja w której siadasz do kodu i zamieniasz coś czego trzeba się wstydzić, na coś co spełnia twoje standardy czystego kodu. Nie trzeba się temu dziwić w końcu w każdym z nas jest przynajmniej trochę takiego wewnętrznego artysty, który chce stworzyć coś swojego i nadać temu pożądany kształt. Po spotkaniu z rzeczywistością jednak często okazuje się że może nie być to najefektywniejszy sposób pracy. O wiele skuteczniejszą metodą niż jednorazowe podrywy refaktoryzujące może okazać się zwykła codzienna troska o jakość dostarczanych rozwiązań. Kto powiedział że nie możemy robić mini-refaktoringu codziennie?

Pobij Netflixa

Skoro już cię przekonałem dlaczego warto używać chmury oraz dlaczego to może być AWS następnym logicznym krokiem jest przedstawienie jakiegoś przykładowego rozwiązania bazującego na chmurze AWS. Dziś na warsztat weźmiemy rozwiązanie streamingowe, które pozwoli ci pobić Netflixa 😉

woman lying on bed while eating puff corn

 

Domena

Dla uproszczenia dziedziny problemu, przyjmijmy, że chcielibyśmy mniej więcej odzwierciedlić podstawowe funkcjonalności Netflixa (oczywiście zrobimy to lepiej!), czyli dostarczyć rozwiązanie pozwalające na oglądanie wideo na życzenie oraz platformę, która owe wideo serwuje. 
Użytkownik w dzisiejszych czasach jest bardzo wybredny. Mając do czynienia z rozwiązaniami typu HBO Go czy Netfix musimy dostarczyć serwis, która będzie:

Niezawodny

Powiedzmy sobie szczerze ile razy nie działał ci Netflix z ich winy?

Szybki

Jeżeli użytkownik będzie musiał czekać na buforowanie wideo zbyt długo to pójdzie tam gdzie nie musi czekać. To nic osobistego.

Relatywnie tani

Oczywiście możemy kupić najlepsze dostępne serwery, bazy danych itd, ale kiedy przyjdzie nam obsługiwać ruch tysięcy użytkowników to rachunki szybko poszybują na poziomy takie, że szybko zrezygnujemy z naszych ambitnych planów.

Skalowalny

Użytkownika mało obchodzi to, że akurat dziś ruch na naszych serwerach jest 2-3 czy 10 razy większy niż zwykle. To się zdarza, naszym zadaniem jest na to się przygotować.

Ogólna architektura

Pomijając zbędne szczegóły architektura takiego przekrojowego rozwiązania może wyglądać następująco:

Omówmy sobie po kolei wszystkie komponenty

Warstwa API Gateway

Odpowiada za odbiór żądań ze strony urządzeń. Jak mówi nazwa to interfejs do naszego systemu. Wszelkiego rodzaju przeglądarki, smartfony czy też TV będą komunikować się właśnie przez tą warstwę. Tutaj załatwimy sprawę cache-owania żądań, kontroli dostępu i przekierujemy żądania do warstwy serwerów aplikacyjnych.

Warstwa serwerów aplikacyjnych

Tutaj umieścimy obliczenia i całą logikę odpowiedzialną za obsługę żądań. Główny kod odpowiedzialny za synchroniczne przetwarzanie zapytań tak by dostarczyć użytkownikowi odpowiednie „ekrany” i ich zawartość. Od strony infrastruktury, logika odpalana jest oczywiście na serwerach czy też ich klastrach. Częściowo mogą być to lambdy przy podejściu bardziej serverless. Zwłaszcza na początku gdy dopiero rozpatrujemy różne możliwości jest to kuszące rozwiązanie. Mamy tu także wykorzystanie kolejek zadań, cache, baz danych, oraz oczywiście innych mikroserwisów czy to naszych, czy też zewnętrznych rozwiązań – SAAS.

Warstwa zadań asynchronicznych.

Wszystkie zapytania, które nie należą do naszej aplikacji bezpośrednio, ale też muszą się zadziać. Jeżeli chcesz zrobić system chociaż w niewielkim stopniu podobny do Netflixa, czy też go pobić, trzeba sobie uświadomić sobie ogrom pracy jaka jest wykonywana „pod spodem”, czyli po prostu na backendzie.

Co musimy przygotować dla użytkowników?

  • Wideo w odpowiednim formacie. Każde urządzenia to inne wymagania odnośnie formatu czy też rozmiaru. Nie mówiąc o tym, że musimy serwować różne pliki w zależności od szybkości łącza użytkownika.
  • Rekomendacje. Kiedyś to może byłby fajny dodatek, teraz to wymóg by prezentować rekomendacje dla użytkowników, jeżeli trafimy w gusta, +5 punktów dla nas!
  • Newslettery. Byle nie za dużo spamu.
  • Bieżąca obsługa. Reset hasła, usunięcie konta itd

Przestrzeń chmurowa (cloud storage) i CDN

Forma przechowywania danych wideo to jedno z bardziej krytycznych zadań. Trzeba pamiętać o tym, że danych tych będzie bardzo dużo (pamiętasz, że mamy wiele kopii tych samych filmów?) Potencjalne koszty będą tutaj silnie skorelowane właśnie z rozmiarem danych. Z drugiej strony nie możemy sobie pozwolić by dostęp do nich był wolny, inaczej użytkownik szybko wybierze konkurencję. Innymi słowy potrzebujemy hybrydy przestrzeni chmurowej i CDNa. Przestrzeń chmurowa to po prostu serwer plików, a CDN, czyli Content Delivery Network lix-a to rozproszona sieć buforująca często używane zasoby poprzez ich kopiowanie na odpowiednie serwery będące blisko użytkownika końcowego. Będąc w Polsce, możemy szybciej streamować film, jeżeli znajduje się od na serwerze we Wrocławiu, niż gdy znajduje się w Los Angeles, prawda?

Uczenie maszynowe

Tutaj umieszczamy cokolwiek, byle by móc powiedzieć, że „robimy machine learning” i ściągnąć zainteresowanie inwestorów i klientów 😛 A tak na prawdę to skoro chcemy serwować naszym użytkownikom wspomniane wcześniej rekomendacje to dobrze jest móc to robić w oparciu nie o sztywne reguły, a właśnie modele wykorzystujące uczenie maszynowe. Dzięki temu będziemy mogli analizować podobieństwo między różnymi użytkownikami oraz podobieństwo miedzy filmami i na tej podstawie decydować czym dana osoba może być zainteresowana. To wszystko powinno przełożyć na trafniejsze rekomendacje i wzrost zainteresowania naszym rozwiązaniem.

Hurtownia danych

Raporty, analiza danych, musisz trzymać rękę na pulsie. Hurtownia to miejsce gdzie dane te są zorganizowane w przystępnej formie.

AWS

Przestwawię teraz przykładową implementację powyższych komponentów w AWSie. Od razu zaznaczam, że pewne kwestie zostały pominięte dla prostoty diagramu, przykładowo logowanie czy monitoring. Jeżeli chodzi o dobór konkretnych rozwiązań to też mamy do wyboru wielorakie rozwiązania na rynku. Nic nie stoi na przeszkodzie by część z nich było poza AWS, jednak formuła tego artykułu jest taka, że tylko do tej chmury zawęzimy nasz wybór. Dobra, koniec gadania:

Wersja w lepszej rozdzielczości: tutaj

Omówię teraz po krótce kluczowe decyzje.

Warstwa API Gateway

Jak wcześnieś zauważyliśmy, chcemy odpowiednio zopytmalizować wrażenia użytkowników różnych platform. Stąd też podejście typu backend for frontend, czyli warstwa API Gateway posiada wyspecjalizowane dedykowane AWS API Gatewaye w zależności od platformy klienckiej.

Warstwa serwerów aplikacyjnych

Sercem rozwiązania są klastry ECS, czyli Elastic Cloud Server. Tak, konteneryzacja zawitała i do nas! Żądania przychodzą od strony load balancer-a. Do tego SQS do kolejkowania zadań. Jako baza danych może posłużyć nam DynamoDB o ile dobrze czujemy się w środowisku baz NoSQL. Zaletą DynamoDB jest łatwa skalowalność, która jest dla nas niemalże przeźroczysta. Alternatywnie możemy posłużyć się RDS Aurorą.

 Warstwa zadań asynchronicznych

Preview(opens in a new tab)

Od strony infrastruktury wygląda to podobnie jak w przypadku warstwy serwerów aplikacyjnych. Różnicą będzie dużo większe wykorzystanie kolejek SQS, potencjalnie także topic-ów SNS to komunikacji zorientowanej na wiadomości, której będzie tutaj dużo.

Jedno z kluczowych zadań do wykonania w tej warstwie to transcoding wideo, który może wyglądać następująco:

Na wejściu mamy nieskompresowany plik wideo, wrzucony być może przez samego autora filmu. Na wyjściu mamy plik dostosowany przez nas process do odpowiedniego urządzenia końcowego.

Przestrzeń chmurowa (cloud storage) i CDN

Troche już w punkcie poprzednim zostało opowiedziane przy okazji omawiania transcodingu. S3+CloudFront załatwiają sprawę. 

Alternatywne rozwiązania – AWS Media Services

Pewną alternatywą dla nas mogą być AWS Media Services

Z naszej perspektywy użyteczne mogło by być zwłaszcza AWS Elemental MediaConvert, które zapewnia właśnie usługę transcodingu. Dużym plusem jest to, że płacimy tutaj za użycie, czyli tak jak w infrastrukturze powyżej, gdzie brudną robotę wykonują za nas EC2-ki. Myślę, że AWS Elemental MediaConvert możemy rozważyć jako element naszego MVP a docelowo możemy kierować się jednak ku ECS/EC2 gdyż pozwoli nam to na większą elastyczność rozwiązania i zoptymalizowanie kosztów. Zasadniczo reguła kciuka mówi nam, że im bardziej wysokopoziomowe rozwiązanie, tak jak w przypadku AWS Elemental MediaConvert, cena rośnie w sposób adekwatny.

Podsumowanie i dalsze kroki

Tak jak pisałem wyżej. Poza zaprezentowanymi rozwiązaniami pozostaje do doprecyzowania zostaje wiele kwestii pobocznych. Przykładowo do logowania możemy użyć serwisu Cloud Watch. To właśnie Cloud Watcha użyjemy także do monitorowania naszych zasobów oraz podnoszenia odpowiednich alarmów gdy przykładowo będziemy mieli do czynienia z jakimś problemem. Nie chcemy przecież by tak błache rzeczy jak np. brak pamięci na serwerze, błęd w logice serwisu czy tego typu sprawy zaprzepaściły nasz cały wysiłek i popsuły wizerunek pogromcy Netflixa.

Do przesyłu danych może przydać się Kinesis streams. Autentykacja użytkowników może przebiegać w Cognito. Potrzebujemy też dobrego cache-a, Amazon ElastiCache, poratuje. A no i nie obędzie się bez kontroli tego co który serwis może robić a czego nie może zrobić. Tu do gry wkracza IAM. Na tym chyba poprzestanę 🙂


Z pewnością tego typu przekrojowa architektura może nieco odstraszać, aczkolwiek pozwala sobie jednak zdać sprawę jak skomplikowane tworzenie systemów może być gdy nie mamy do dyspozycji tych gotowych serwisów i chcielibyśmy implementować skalowalny system przy pomocy softwareu instalowanego na naszych maszynach.

Fzf czyli twój specjalista od rozmytego wyszukiwania

https://github.com/junegunn/fzf


Co to dla nas znaczy i w czym pomaga to narzędzie? Najprościej będzie wytłumaczyć na przykładach. W końcu jeden przykład jest warty więcej niż 1000 słów czy jakoś tak.

Przykłady

Interaktywne wyszukiwanie

Zacznijmy od najważniejszego, wyszukiwanie odbywa się w sposób interaktywny tzn. zmiana naszego zapytania wpływa od razu na wyświetlane wyniki! Sprawia to, że wyszukiwanie jest po prostu super-wydajne!

Otwieranie pliku

Wyobraźmy sobie, że potrzebuję otworzyć jeden z plików źródłowych z poziomu konsoli. (jak on się nazywał…) 

Załatwione. Jeżeli dana fraza nam nie pasuje możemy strzałkami poruszać się w górę i w dół by wybrać to czego szukaliśmy.

Użycie wyniku w pipeline-nie

Przeszukiwanie plików otwiera przed nami morze możliwości. Nic nie stoi na przeszkodzie by wykorzystać rezultat wyszukiwania w fzf i przykładowo otworzyć wyszukany plik w wybranym edytorze.

fzf | ? { edit $_ }

Potrzebujesz przeszukać pojedyncze linie tekstu w ten sam sposób? Przekierowywujemy tekst do fzf i mamy to! 

To tak na prawdę najpopularniejszy przypadek użycia fzf. Zobacz jak wygodne jest teraz przeszukiwanie pliku.

Jeżeli szukasz jakiegoś kawałka kodu i chcesz to zrobić w sposób interaktywny to fzf ułatwi sprawę 

Jeżeli znudzi nam się standardowy wygląd fzf, mamy też całkiem spore możliwości manipulacji np 

fzf --layout reverse --border --color 'fg:#ff0000,fg+:#55aa66,bg:#00ff00,border:#0000ff'

Są jeszcze inne zastosowania, których sam nie wykorzystuję ale myślę, że warto wiedzieć, że są. Może komuś przypadnie do gustu:

  • Przechodzenie po folderach, można wykorzystać fzf do znajdywania interesującego folderu w drzewie.
  • Zabijanie procesów bazując na wyniku wyszukiwania z fzf do którego przekazujemy listę procesów.
  • Przeszukiwanie historii GIT pod kątem interesującego commita.

Jak widać możliwości są ogromne. Do wielu akcji, które obejmują wyszukiwanie tekstu możemy zaprząc fzf i ułatwić sobie życie!

Dla ułatwienia poszukiwań, zachęcam do rzucenia okiem na wiki fzf https://github.com/junegunn/fzf/wiki