e-Informatica Software Engineering Journal Weryfikacja przekształceń refaktoryzacyjnych

Weryfikacja przekształceń refaktoryzacyjnych

Bartosz Walter[2] ,  Jerzy R. Nawrocki
Politechnika Poznańska Instytut Informatyki
{Bartosz.Walter, Jerzy.Nawrocki}@put.poznan.pl

Grow, not build software

–Frederick Brooks
Streszczenie

Refaktoryzacja oprogramowania to zachowujące funkcjonalność przekształcenie kodu źródłowego programu, które poprawia jego czytelność i strukturę. Jednak refaktoryzacja powoduje również wzrost kosztów wytwarzania oprogramowania, a ponadto zwiększa ryzyko wprowadzenia do niego dodatkowych błędów spowodowanych zmianami. Dlatego istotnym problemem jest zapewnienie poprawności i bezpieczeństwa przekształceń. Dwie podstawowe metody weryfikacji, statyczna analiza kodu oraz testowanie jednostkowe, są trudne i pracochłonne, dlatego istnieje potrzeba precyzyjnego wskazania zakresu ich stosowania oraz ich ograniczeń. W rozdziale tym przedstawiono klasyfikację przekształceń z katalogu M. Fowlera według sposobu ich weryfikacji wraz z omówieniem wybranych przypadków. Zaprezentowano także metodę automatycznego generowania przypadków testowych na potrzeby refaktoryzacji.

1 Wprowadzenie#

Ciągła ewolucja oprogramowania stanowi poważny problem. Jej przyczyną może być zarówno zmiana wymagań, jak i konieczność optymalizacji kodu czy wykryty błąd. Niestety, każda zmiana, niezależnie od związanego z nią dodatkowego kosztu, nie pozostaje bez wpływu na strukturę systemu. Architektura przystosowana do poprzedniej wersji może okazać się nieprzydatna po zmianie założeń lub wprowadzeniu nowych funkcji, a wprowadzona naprędce poprawka – jedynie utrudnieniem w dalszym rozwoju oprogramowania. Taki system staje się droższy w utrzymaniu i bardziej narażony na błędy.Dlatego rosnącą popularnością cieszy się refaktoryzacja – metoda wprowadzania kontrolowanych zmian w kodzie źródłowym, która pozwala zachować dotychczasową funkcjo funkcjonalność, zwiększając jednocześnie czytelność lub elastyczność [Fowler1999] ]. Szczególnie ważny jest warunek zachowania funkcjonalności, ponieważ pozwala uniknąć wprowadzenia do programu nowych błędów, a co za tym idzie – istotnie ograniczyć koszt wprowadzania zmian i późniejszej pielęgnacji programu.

Spośród istniejących sposobów weryfikacji poprawności refaktoryzacji powszechnie stosowane są dwa: statyczna analiza warunków wstępnych i końcowych przekształcenia oraz testowanie jednostkowe. Niestety, mimo że mechanika poszczególnych przekształceń została dość szczegółowo opisana przez Fowlera, to jednak jego wskazówki dotyczące ich weryfikacji zwykle sprowadzają się do polecenia ‘skompiluj i przetestuj’. Dlatego tak ważna jest określenie, jakie warunki decydują o poprawności konkretnego przekształcenia. O ile przekształcenia, które można weryfikować analitycznie, zostały zidentyfikowane i opisane wraz z warunkami poprawności (np. [Opdyke1992] ]), to w przypadku weryfikacji przez testowanie liczba i rodzaj testów pozostają niezbadane, co w znacznym stopniu ogranicza możliwość zautomatyzowania tego procesu.

W rozdziale tym przedstawiono klasyfikację przekształceń z katalogu Fowlera [Fowler1999] pod kątem sposobu ich weryfikacji. Obok istniejących dotąd nieformalnie kategorii przekształceń łatwych (analitycznych) i trudnych (wymagających testowania) wprowadzono nową kategorię przekształceń o znanym zakresie testowania, dla których można wskazać szablony typowych testów. Pozwala to zwiększyć stopień automatyzacji dla wybranych przekształceń. Ponadto omówiono krótko czynniki wpływające na złożoność weryfikacji przekształceń i zaproponowano metodę generowania testów na potrzeby refaktoryzacji.

W sekcji 2. rozdziału przedstawiono proces refaktoryzacji oraz krótko scharakteryzowano związane z nim problemy.

Sekcje 3. i 4. stanowią wprowadzenie do weryfikacji poprawności przekształceń wraz z prezentacją proponowanej klasyfikacji refaktoryzacji.

Sekcja 5. zawiera podsumowanie oraz propozycje kierunków dalszych prac.

2 Refaktoryzacja oprogramowania#

