Pisanie obiektowe a myślenie obiektowe przy refaktoryzacji z testowaniem
Celem artykułu jest pokazanie kilku rozwiązań problemów obecnych przy refaktoryzacji kodu zmierzającego do testów kodu aplikacji.
Artykuł docenią programiści pracujący na co dzień z kodem odziedziczonym (Legacy Code) lub kodem drogim w utrzymaniu (w którym wprowadzenie drobnych zmian zajmuje wiele wysiłku i/lub skutkuje pojawieniem się wielu błędów)
W poniższym artykule dodałem również wskazówki odnośnie zespołu, gdyż często bariery i problemy tworzą się w głowach a potem w czynach kolegów, którzy nie zawsze mówią jasno o tym co myślą na temat kodu i rozwiązań.
Warto rozumieć, że programista najpierw jest człowiekiem, pracownikiem i jego cele z reguły dotyczą tej warstwy, czyli nie liczy się tylko fakt, ale również oddziaływanie w zespole oraz rachunek zysków i strat na poziomie pozycji społecznej.
Być może warto spokojnie poczekać na odpowiedni czas dla zespołu, aż większość zrozumie jak w praktyce lokalnie szybko implementować to co wydaje się dziś tak trudne i pracochłonne w skali całego systemu.
Legacy code
Contents
Termin legacy code pierwotnie oznaczał on kod, który jest zależny od niewspieranej wersji systemu, języka, frameworka, etc.
Cechy profesjonalnej zmiany kodu
Zmieniamy kod, gdyż chcemy uzyskać nową jakość lub funkcjonalność.
Jesteśmy profesjonalistami, tzn. że nie mamy czasu na eksperymentowanie.
Mamy cel i wiemy jak go uzyskać.
Stosujemy małe precyzyjne kroki.
Miarą sukcesu nie jest ilość kroków, ale brak błędów podczas zmian, które wykluczyłyby cel w ogóle lub na jakiś czas.
Jakość kodu
Chcesz być dobrym programistą, dlatego troszczysz się o jakość kodu.
Działajcy kod, który rozwiazuje problem
Nigdy nie piszesz kodu, który tylko wydaje się, że działa, Starasz się tworzyć prawidłowo działajacy kod i masz dobre testy pozwalające to udowodnić. kod jest prawidłowy gdy rzeczywiście rozwiązuje problem. Nie ograniczasz się do tego, aby program tylko wyglądał na działający).
Kod łatwy w zrozumieniu i utrzymaniu
Dla kogo piszesz kod? dla innych ludzi. Piszesz kod, który jasno pokazuje swoje przeznaczenie, dlatego inni programiści mogą go szybko zrozumieć, przez to jest łatwy w konserwacji .Dobrze współpracujesz z innymi programistami. Uwzględniasz innych programistów i tworzysz czytelny dla nich kod. Twoim celem jest przecież to, aby zespół tworzył jak najlepsze oprogramowanie, a nie popisywanie się przed innymi.
Refaktoryzacja przy każdej okazji
Za każdym razem, gdy modyfikujesz jakiś fragment kodu, starasz się go ulepszyć, nadać mu lepszą strukturę, dokładniej go przetestować, ułatwić jego zrozumienie itd.). Zależy Ci na jakości kodu i programowania, dlatego nieustannie poznajesz nowe języki, idiomy i techniki. Stosujesz je jednak tylko wtedy, gdy ma to sens.
Dobra struktura kodu
Pisz kod w taki sposób, jakbyś pisał artykuł, ksiazke lub inna publikację dla ludzi, nie dla procesora.
- Dziel kod na rozdziały, akapity i zdania.
- Łącz podobne elementy ze sobą i rozdzielaj różne rzeczy.
- Funkcje są odpowiednikiem rozdziałów. W każdym rozdziale może znajdować się kilka odrębnych, ale powiązanych fragmentów kodu. Podziel je na akapity za pomocą pustych wierszy.
Testowanie
Według Feathersa („Working Effectively with Legacy Code”) testy są tak istotne, jak możliwość sprawdzenia, czy po zmianie kodu, system nadal będzie działał w sposób oczekiwany.
Wraz z próbą przetestowania chociaż jednego elementu systemu informatycznego wchodzimy w nową warstwę możliwości i problemów.
https://pl.wikipedia.org/wiki/Testowanie_oprogramowania
Typy i sposoby testowania
- Funkcjonalne testy
- Jednostkowe testy
- …
Proces testowania
kwestia procesu odnosi się do samego momentu tworzenia testu
- utworzonie testu
- warunek określający poprawne funkcjonowanie
- więcej testów w odniesieniu do tej samej funkcjonalności
Koszt i Amortyzacja Testów
Przeprowadzanie testów jest oczywiste dla producentów srzętu elektronicznego, ale mogą być abstrakcyjne dla kogoś kto widzi działający produkt i nie zna całego cyklu produkcji i związanym z nim ryzykiem.
skoro 'wszystko’ działa to testowanie jest zbędne?
Tak może zapytać każda osoba nieznająca kodu i procesu jego tworzenia, więc jak uzasadnić ponoszenie kosztów na coś co nie jest finalnym produktem?
Warto wobec tego zadać pytanie:
Co jest rezultatem tworzenia testów?
- zrefaktoryzowany, uproszczony kod, bardziej zrozumiały oparty o standardy i wzorce
- kod testów, która wskazuje czy aplikacja działa prawidłowo
- gdy testy już istnieją można wprowadzić automatyzację testowania
- programiści uczą się nowych umiejętności, które pozwalają pisać świadomie lepszej jakości kod
- kod jest mniej zależny i łatwiej o jego zrozumienie i rozbudowę
Zespół programistów a wprowadzenie refaktoryzacji w celu pokrycia kodu testami
Czy w waszym zespole jest czas na przegląd kodu?
Nic bardziej nie zmusi programisty do przemyślenia tego, co i jak napisać niż perspektywa kontroli. Jest to podobna ale idąca od góry droga prowadząca do podobnych rezultatów w jakości pisanego kodu przez programistów.
Co kontroluje się w przeglądzie kodu (Code Review)?
- wzorce i standardy
- warto zacząć CR od samego szkicu, komentarzy, by już na początku określić strukturę i przeznaczenie kodu
Testować czy nie testować?
Prościej zadać sobie pytanie:
- jaka jest twoja odpowiedzialność w projekcie?
Jeśli do obowiązków nie należy testowanie, to nie musisz się tym martwić.
Czy warto współpracować z zespołem, który nie rozumie specyfikacji tworzenia oprogramowania?
Każdy się rozwija w swoim tempie a nie każdy ma szefa który mu dozgonnie ufa co do propozycji zmian.
Czy warto tworzyć kolejne części aplikacji a potem zmuszać zespół do tworzenia i użcia nowych standardów, tylko dla tego projektu?
Kto wówczas będzie odpowiedzialny za tworzenie nowego standardu i jak długo?
Finalnie nie do programisty należy decyzja tylko do osoby odpowiedzialnej za koszty tworzenia kodu, więc nie zawsze można zrobić odpowienie testy.
Warto mieć na uwadze, że lepszy jest brak testów, gdy świadomość zespołu jest niska, niż gdy, przy tak niskiej świadomości, każdy programista będzie próbował tworzyć kod, który nie będzie zgodny z duchem i myślą w zespole. Wówczas może okazać się że implementacja testów będzie musiała być odroczona, lub na nowo wprowadzana z uwagi na brak wcześniejszych standardów i idącej z nimi jakości, a to będzie niosło ze sobą zbyt wysoki koszt w porównaniu do innej formy edukacji.
Alternatywa w perspektywie
Gdy mimo chęci i możliwości czasowych nie zostaje udzielona programiście lub całemu zespołowi możliwość wykonywania testów, warto iść drogą edukacji i inspiracji.
Kilka lat temu… starałem się przekonać szefa projektu, aby umożliwił nam używanie lepszego IDE. Nie przekonała go moja wiedza i doświadczenie z PHPStorm. Jednak po miesiącu sam zainicjował zmiany, pokazał mi nawet dlaczego… po prostu przeczytał w pewnej książce o Magento (systemie do sklepów internetowych), że PHPStorm jest polecany.
Właśnie dlatego, że nie każdy ma autorytet, zwłaszcza gdy pracuje w firmie krótko i uczy się nowego systemu. Dlatego warto próbować szukać publikacji i książek, które pozwolą nam przekonać osobę decyzyjną. Kilka pozycji jest na końcu tego artykułu.
Problemy i Rozwiązania
Koszty wytworzenia i zmiany kodu
Często wiele zmian, które czekają w poczekalni powstrzymuje koszt ich wdrożenia.
Wynika to bardziej z tego, że trudno przewidzieć ile czasu będzie potrzebne w rzeczywistoiści w odniesieniu do prawdopodobnej kalkulacji.
Istnieje bardzo dużo czynników mających wpływ na wykonanie zadania, dlatego można wiele kalkulacji określić intuicyjnie z małym błędem, tylko nie każdy ma taki dar, dlatego też warto te kwestie rozpatrywać razem w zespole, gdyż często ważniejsze jest zaangażowanie zespołu, niż techniczne prawdopodobnieństwo.
Kontrola jakości standardów i automatyzacja
Kod tworzony przez programistę ma strukturę i opiera sie o zdefiniowane standardy, np OOP. Skoro oczekujemy powtarzalności standardów, to również oczekujemy przestrzegania jakości tych standardów.
Celem Testu Jednostkowego w pierwszej kolejności jest forma a potem treść.
Dlaczego?
Wynika to z faktu iż najpierw trzeba zdefiniować instancję obiektu a dopiero potem jej użyć, by uzyskać w drodze porównania określoną wartość.
Jak to zrobić?
Gdy chcemy przeprowadzić kontrolę jak najszybciej to tylko w sposób możliwie zautomatyzowany. Miejmy na uwadze, że obecnie kontrola jakości i tak jest przeprowadzana ale manualnie i szczątkowo.
Refaktorzyacja kodu polega na zmianie struktury kodu, bez modyfikowania jego zachowania.
W refaktoryzacji w odróżnieniu od optymalizacji, nie jest naszym celem poprawienie funkcjonowania samego kodu ze względu na szybkość działania czy zajętość w pamięci.
Architektura
Nie ma sensu na siłę budować nowego lepszego świata, gdy kod zamykamy w modułach, bilbiotekach, API on może być już dziś lepszy w małej części.
Każda architektura ma jakieś cechy, wady, zalety, stąd zamykamy ją w małych boxach i idziemy dalej.
Efektem stworzenia sieci modeli jest bardziej użyteczna forma każdego modelu wchodzącego w skład systemu
Wzorce
Warto od razu nakreślić początek i koniec odpowiedzialności dla danej części aplikacji z wykorzystaniem zdefiniowanych i znanych już w zespole wzorców.
Nowa funkcjonalność korzystająca ze starego kodu może być nowym modułem, jest to możliwe przy tworzeniu z użyciem wzorców DI, API
Bo to programiści determinują czas i jakość a nie wzorzec.
Single Responsibility
Piszesz klasę tylko dla jednego celu
Dependency Injection
Bazujesz na programowaniu zorientowanemu objektowo, więc używasz klas jako dane wejścia i wyjścia, dzięki czemu każda dana jest ściśle zdefiniowana
Standardy w zespole
Warto zastosować standardy, które są znane każdemu programiście piszącego kod w naszym zespole.
- wzorzec
- biblioteka
- framework
- API
Warto zamykać różne elementy aplikacji, gdzie są stosowane różne technologie i wzorce jako autonomiczne moduły. Niech moduł jest określony jasno przez jedną architekturę.
Nie wszystko od razu
Wiele spraw trzeba przedyskutować:
- realizację kolejnych zmian
- priorytety
Gdzie zostawić informację, listę TODO dla kodu?
Najlepiej w komentarzach, np #TODO piszesz w komentarzu w kodzie, wtedy każdy widzi zmiany przy commitach/review.
Struktura kodu, zależności i modularyzacja
Struktura kodu jest jedną z przeszkód przy wprowadzaniu testów, stąd konieczność refaktoryzacji.
Powody zmiany wiążą się m. in. z:
- klasa jest zbyt rozbudowana
- klasa nie spełnia zasady Single Responsibility
Rozwiązanie:
- zamiast testować jedną dużą klasę, warto ją rozbić na kilka klas, zgodnie z zasadą Single Responisibility.
Wnioski:
- Wzrasta czytelność kodu
- Przeznaczenie kodu jest jasno określone, chociażby nazwą oraz użtymi metodami. W związku z tym nie potrzeba tyle opisowej dokumentacji, bo dodatkowo testy dają informację o użyciu kodu.
- wzrasta prawdopodobieństwo ponownego użycia klasy
Zależności
Zależności w kodzie źródłowym aplikacji występują w naturalny sposób, gdy nie dbamy o modularyzację, czyli nie dbamy o autonomiczność i modułowość elementów aplikacji.
Warto również określić co oznacza modularyzacja, gdyż podobnie jak pisanie i myślenie obiektowe może być uproszczone i źle zrozumiane.
Perspektywa zmian
Zamiast myśleć o całej perspektywie zmian, warto skupić się na myśleniu lokalnym tu i teraz.
- Kierunkowo i fachowo.
Więcej na: http://ronjeffries.com/xprog/articles/refactoring-not-on-the-backlog/
Modularyzacja
Modularyzacja jest przeciwieństwem zależności
Zależność jest cechą Spaghetti Code.
Modularny kod, nawet gdy nie jest testowany pozwala na zastosowanie alternatywy do każdego elementu kodu.
Przykładem modularnego i nie obiektowego kodu może być aplikacja oparta o wiele API, z którym komunikują się elementy aplikacji. Dodatkowa cecha takiego rozwiązania to fakt, że nie ma znaczenia kod jaki jest użyty po stronie API.
Modularyzacja z pewnością jest cechą aplikacji, która posiada niezależne moduły, które mają otwarty interfejs, określony i opisany przez API, Dokumentację, Testy.
Modularyzuj czyli: dziel i rządź!
Dziel i rządź, sprawdza się w kodzie i na ludziach.
Oznacza to, że finalnie zdefiniowaną funkcjonalność zamykamy w modułach, bilbiotekach, API, …
Pisanie obiektowe
Pisanie obiektowe to użycie klas i metod, zamiast plików z funkcjami charakterystycznymi dla kodu strukturalnego.
Myślenie obiektowe
Myślenie obiektowe to przede wszystkim przemyślana struktura, zakładająca dwie podstawowe zasady:
- Zasada pojedynczej odpowiedzialności (ang. single responsibility principle)
- Wstrzykiwanie zależności (ang. Dependency Injection, DI) – wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami na rzecz architektury typu plug-in
Myślenie obiektowe nie jest zależne od technologii, tylko od zasad, które pozwalają na powstanie kodu, jest ściśle określonym planem na utwrzenie nowego kodu.
Systematyka, czyli małe kroki już dziś!
najbardziej optymalne jest robienie małych usprawnień.
Ponadto małe zmiany są łatwo przyswajalne i inny developer może użyć rozwiązanie już istniejące w mikroskali, by rozwiązać taki sam problem.
Z czasem z kilku helperów można utworzyć małą bibliotekę.
Fakt, że zmiana jest oddolna – czyli mniejsze komponenty uległy zmianie jako pierwsze – przekłada się później na niższy koszt kolejnych usprawnień.
Za każdym razem staraj się zostawić to co zastałeś w lepszym stanie
SOLID
Lokalny refactoring klasy, tak by była bliżej SOLID.
Zamiast wprowadzać kolejne warstwy abstrakcji w całym systemie warto stosować abstrakcję lokalnie, gdy jest na nią zapotrzebowanie, w precyzyjnie określonych typach danych.
Ważne jest pokrywanie tych zmian testami, dzięki którym kolejne pokolenia będą miały ułatwione zadania tego typu.
Przykład prywatnej methody w klasie
Skoro występuje prywatna metoda to znaczy, że nie ma znaczenia na zwenątrz systemu.
Ma lokalne zastosowanie, więc po co ją testować? a jeśli testować, to powinna być zwenętrzną klasą, helperem z publiczną methodą poprzez wstryzknięcie zależności (Dependency Injection).
Literatura
Poniżej kilka pozycji pozwalające na efektywne wprowadzenie jakości we własnym kodzie, aby był szybko zrozumiany przy współpracy z zespołem:
- „Effective Java” – Joshua Bloch
- „Clean Code – A Handbook of Agile Software Craftsmanship” – Robert C. Martin
- „Implementation Patterns” – Kent Beck
Jeśli w projekcie nie zostały stosowane praktyki i techniki zawarte w powyższych książkach to warto sięgnąć po:
- „Beheading the Legacy Beast” – Daniel Brolund, Ola Ellnestam
- „Working Effectively with Legacy Code” – Michael Feathers
Wnioski
- Testy nie czynią oprogramowania wolnego od błędów;
- Testy obrazują działanie określonego wycinka aplikacji, pozwalają na symulację działania w inkubatorze, by znaleźć lepsze rozwiązanie starego problemu;
- Testy otwierają kod na zmiany
- Aktualne testy opisują działanie, lepiej niż często nieaktualna dokumentacja
- Testy sygnalizują i informują o działaniu każdego elementu programu, nawet przy tysiącach zmian w kodzie i setek tysięcy testów na godzinę w środowisku continuous delivery
- Testy wskazują osobę odpowiedzialną gdy powstają błędy (np. przy odpowiednim skonfigurowaniu poprzez oprogramowanie gitlab)
A jakie są Twoje wnioski i pomysły?