Istotą refaktoryzacji oprogramowania jest zmiana jego wewnętrznej struktury w sposób nie naruszający jego widocznego zachowania, natomiast poprawiający jego wewnętrzną strukturę [Fowler1999] . Szybko zmieniające się wymagania funkcjonalne i konieczność rozszerzania możliwości programu z jednej strony oraz wprowadzane lokalne poprawki z drugiej pogarszają jakość kodu, powodują, że staje się on nieczytelny i kosztowny w pielęgnacji. K. Beck wprowadził pojęcie przykrego zapachu, czyli działającego fragmentu kodu, który jednak wymaga poprawienia z uwagi na złą strukturę, brak elastyczności czy niebezpieczeństwo pojawienia się błędu. Wśród najczęściej spotykanych zapachów wymienia się m.in. duplikację kodu, zbyt długie metody, rozbudowane sygnatury metod, za duże klasy, błędny podział odpowiedzialności pomiędzy klasami i skomplikowane wyrażenia warunkowe. Wykrywanie tych zapachów jest w niewielkim stopniu zautomatyzowane i w wielu przypadkach zależy od intuicji programisty, co znacznie zwiększa ryzyko i konieczny nakład pracy związane z refaktoryzacją.Ponieważ refaktoryzacja z założenia pozwala bezpiecznie wprowadzać zmiany w kodzie, jest ona kluczowym elementem tzw. zwinnych metodyk tworzenia oprogramowania (ang.agile methodologies), jak np. XP [Beck2000]. W praktyce jednak możliwość bezpiecznego przeprowadzania zmian w kodzie jest ważna niezależnie od przyjętej metodologii, gdyż samo przyjęcie założenia o stworzeniu kompletnego i spójnego projektu przed przystąpieniem do implementacji nie chroni przed tymi zmianami.

Refaktoryzacja, jako czynność nie zwiększająca funkcjonalności oprogramowania, nie przynosi szybkich korzyści, natomiast wymaga sporego nakładu pracy i jest obarczona ryzykiem wprowadzenia błędów do programu. Dlatego celem prowadzonych badań jest zmniejszenie jej pracochłonności poprzez automatyzację, przy jednoczesnym zapewnieniu bezpieczeństwa tego procesu. Refactoring Browser [Roberts1997] autorstwa D. Robertsa był pierwszym narzędziem pozwalającym na łatwą refaktoryzację programów napisanych w Smalltalku. Od tego czasu powstało wiele innych dedykowanych narzędzi, szczególnie dla języka Java, a wsparcie dla refaktoryzacji pojawiło się w popularnych środowiskach IDE, np. Eclipse, JBuilder czy TogetherJ. Niestety, w większości tych środowisk zaimplementowany jest zbiór tych samych, prostych przekształceń. Przyczyna takiego stanu leży w trudności określenia, czy dane przekształcenie faktycznie zostało wykonane poprawnie.

3 Weryfikacja poprawności przekształceń#

Istotą refaktoryzacji jest przeprowadzenie zmiany w sposób bezpieczny, to znaczy utrzymujący dotychczasową funkcjonalność zmienianego fragmentu programu. Istnieją dwie podstawowe metody weryfikacji poprawności przekształceń: analiza własności zmienianego kodu oraz testowanie jednostkowe wprowadzonych zmian.

3.1 Analiza warunków wstępnych i końcowych#

Metoda analizy kodu programu sprowadza się do wyznaczenia dla każdego przekształcenia warunków wstępnych oraz końcowych, które muszą być spełnione, aby zmiana była poprawna. Warunki te to najczęściej statyczne własności klasy lub metody (np. jej relacje dziedziczenia, powiązania z innymi elementami systemu), dlatego ich weryfikacji w większości przypadków może dokonać kompilator lub analizator składniowy. Opdyke i Roberts w swoich pracach doktorskich [Opdyke1992] [Roberts1996] wskazali dwadzieścia sześć podstawowych i trzy złożone przekształcenia, które można zweryfikować w ten sposób, oraz podali wstępne i końcowe warunki ich poprawności. Niestety, wiele z tych przypadków dotyczy oczywistych sytuacji, m.in. usunięcia pustej klasy, do której nie ma odwołań. Dlatego metoda analityczna jest użyteczna jedynie przy weryfikacji wybranych, prostszych przekształceń.

3.2 Testowanie jednostkowe#

Drugą podstawową metodą weryfikacji poprawności przekształceń są testy jednostkowe (ang. unit tests). Są one implementowane przy użyciu bibliotek z serii xUnit, (np. jUnit [JUnit2000] w postaci asercji sprawdzających istnienie pożądanych relacji w kodzie programu. Ich podstawowym przeznaczeniem jest regresyjna weryfikacja poprawności działania poszczególnych metod – czyli stwierdzenie, czy wprowadzane zmiany nie zmieniają ich zachowania. Ta ich cecha jest wykorzystywana przy refaktoryzacji oprogramowania – stanowią one niezmienniki przekształceń: testy muszą być spełnione zarówno przed, jak i po przeprowadzeniu zmiany. Dzięki ich automatyzacji mogą być wykonywane często i przy niskim nakładzie pracy.Niestety, tworzenie i pielęgnacja testów wymagają znacznie większego nakładu pracy, co powoduje, że testowanie jako metoda weryfikacji przekształceń refaktoryzacyjnych jest trudne, pracochłonne i nie zawsze gwarantuje poprawność weryfikowanych przekształceń. Z tego powodu istotne jest również zautomatyzowanie procesu tworzenia testów. Koncepcję generowania przypadków testowych opartą na szablonach testów przedstawiono w sekcji 4.5.

4 Klasyfikacja przekształceń według sposobu weryfikacji#

Istnieje kilka klasyfikacji przekształceń refaktoryzacyjnych. Najpopularniejsza z nich została podana przez Fowlera [Fowler1999] ], który podzielił je według funkcji dokonywanej zmiany oraz opisał ich mechanikę. Inna klasyfikacja, podana przez Van Deursena i Moonena [Deursen2001b] ], dzieli przekształcenia według ich wpływu na testy refaktoryzacyjne.Istotnym problemem przy dokonywaniu refaktoryzacji jest brak wskazówek, jakiego rodzaju weryfikacji wymaga dane przekształcenie. Fowler pozostawia tę kwestię inwencji programisty, podając jedynie w opisie mechaniki, kiedy należy zmieniany kod skompilować i przetestować. Opis ten jest zbyt lakoniczny i wieloznaczny, dlatego nie zawsze jest użyteczny. Istnieje zatem potrzeba wskazania, jaki rodzaj weryfikacji jest wystarczający dla każdego przekształcenia, oraz jakie okoliczności mogą mieć na to wpływ.

Zaproponowana w tym rozdziale klasyfikacja przekształceń według sposobu ich weryfikacji stanowi rozszerzenie istniejącego nieformalnego podziału na przekształcenia proste(weryfikowane analitycznie) oraz trudne (pozostałe). Nowa klasyfikacja dzieli przekształcenia na trzy kategorie:

  • Przekształcenia proste, które można zweryfikować poprzez sprawdzenie określonych warunków wstępnych i końcowych. Do kategorii tej należą m.in. przekształcenia, których poprawność można stwierdzić poprzez kompilację – niepowodzenie oznacza, że zostały przeprowadzone nieprawidłowo;
  • Przekształcenia o znanym zakresie testowania, które wprawdzie wymagają przeprowadzenia testów jednostkowych (dokonanie analizy statycznej jest trudne lub niemożliwe), jednak można wskazać, jakiego typu testy są potrzebne. Refaktoryzacje znajdujące się w tej kategorii można w pewnym stopniu zautomatyzować, wskazując sposoby testowania i wymagające tego punkty w programie;
  • Przekształcenia o nieznanym zakresie testowania, które również wymagają testowania, jednak z powodu ich ogólności bądź wpływu wielu okoliczności, które trudno kontrolować, nie można wskazać uniwersalnego zbioru testów niezbędnych do zweryfikowania poprawności. Grupa ta obejmuje przekształcenia trudne i kosztowne w realizacji.

Nowością w tym podziale jest kategoria przekształceń o znanym zakresie testowania. Znajdują się w niej przekształcenia, dla których zestaw testów wynika z natury wprowadzanej zmiany. Dzięki temu jest możliwe częściowo zautomatyzowane tworzenie przypadków testowych lub przynajmniej wskazywanie sposobów i rodzajów testów. Choć pełna automatyzacja przekształceń z tej kategorii wydaje się trudna, to jednak sama informacja o niezbędnych testach stanowi istotną pomoc dla programisty.

Jednak klasyfikacja ta nie wprowadza stabilnego podziału przekształceń. Przynależność przekształcenia do danej kategorii zależy od dodatkowych czynników, np. języka programowania czy środowiska uruchomienia. Powoduje to, że dla każdej konfiguracji tych czynników liczebny rozkład przekształceń może wyglądać inaczej, choć sama koncepcja pozostanie prawdziwa. W przedstawionym zestawieniu podano klasyfikację przekształceń podanych przez Fowlera [Fowler1999] dla implementacji w języku Java, zakładając jednowątkowe wykonywanie programu bez użycia mechanizmu refleksji. Czynniki wpływające na klasyfikację zostały krótko omówione w sekcji 4.4. Same kategorie zostały przedstawione w sekcjach 4.1–4.3 wraz z listą należących do nich przekształceń oraz wybranymi przykładami.

4.1 Przekształcenia proste#

Do przekształceń prostych zaliczono takie, których poprawność można zweryfikować poprzez statyczną analizę zmienianego kodu oraz związanych z przekształceniem warunków wstępnych i końcowych. W przypadku części tych przekształceń do weryfikacji wystarczająca jest kompilacja – jeżeli nie powiedzie się, wówczas przekształcenie jest niepoprawne.W kategorii tej znalazły się przekształcenia najprostsze w stosowaniu, dlatego stanowią większość refaktoryzacji implementowanych w środowiskach IDE. Jednak weryfikacja warunków wstępnych w wielu przypadkach jest na tyle trudna (lub warunki te istotnie zmieniają się w zależności od okoliczności), że nawet spośród nich jedynie część jest faktycznie implementowana.

Lista przekształceń prostych została przedstawiona w tabeli 1.

Tabela 1. Proste przekształcenia refaktoryzacyjne
Extract Method Split Temporary Variable
Inline Method Remove Assignments to Parameters
Introduce Explaining Variable Self Encapsulate Field
Replace Magic Number with Symbolic Constant Encapsulate Field
Replace Record with Data Class Decompose Conditional
Rename Method Hide Method
Add Parameter Remove Parameter
Pull Up Field Pull Up Method
Push Down Method Push Down Field
Collapse Hierarchy Encapsulate Downcast
Introduce Foreign Method Extract Interface

Ponieważ te przekształcenia zostały szczegółowo zbadane (m.in. w [Opdyke1992], [ÓCinnéide2000] i [Tokuda2001] ), dlatego omówione krótko zostaną dwa przykłady prostych przekształceń: Extract Method oraz Add Parameter.

Extract Method#

Przekształcenie to polega na wyłączeniu z długiej metody fragmentu kodu, a następnie utworzenia z niej nowej metody i wywołaniu jej miejscu, z którego została wyłączona.Podstawowym problemem, które wiąże się z tym przekształceniem, to zmiana kontekstu wywołania metody oraz idąca za tym konieczność przekazania do i z metody potrzebnych parametrów. Ponieważ w Javie parametry są przekazywane wyłącznie przez wartość, dlatego nie ma możliwości zmodyfikowania przekazanego w ten sposób argumentu. W przypadku typów podstawowych jest to oczywiste, natomiast gdy przekazywana jest referencja (a ściślej jej kopia) do obiektu, wówczas metoda może wprawdzie zmienić stan obiektu, ale nie może zmienić samego obiektu (utworzyć nowego i przypisać go tej referencji). Dlatego w przypadku Javy przekazanie parametrów jest operacją bezpieczną.

Niestety, ta właściwość języka powoduje, że jedyną możliwością zwrócenia zmodyfikowanych wartości z metody jest instrukcja return. Gdy istnieje konieczność zwrócenia większej liczby atrybutów, można albo podzielić nową metodę na mniejsze, tak aby każda zwracała jedną wartość, albo utworzyć klasę grupującą te atrybuty i zwrócić referencję do obiektu tej klasy. Oba rozwiązania nie są jednak uniwersalne, dlatego nie spotyka się ich w rzeczywistych implementacjach tego przekształcenia.

Sytuacja jest nieco trudniejsza w przypadku języków dopuszczających przekazywanie parametrów przez referencję. W tym przypadku można zapewnić niezmienność wybranych parametrów, korzystając z innego prostego przekształcenia Remove Assignments to Parameters lub deklarując je jako niemodyfikowalne (zamknięte, ang.final), natomiast zwrócić zmienione atrybuty dzięki mechanizmowi parametrów przekazywanych przez referencje.

Do stwierdzenia poprawności tego przekształcenia (przy założonych ograniczeniach) wystarczy, by nowa metoda nie pokrywała innej metody z nadklasy (to jednak sprawdza kompilator, ponieważ nowa metoda jest prywatna), oraz zapewnienie, że wszelkie zmieniane wartości są zwracane przez referencję (w sposób umożliwiający ich odczyt z metody wywołującej). Przekształcenie to nie powoduje zmiany interfejsu, a więc nie ma wpływu na przebieg istniejących testów.

Add Parameter#

To przekształcenie powoduje dodanie do sygnatury metody nowego parametru. Ponieważ Java nie pozwala na stosowanie parametrów domyślnych, dlatego odwołania do nieprzeciążonej metody o zmienionej sygnaturze zostaną wykryte podczas kompilacji. Kwestię ewentualnego naruszenia hierarchii dziedziczenia można również łatwo rozwiązać poprzez analizę kodu.Większym problemem jest możliwość przeciążenia istniejącej metody w ten sposób, że dwie metody będą miały tę samą liczbę parametrów, które można odpowiednio rzutować na siebie. Wówczas wywołania z parametrami wymagającymi niejawnego rzutowania mogą spowodować wywołanie innej metody niż dotychczas, co może wpłynąć na działanie programu. Jednak ten problem również można jednoznacznie rozwiązać analitycznie (specyfikacja języka Java [Java2002] definiuje dokładnie, która metoda zostanie wywołana w takim wypadku).

Dodanie parametru w metodzie zmienia interfejs metody, dlatego testy jednostkowe (jeżeli istnieją) muszą zostać zaktualizowane.

4.2 Przekształcenia o znanym zakresie testowania#

Jest to najważniejsza kategoria w proponowanej klasyfikacji. Należące do niej przekształcenia wprawdzie wymagają testowania, jednak można wskazać zestaw konkretnych szablonów testów, które są związane z danym przekształceniem, podobnie jak jego nazwa, mechanika i opis,.Naturalnie, w przypadku tych przekształceń nadal konieczne jest sprawdzenie określonych warunków statycznych, a więc kategoria stanowi nadzbiór przekształceń prostych. W niektórych przypadkach testowanie (stanowiące metodę weryfikacji a posteriori) może również zastąpić elementy analizy statycznej, jeżeli ta okazałaby się zbyt złożona.

Przekształcenia należące do tej kategorii przedstawiono w tabeli 2.

Spośród przekształceń w tej kategorii pokrótce omówiono sposób oraz punkty testowania wymagane do weryfikacji poprawności trzech wybranych przekształceń.

Tabela 2. Przekształcenia o znanym zakresie testowania
Inline Temp Replace Temp with Query
Move Method Replace Method with Method Object
Move Field Hide Delegate
Inline Class Introduce Local Extension
Remove Middle Man Replace Array with Object
Replace Data Value with Object Remove Control Flag
Encapsulate Collection Replace Parameter with Method
Duplicate Observed Data Change Unidirectional Association
to Bi-directional
Replace Type Code with Class Replace Parameter with Explicit Method
Consolidate Duplicate Condition Fragments Replace Nested Conditional with Guard Clauses
Replace Constructor with Factory Method Remove Setter
Introduce Null Object Pull Up Constructor Body
Introduce Param Object

Move Method#

Przekształcenie to stosowane jest w przypadku, gdy metoda znajduje się w niewłaściwej klasie, np. gdy częściej odwołuje się do pól i metod obcego obiektu niż własnego.W przypadku metody statycznej jej przeniesienie wymaga jedynie udostępnienia w nowej klasie potrzebnych jej pól i metod (które także muszą być statyczne), co można zweryfikować analitycznie (Eclipse w obecnej wersji 2.1 implementuje to przekształcenie właśnie tylko dla metod statycznych). Składowe te można przekazać bądź jako parametry, bądź też zwiększając zakres dostępu do nich.

W przypadku metody obiektu jej przeniesienie, poza udostępnieniem niezbędnych danych, wiąże się często z koniecznością pozostawienia w dotychczasowym miejscu delegacji do nowych metod, a więc powiązania „starej” i „nowej” klasy asocjacją (choć zwykle takie powiązanie już istnieje). Dlatego do stwierdzenia poprawności wymagane jest wykonanie następujących testów:

  • Czy metoda w dotychczasowej klasie, zwracająca referencję do nowego obiektu, zwraca niepustą referencję, wskazującą na właściwy obiekt?
  • Czy istnienie dotychczasowego właściciela metody implikuje istnienie nowego?
  • Czy wywołanie metody w nowym obiekcie (bezpośrednie lub poprzez delegację w dotychczasowym) w identyczny sposób powoduje zmiany w obiektach, do których te metody się odwołuje, co wywołanie metody przed zmianą?

Change Unidirectional Association to Bi-directional#

Zmiana jednokierunkowej asocjacji na dwukierunkową oznacza, że oba uczestniczące w niej obiekty (lub grupy obiektów) będą posiadały referencje do siebie nawzajem.Podstawowym problemem jest utrzymanie spójności tego powiązania, czyli zapewnienie, że dodanie lub usunięcie obiektu po kontrolowanej stronie asocjacji powoduje aktualizację referencji zarówno po kontrolowanej, jak i kontrolującej stronie powiązania. W wyniku refaktoryzacji kontrolowana strona asocjacji powinna uzyskać wskaźnik (zbiór wskaźników) do obiektu (obiektów) kontrolującego.

Nie można tego stwierdzić wyłącznie poprzez statyczną analizę, dlatego po dokonaniu zmiany konieczne jest przetestowanie następujących sytuacji:

  • Czy dodanie obiektu po kontrolowanej stronie powoduje utworzenie jednoczesne referencji do obiektu kontrolującego (inicjującego dodanie) w obiekcie kontrolowanym i referencji do obiektu kontrolowanego w obiekcie kontrolującym?
  • Czy powtórne dodanie referencji do tego samego obiektu powoduje utrzymanie liczności zbioru referencji (nie pojawia się duplikat)?
  • Czy próba dodania referencji pustej nie powoduje rzeczywistego dodania jej do zbioru referencji?
  • Czy usunięcie obiektu po kontrolowanej stronie asocjacji powoduje jednoczesne usunięcie referencji do obiektu kontrolującego (inicjującego) w obiekcie kontrolowanym i referencji do obiektu kontrolowanego w obiekcie kontrolującym?

Remove Setter#

To przekształcenie powoduje usunięcie metod zmieniających stan obiektu i służy do zmiany obiektu w obiekt niezmienny (ang. immutable), którego stanu po utworzeniu obiektu nie można zmienić. Jest ono często pierwszym krokiem przy realizacji przekształcenia Change Reference to Value.W tym celu konieczne jest przeprowadzenie następujących testów:

  • Czy konstruktor wykonuje funkcje realizowane dotąd przez metody zmieniające stan obiektu (np. walidację danych)?
  • Czy metoda equals() porównujące dwa obiekty działa prawidłowo dla obiektów takich samych, tego samego typu, różnych typów, wartości pustych?
  • Czy metoda hashCode() jest obliczana dla wszystkich pól obiektu?
  • Czy metody zwracające dotąd instancję tej samej klasy po zmianie zwracają nowy obiekt?
  • Czy wywołanie dowolnej metody nie powoduje zmiany stanu obiektu (wartość metody hashCode() nie ulega zmianie)?

4.3 Przekształcenia o nieznanym zakresie testowania#

Kategoria ta, w odróżnieniu od poprzedniej, zawiera przekształcenia, dla których nie można podać wyczerpującego zestawu testów weryfikujących ich poprawność. Przyczyną tego stanu jest albo zbytnia ogólnikowość przekształcenia podana przez Fowlera, albo nie dający się ocenić wpływ przekształcenia na resztę systemu. Do tej kategorii należą przekształcenia trudne, o dużej złożoności, wprowadzające istotne zmiany w znacznej części systemu.Sposób testowania każdego przekształcenia z tej grupy zależy w dużej mierze semantyki zmienianego kodu i sposobu implementacji, nie jest natomiast cechą samego przekształcenia. Dlatego testy trzeba opracowywać w każdym przypadku niezależnie, co znacznie zwiększa pracochłonność przekształcenia i praktycznie wyklucza jakąkolwiek automatyzację.

Ponieważ wiele z przekształceń należących do tej kategorii jest sformułowanych bardzo ogólnie, dlatego przy założeniu pewnych dodatkowych ograniczeń można sklasyfikować je jako refaktoryzacje o znanym zakresie testowania i podać dokładne metody testowania ich poprawności.

Przekształcenia należące do tej kategorii znajdują się w tabeli 3.

Tabela 3. Przekształcenia o nieznanym zakresie testowania
Substitute Algorithm Change Bi-directional Association to Unidirectional
Replace Type with State/Strategy Replace Type Code with Subclasses
Introduce Assertions Separate Query from Modifier
Replace Error Code with Exception Replace Exception with Test
Extract Class Change Value to Reference
Change Reference to Value Replace Subclass with Fields
Parameterize Method Preserve Whole Object
Consolidate Conditional Expressions Replace Conditional with Polymorphism
Extract Subclass Extract Superclass
Form Template Method Replace Inheritance with Delegation
Replace Delegation with Inheritance Tease Apart Inheritance
Convert Procedural Style to Objects Separate Domain from Presentation
Extract Hierarchy

Przedstawimy teraz przykład przekształcenia należącego do tej kategorii.

Replace Exception with Test#

Mechanizm wyjątków, obecny m.in. w Javie, pozwala łatwo i skutecznie reagować na błędy pojawiające się w trakcie wykonywania programu. Niestety, obsługa wyjątku zmienia także sposób wykonywania programu, kolejność instrukcji itp. Z tego powodu, a także wskutek wysokiego kosztu związanego ze zgłoszeniem wyjątku, zaleca się unikanie i zapobieganie wyjątkom, np. poprzez sprawdzenie okoliczności mogących spowodować wyjątek.Wyjątki w Javie dzielą się na sprawdzane (ang. checked) i niesprawdzane (ang.unchecked). Wyjątki sprawdzane podlegają statycznej kontroli w momencie kompilacji, natomiast wyjątki dynamiczne mogą być zgłaszane w dowolnym miejscu programu i momencie jego wykonywania, a nieprzechwycone – spowodować zakończenie programu.

Dlatego zastąpienie zgłoszenia wyjątku sprawdzeniem, czy wyjątek taki może się pojawić, mocno zmienia sposób przepływu sterowania w programie, w szczególności w obrębie instrukcji catch i finally. Z tego powodu nie można podać uniwersalnych metod sprawdzania, czy w każdych okolicznościach dana metoda zachowuje się tak samo. Sposób testowania takiego przekształcenia zależy od rodzaju wyjątku, kontekstu, w jakim jest zgłaszany oraz samego testu, który go zastępuje, a więc nie poddaje się automatyzacji.

4.4 Czynniki wpływające na klasyfikację#

Podana klasyfikacja nie wprowadza stabilnego podziału, ponieważ przynależność wielu przekształceń do danej kategorii zależy od dodatkowych czynników, wśród nich języka programowania, sposobu realizacji współbieżności, obsługi błędów czy metody weryfikacji typów. Jednak niezależnie od tego, koncepcja podziału wraz z definicją nowej kategorii pozostają bez zmian; zmieniają się jedynie liczbowe proporcje pomiędzy kategoriami.Przedstawiona klasyfikacja jest przykładem podziału implementację przekształceń podanych przez Fowlera dla języka Java, przy założeniu jednowątkowego wykonywania programu i bez wykorzystania mechanizmu refleksji. Jednak np. użycie języka posiadającego wyłącznie niesprawdzane wyjątki (C#) spowoduje, że niektóre przekształcenia o znanym zakresie testowania znajdą się w kategorii przekształceń trudnych.

Klasyfikacja ta zupełnie zmienia postać także w przypadku zastosowania mechanizmu dynamicznej kontroli typów, np. Java reflection. W takiej sytuacji statyczne określanie właściwości programu jest niemożliwe, a możliwość zastosowania analizy staje się bardzo ograniczona.

Z drugiej jednak strony, po nałożeniu dodatkowych ograniczeń na zakres stosowania poszczególnych przekształceń stają się one prostsze, i zamiast testowania można je zweryfikować poprzez analizę statyczną. Na przykład, przekształcenie Inline Temp, które zastępuje odwołania do zmiennej przechowującej wartość wyrażenia samym wyrażeniem, staje się przekształceniem prostym, jeżeli wiadomo (np. poprzez użycie odpowiedniego języka), że proces obliczenia wartości wyrażenia nie zmienia stanu systemu (czyli żadne podwyrażenie nie zmienia wartości żadnej zmiennej i stanu żadnego obiektu).

4.5 Szablony testów refaktoryzacyjnych#

Jednym z założeń przyjmowanych podczas refaktoryzacji jest fakt, że testy jednostkowe się nie zmieniają i stanowią punkt odniesienia, element, który nie ulega zmianie w trakcie realizacji przekształcenia.Jednak nie wszystkie testy jednostkowe pełnią taką rolę. Niektóre sprawdzają elementy specyficzne dla danej implementacji, a więc po jej zmianie mogą zostać zweryfikowane negatywnie, co jednak nie oznacza, że samo przekształcenie jest niepoprawne. Wiąże się z tym także niebezpieczeństwo niewykrycia błędu (błąd drugiego rodzaju) albo stwierdzenia go w sytuacji, gdy przekształcenie jest poprawne (błąd pierwszego rodzaju). Znajomość testów wymaganych do sprawdzenia poprawności redukuje ryzyko związane z tą sytuacją: wówczas wiadomo, które spośród testów muszą być spełnione.

Znacznie większym problemem jest konieczność modyfikacji testów, spowodowana np. zmianą interfejsów klas i metod podczas wprowadzania niektórych przekształceń. Wprawdzie ta zmiana jest zwykle wykrywana już na etapie kompilacji i nie prowadzi do błędów, jednak aktualizacja testów nadal jest utrudniona. Van Deursen i Moonen [Deursen2001b] podzielili przekształcenia z katalogu Fowlera w zależności od ich wpływu na testy jednostkowe na pięć kategorii: złożone – które składają się z łańcuchów przekształceń podstawowych, zgodne – które nie zmieniają pierwotnych interfejsów,zgodne wstecz – w których przekształcenie rozszerza interfejs, potencjalnie zgodne wstecz – które można uczynić zgodnymi wstecz oraz niezgodne – które całkowicie zmieniają interfejsy. Podział ten stanowi wskazówkę, które z przekształceń i w jaki sposób wpływają na testy weryfikujące ich poprawność.

Rozwiązaniem może być rozszerzenie koncepcji szablonów testowych, zaproponowanych dla przekształceń o znanym zakresie testowania. Skoro wiadomo jakie warunki należy sprawdzić, wówczas można przygotować szablon opisujący poprawny przypadek testowy zarówno przed, jak i po dokonaniu zmiany. Każdy z testów wymaganych przez konkretne przekształcenie jest wówczas przechowywany w postaci szablonu, na podstawie którego można utworzyć plik z przypadkiem testowym. W zależności od parametrów szablonu, wynikowy test jest zgodny z programem testowanym przed lub po refaktoryzacji.

Przykład: Introduce Null Object#

Przekształcenie Introduce Null Object polega na zastąpieniu referencji pustych nullinstancjami specjalnej podklasy rozważanej klasy. Pozwala to uniknąć ciągłego sprawdzania, czy referencja wskazuje na właściwy obiekt oraz ograniczyć liczbę wyjątków klasy NullPointerException.Na przykład, metoda getSupervisor() klasy Student zwracająca referencję do obiektu klasy Pracownik, wskazuje na promotora pracy dyplomowej studenta. Przyjmuje ona wartość null w przypadku studentów, którzy jeszcze nie otrzymali karty pracy dyplomowej, oraz wskazuje na właściwego pracownika – w pozostałych przypadkach. Zarys metody getSupervisor() przed i po przekształceniu przedstawiono na rysunku 1.

Employee getSupervisor() {
   return Employee.getNullEmployee();
}


Employee getSupervisor() {
   return null;
}
Rys. 1. Szkic metody getSupervisor() przed i po zmianie

Przed zmianą metoda getSupervisor() może zwracać wartość null, po zmianie – referencję do obiektu podklasy NullEmployee. Testy, które przed zmianą sprawdzały, czy u studentów młodszych lat wartością tej metody jest null, po zmianie nie będą spełnione.Przykładowy szablon takiego testu sprawdza, czy zwracany przez metodę getSupervisor()obiekt reprezentuje wartość pustą: przed zmianą jest to referencja pusta, po zmianie – referencja do obiektu odpowiedniej podklasy. Parametrem szablonu jest właśnie reprezentacja wartości pustej: null i instancja klasy NullEmployee. Szablon generuje różne postaci tego samego testu, przedstawione na rysunku 2.

void testSupervisorIsNull()
   assertNull(student.getSupervisor());
}


void testSupervisorIsNull()
   assertEquals(student.getSupervisor(),
      Employee.getNullEmployee());
}
Rys. 2. Szkic metody testującej metodę getSupervisor() przed i po zmianie

W ten sposób szablon pozwala przekształcić istniejący przypadek testowy i dostosować go do wprowadzonych w kodzie zmian.

5 Podsumowanie#

Refaktoryzacja jest procesem trudnym i kosztownym, dlatego jego automatyzacja stanowi bardzo istotny problemem. Prowadzone wcześniej badania służyły określeniu, które z przekształceń można bezpiecznie stosować bez konieczności implementacji testów, wyłącznie poprzez analizę kodu programu i relacji zachodzących między elementami programu. Większość środowisk IDE stosuje wyłącznie analizę kodu, ponieważ pozwala ona stwierdzić poprawność przekształcenia bez konieczności uruchamiania kodu i testów.Jednak w przypadku wielu przekształceń analiza jest trudna lub w ogóle niemożliwa i wówczas testowanie okazuje się niezbędne. Zaproponowana klasyfikacja przekształceń refaktoryzacyjnych wskazuje, które przekształcenia można weryfikować analitycznie, a jakie poprzez testowanie. Ponadto wprowadzono kategorię przekształceń o znanym zakresie testowania – czyli posiadających uniwersalny zestaw testów sprawdzających ich poprawność. Pozwala to uprościć proces refaktoryzacji, zmniejszyć jego pracochłonność i ograniczyć liczbę potencjalnych błędów. Zaprezentowano także koncepcję szablonów testowych, które pozwolą generować testy refaktoryzacyjne zgodne z programem przed i po jego przekształceniu.

Dalsze kierunki prac obejmują implementację koncepcji szablonów dla wybranych przekształceń, a także dokładne zbadanie czynników wpływających na klasyfikację przekształceń.

Bibliografia#

[Beck2000] K. Beck, Extreme Programming explained. Embrace Change.,Addison-Wesley 2000.
[Deursen2001a] A. Deursen i A. Moonen, The Video Store Revisited – Thoughts on Refactoring and Testing, maj 2003, http://www.agilealliance.org/ .
[Deursen2001b] A. Van Deursen, A. Moonen, A. Van der Bergh i A. Kok, Refactoring Test Code, maj 2003, http://www.agilealliance.org/ .
[Eclipse2003] Eclipse, maj 2003, http://www.eclipse.org/ .
[Fowler1999] M. Fowler, Refactoring. Improvig design of existing code, Addison-Wesley 1999.
[Gossling2000] J Gossling, J Joy, J Steele i J Bracha, The Java Language Specification (Second Edition), Addison-Wesley 2000.
[Johnson1993] R Johnson i R Opdyke, Creating abstract superclasses by refactoring, In: The ACM 1993 Computer Science Conference (CSC’93) February 1993 66-73 .
[JUnit2000] JUnit, http://www.junit.org/.
[ÓCinnéide2000] M Ó Cinnéide, Automated Application of Design Patterns: A Refactoring Approach, Ph. D. thesis University of Dublin, Trinity College, Dublin 2000.
[Opdyke1992] W. Opdyke, Refactoring object-oriented frameworks, Ph.D. thesis University of Illinois at Urbana-Champaign, Urbana 1992.
[Roberts1996] D. B. Roberts, Practical analysis for refactoring, PhD thesis University of Illinois at Urbana-Champaign, Urbana 1992.
[Roberts1997] D. Roberts, D. Johnson i D. Brant, A refactoring tool for Smalltalk,Theory and Practice of Object Systems. 1997 3(4).
[Simon1999] F. Simon, F. Steinbruecker i F. Lewerentz, Metrics Based Refactoring, maj 2003 , http://www.agilealliance.org/ .
[Tokuda2001] L. Tokuda, L. Batory i L. Kluwer, Evolving object-oriented designs with refactorings, Journal of Automated Software Engineering August 2001 89-120 .
[Walter2003] B. Walter, Extending testability for automated refactoring, In: Proceedings of XP2003 Conference, Genova, 25-29.05.2003 in printing .

[#1] Praca współfinansowana z grantu BW/91-394/2003
[#2] Stypendysta Fundacji na rzecz Nauki Polskiej w roku 2003

©2015 e-Informatyka.pl, All rights reserved.

Built on WordPress Theme: Mediaphase Lite by ThemeFurnace.