powrót do spisu treści

Rozdział 4

Wsparcie refaktoryzacji w IBM WebSphere Studio Workbench

W tym rozdziale prezentujemy opis projektu i implementacji narzędzia do wspomagania procesu refaktoryzacji programów w języku Java. Obecnie jest ono integralną częścią środowiska IBM WebSphere Studio Workbench. Jest dostępne (wraz z kodem źródłowym pod adresem [24]).

Cała niniejsza praca jak i wspomniane narzędzie, którego opis stanowi jej istotną część, powstały podczas stażu autora w biurze firmy Object Technology International w Zurychu (1) pod kierownictwem dra Ericha Gammy. Projekt i implementacja narzędzia zostały wykonane przez Adama Kieżuna (ogólny projekt, wszystkie refaktoryzacje poza Wydzieleniem Metody, operacje na plikach, operacje na pakietach, infrastruktura zestawu testów, interfejs graficzny) oraz dra Dirka Baeumera (refaktoryzacja Wydzielenie Metody, operacje tekstowe, interfejs graficzny).
Integracja wsparcia refaktoryzacji ze środowiskiem programistycznym IBM WebSphere Studio Workbench została prez nas opisana w artykule [1].

Do tej pory (wersja R0.9 produktu) zaimplementowano część planowanych refaktoryzacji wraz z dużym zestawem testów towarzyszącym każdej z nich. Znaczną część pracy pochłonęło stworzenie elastycznej architektury, dzięki której, jak się spodziewamy, z czasem stosunkowo łatwo będzie dodawać kolejne refaktoryzacje. Nasze narzędzie było z powodzeniem stosowane do refaktoryzacji dużego projektu - IBM WebSphere Studio Workbench (ponad 500000 wierszy kodu źródłowego) - jest też na codzień używane przez samych autorów.

Program został w całości napisany w języku Java. Dało nam to możliwość użycia go do refaktoryzacji jego własnego kodu źródłowego - co z powodzeniem robimy podczas pracy nad kolejnymi udoskonaleniami.

IBM WebSphere Studio Workbench jest pierwszym dużym środowiskiem programistycznym zawierającym wsparcie dla refaktoryzacji. Począwszy od jednej z kolejnych wersji będzie dostępne bezpłatnie wraz z kodem źródłowym. Mamy nadzieję, że dzięki temu damy wielu programistom możliwość zapoznania się z nowym sposobem pracy, jaki umożliwia refaktoryzacja. Ponadto przez stworzenie elastycznej infrastruktury chcemy dać innym twórcom narzędzi możliwość łatwej implementacji nowych refaktoryzacji. Sądzimy, że otwartość środowiska umożliwi stworzenie lepszej jakości narzędzi do refaktoryzacji (i nie tylko).

4.1 Opis architektury środowiska programistycznego

Najważniejszymi częściami platformy, wykorzystywanymi w naszym programie (poza interfejsem graficznym) są: system zarządzania zasobami, kompilator Javy oraz wyszukiwarka (ang. search engine).

System zarządzania zasobami kontroluje dostęp do zasobów (w naszym przypadku plików i katalogów) i sposób ich modyfikacji. Innym bardzo istotnym jego zadaniem jest zarządzanie znacznikami (ang. markers). Znacznik to obiekt związany z zasobem. Każdy znacznik związany z plikiem ma atrybut określający jego pozycję w danym pliku. Przykładowe rodzaje znaczników to: punkty kontrolne (ang. breakpoints), zakładki (ang. bookmarks), wyniki wyszukiwań (ang. search results) oraz znaczniki oznaczające błędy kompilacji.

Najważniejszą cześcią kompilatora Javy, z której korzystamy w naszym programie, są drzewa składni (ang. abstract syntax trees, w skrócie AST). Używamy ich do analizy warunków wstępnych, a także, w kilku przypadkach, do wyszukiwania odniesień do elementów programu.

Wyszukiwarka jest, obok kompilatora i kilku innych komponentów, elementem modelu Javy (czyli tej części modułu wspomagającego programowanie w Javie, która jest pozbawiona interfejsu graficznego). Służy do efektywnego i precyzyjnego znajdowania deklaracji i odniesień do elementów programu, np. klas, metod itp.

4.2 Przyjęte założenia projektowe

Roberts stworzył listę warunków, jakie musi powinno spełniać każde narzędzie do refaktoryzacji [19]. Tokuda rozwinął tę listę, czerpiąc ze swoich doświadczeń związanych z refaktoryzacją dużych projektów [22].
Uzupełniliśmy stworzoną przez tych autorów listę o dodatkowe warunki jakie, naszym zdaniem, są niezbędne do stworzenia narzędzia o wysokiej niezawodności. Wymogi w niej podane przyjęliśmy za założenia projektowe naszego programu.

Najpierw przedstawimy warunki podane przez Robertsa i Tokudę.

Integracja ze środowiskiem programistycznym Wspomaganie refaktoryzacji musi być ściśle zintegrowane ze środowiskiem programistycznym. Przeprowadzenie refaktoryzacji powinno odbywać się tak jak każda inna czynność wykonywana przez użytkownika (najczęściej przez wybranie opcji z menu).

Możliwość wycofania zmian Musi istnieć możliwość sprawnego wycofania zmian wprowadzonych do programu przez refaktoryzację. Dopuszczalne jest przy tym, by dokonane zmiany można było wycofać tylko do pewnego momentu. Przykładowo, jeśli po przeprowadzeniu refaktoryzacji wewnątrz pliku z kodem źrodłowym usuniemy ten plik, to wycofanie refaktoryzacji może okazać się niemożliwe.

Niezawodność Zachowywanie działania programu powinno być tak pełne, jak to możliwe (biorąc pod uwagę zawiłą konstrukcję języka bardzo trudne byłoby zagwarantowanie pełnej niezmienności działania - nawet pod nieobecność metod natywnych i mechanizmu odbicia). Przyjęta przez nas definicja niezmienności działania programu znajduje się w punkcie 2.1. Dopuszczalne są jednak sytuacje, gdy z powodów wydajności pominięta zostanie część analizy warunków wstępnych. Musi to być jednak traktowane jako sytuacja wyjątkowa, a pominięte warunki wstępne muszą być spełnione przez typowe programy.

Formatowanie kodu Formatowanie kodu powinno zostać niezmienione, tzn. takie, jakie było przed refaktoryzacją. Jedynym wyjątkiem są sytuacje, gdy narzędzie generuje nowy kod. Także wtedy należy, w miarę możliwości, stosować poprzednie formatowanie kodu.

Komentarze Komentarze muszą pozostać nienaruszone tzn. takie, jakie były przed refaktoryzacją. W przyszłości nasze narzędzie będzie wspierać także modyfikacje tzw. komentarzy dokumentujących - w języku Java są to tzw. komentarze JavaDoc. Komentarze te traktuje się wówczas jako część kodu. Jednak w żadnym wypadku narzędzie nie może usuwać bądź zmieniać położenia istniejących komentarzy w kodzie programu.

Umieszczenie nowotworzonego kodu Należy dać użytkownikowi możność wyboru miejsca, w którym zostanie umieszczony kod wygenerowany podczas refaktoryzacji.

Kontrola dostępności modyfikowanych plików Narzędzie służące do refaktoryzacji musi wykrywać sytuacje, w których pewne objęte refaktoryzacją pliki są niedostępne do zapisu. Należy wówczas poinformować o tym fakcie użytkownika. Wykrycie takiej sytuacji musi być częścią sprawdzania warunków wstępnych, więc odbywa się przed dokonaniem jakichkolwiek zmian w programie.

Nazwy dla nowotworzonych bytów Każdorazowo gdy narzędzie do refaktoryzacji tworzy nowe elementy programu - klasy, metody itp., użytkownik musi mieć możliwość ustalenia dla nich nazw. Nazwa wybrana przez użytkownika może jednak powodować błędy (np. powtarzające się nazwy zmiennych) bądź wpływać na zmianę zachowania programu. Należy wówczas poinformować użytkownika o tym fakcie.

Następujące warunki zostały przez nas dodane do listy podanej przez Robertsa i Tokudę:

Pełna lista niespełnionych warunków wstępnych W miarę możliwości należy zaprezentować użytkownikowi wszystkie niespełnione warunki wstępne - w podobny sposób, jak robią to kompilatory informując o błędach kompilacji. Użytkownik może wówczas naprawić wiele błędów na raz bez konieczności ponownego uruchamiania narzędzia po wprowadzeniu każdej poprawki z osobna.

Pełna lista wprowadzanych zmian w programie Należy pokazać użytkownikowi wszystkie zmiany w programie, jakie są potrzebne do wykonania refaktoryzacji. Ponadto użytkownik musi mieć możliwość niezgodzenia się na dowolną z tych zmian (choć wtedy prawie na pewno program nie będzie działał tak, jak poprzednio). Również w tym przypadku opcja wycofania zmian musi działać bez zarzutu.

Konwencje nazw Nie możemy niczego zakładać o przestrzeganiu bądź nieprzestrzeganiu konwencji nazw (konwencje nazw są opisane w specyfikacji języka [JLS 6.8]).

Analiza warunków wstępnych Wszelka analiza wykonalności refaktoryzacji musi zostać przeprowadzona bez dokonywania zmian w programie. Wewnętrzne struktury kompilatora (jak drzewa składni) także muszą bezwzględnie pozostać nietknięte podczas tej fazy refaktoryzacji.

Transakcyjność operacji Modyfikacja kodu źródłowego programu powinna odbywać się w sposób transakcyjny. Niepożądane są sytuacje, w których narzędzie ma zmodyfikować wiele plików i, z powodu błędu w programie lub błędu zewnętrznego, modyfikuje tylko część i przerywa pracę. Ręczne przywrócenie systemu do pierwotnego stanu może okazać się wówczas bardzo pracochłonne (2).

Zaprezentowana lista wymagań jest niezależna od środowiska programistycznego w jakim powstaje rozważane narzędzie. Ostatnie założenie projektowe dotyczy środowiska, w którym zaimplementowany jest nasz program:

Nietykalność znaczników Znaczniki (opisane w punkcie 4.1.) powinny pozostać nienaruszone. Stanowią one bardzo istotną część środowiska programistycznego, w którym zaimplementowane jest nasze narzędzie. Jeśli to możliwe, powinny zostać uaktualnione (ich atrybuty), by odzwierciedlać nowy stan systemu.

4.3 Przepływ sterowania

W tym punkcie podamy typowy przepływ sterowania w naszym programie podczas wykonywania refaktoryzacji.

    1. Użytkownik wywołuje refaktoryzację przez wybranie opcji z menu i podanie niezbędnych informacji (np. nazwy dla nowej metody).
    2. Część refaktoryzacji uruchamia w tym momencie wyszukiwarkę w celu efektywnego odszukania odniesień do modyfikowanych elementów programu.
    3. Budowane są drzewa składni dla tych jednostek kompilacji, które będą modyfikowane (3).
    4. Sprawdzane są warunki wstępne refaktoryzacji.
    5. Jeżeli analiza wykryje niespełnione warunki wstępne, to użytkownik ma możliwość kontynuacji - mimo prawdopodobnych błędów, jakie pojawią się w programie. Uznaliśmy, że jest istotne, by nasze narzędzie nie przeszkadzało użytkownikowi w pracy. Jeżeli chce on wprowadzić zmiany do swego programu mimo ostrzeżeń, to ma taką możliwość.
    6. Obliczane są wszystkie zmiany, jakie należy wprowadzić do programu, by przeprowadzić refaktoryzację.
    7. Jeżeli użytkownik chce zobaczyć zmiany przez ich wprowadzeniem, to pełna ich lista jest prezentowana w interfejsie użytkownika.
    8. Narzędzie wprowadza zmiany do refaktoryzowanego programu.
    9. Na stos operacji do wycofania jest wkładana informacja o tym, w jaki sposób wycofać ostatnią przeprowadzoną refaktoryzację.
 

4.4 Wspomagane refaktoryzacje

Poniżej podajemy słowny opis refaktoryzacji wspomaganych przez nasz program. Pomijamy, ze względu na znaczną objętość, spis warunków wstępnych każdej z refaktoryzacji - przykłady podano w dodatku B.

4.4.1.Wydzielenie Metody

Tworzy metodę z pewnej części innej metody. Wydzielana część może być listą instrukcji lub wyrażeniem. Zamienia wydzielaną część na wywołanie nowej metody, która musi mieć odpowiednią sygnaturę, modyfikatory, deklaracje zgłaszanych wyjątków. Wiele przykładów zastosowania tej niezwykle użytecznej refaktoryzacji (wraz z opisem części trudności związanych z jej automatyzacją) znajduje się w [6].

4.4.2. Zmiana Nazwy Pakietu

Powoduje zmianę nazwy wybranego pakietu wraz z uaktualnieniem odniesień do tego pakietu i wszystkich typów w nim zdefiniowanych.

4.4.3. Zmiana Nazwy Typu

Powoduje zmianę nazwy wybranego typu (zdefiniowanego nielokalnie, czyli nie we wnętrzu metody) wraz z uaktualnieniem wszystkich odniesień do tego typu oraz typów w nim zagnieżdżonych. Jeżeli wybrany typ jest niezagnieżdżony i ma tę samą nazwę co jednostka kompilacji, w której się znajduje, to również nazwa tej jednostki kompilacji jest zmieniana. Jest to konieczne w wypadku typów publicznych (wymóg specyfikacji języka) i zalecane w wypadku typów widocznych w obrębie pakietu.

4.4.4. Zmiana Nazwy Metody

Powoduje zmianę nazwy metody i aktualizację wszystkich odniesień do niej. W przypadku metod niewirtualnych zmieniana jest nazwa tylko jednej metody. Przy metodach wirtualnych zmieniana jest nazwa wybranej metody i wszystkich metod w hierarchii (począwszy od maksymalnie abstrakcyjnego typu deklarujacego tę metodę), które ją zastępują. W przypadku metod zdefiniowanych w interfejsach używa się algorytmu znajdowania metod, których nazwy muszą zostać zmienione wraz z wybraną. Algortym ten jest podany w punkcie 3.1.

4.4.5. Zmiana Nazwy Pola

Powoduje zmianę nazwy pola wraz z aktualizacją wszystkich odniesień do niego.

4.4.6. Zmiana Nazw Parametrów Metody

Powoduje zmianę nazwy jednego lub więcej parametrów wybranej metody (lub konstruktora) wraz z aktualizacją wszystkich odniesień.

4.4.7 Zmiana Nazwy Jednostki Kompilacji

Powoduje zmianę nazwy wybranej jednostki kompilacji. Jeżeli jest w niej zdefiniowany niezagnieżdżony typ o tej samej nazwie, co nazwa tej jednostki, to refaktoryzacja ta ma ten sam skutek, co opisana wcześniej Zmiana Nazwy Typu. W przeciwnym przypadku zmieniana jest jedynie nazwa samej jednostki kompilacji.

4.4.8. Przemieszczenie Jednostki Kompilacji do Innego Pakietu

Powoduje przemieszczenie jednostki kompilacji wraz z aktualizacją wszystkich odniesień do publicznego typu (jeżeli istnieje), który jest w niej zadeklarowany.

4.5 Główne części programu

Nasze narzędzie składa się z czterech głównych części: klasy Refactoring, klasy Change, analizatora drzew składni i interfejsu użytkownika.

4.5.1. Klasa Refactoring

Każdej refaktoryzacji odpowiada jedna klasa będąca podklasą abstrakcyjnej klasy Refactoring i implementujaca dwie metody: checkPreconditions oraz createChange. Metoda checkPreconditions jest odpowiedzialna za sprawdzenie, czy spełnione są warunki wstępne dla danej refaktoryzacji, natomiast metoda createChange buduje i przekazuje obiekt klasy Change (opisanej w punkcie 4.5.2.), który jest odpowiedzialny za przeprowadzenie zmian w programie. Metoda checkPreconditions przekazuje obiekt opisujący wynik sprawdzenia warunków wstępnych i zbierający wszystkie te, które nie zostały spełnione. Wszelkie możliwe wyniki podzielone są na cztery kategorie, podane tu w rosnącej kolejności ważności:

Informacje (ang. infos) nie oznaczają problemów. Użytkownicy mogą bezpiecznie ignorować komunikaty z tej kategorii. Służą one jedynie jako forma komunikacji programu z użytkownikiem.

Ostrzeżenia (ang. warnings) zgłaszane są np. wtedy, gdy przeprowadzenie refaktoryzacji spowoduje pojawienie się ostrzeżeń kompilatora lub w inny negatywny lecz nie destruktywny sposób wpłynie na modyfikowany program.

Przykładowy komunikat z tej grupy pojawia się wówczas, gdy użytkownik chce zmienić nazwę metody main lub klasy zawierającej taką metodę. Programu, który modyfikujemy nie będzie już można wywoływać z wiersza poleceń. Przestaną też działać skrypty go uruchamiające. Nie prowadzi to do błędów kompilacji. Należy jednak poinformować użytkownika o możliwości wystąpienia problemów i przypomnieć o konieczności aktualizacji skryptów itp.

Błędy (ang. errors) oznaczają, że można przeprowadzić modyfikację programu, ale prawie na pewno spowoduje to wystąpienie błędów kompilacji bądź, co gorsza, zmianę działania programu bez wystąpienia błędu kompilacji.

Przykładowo zmiana nazwy metody natywnej na pewno spowoduje przerwanie wykonania programu z błędem UnsatisfiedLinkError przy pierwszym wywołaniu tej metody - jeśli nie zostaną dokonane zmiany w kodzie natywnym. Nasz interfejs użytkownika zezwala na przeprowadzenie modyfikacji, gdy występują jedynie ostrzeżenia. Są one prezentowane użytkownikowi w postaci listy, z opisem i miejscem wystąpienia. Użytkownik może zrezygnować z przeprowadzenia modyfikacji programu bądź mimo wszystko jej dokonać. W takim przypadku, jeśli po przeprowadzeniu modyfikacji (i zapoznaniu się z błędami kompilacji, które ona wówczas zwykle powoduje) zdecyduje sie wycofać zmiany, to może tego dokonać.

Poważne Błędy (ang. stop errors) są zgłaszane w sytuacjach, gdy nie można przeprowadzić refaktoryzacji.
Przykładowo, użytkownik wybrał dla publicznej, niezagnieżdżonej klasy nazwę, która nie może być nazwą klasy i pliku bądź plik o tej nazwie już istnieje. Zgodnie ze specyfikacją języka takie klasy muszą znajdować się w pliku o nazwie identycznej z nazwą klasy, a system operacyjny nie zezwoli na stworzenie drugiego pliku o tej samej nazwie. Podobnie błąd zgłaszany jest wówczas, gdy podczas wykonywania refaktoryzacji musiałby zostać zmodyfikowany plik, do którego nie ma dostępu (np. jest otwarty tylko do odczytu).

Interfejs użytkownika prezentuje wszystkie te komunikaty (jeśli występują) i zezwala na wykonanie modyfikacji tylko wówczas, gdy nie ma wśrod nich Poważnych Błędów. Jednak, jak wspomniano poprzednio, w przypadku występowania Błędów oraz Ostrzeżeń program, po wykonaniu modyfikacji, najprawdopodobniej przestanie działać (wystąpią błędy kompilacji bądź błędy wykonania) lub jego zachowanie będzie zmienione. Metodę createChange wywołuje się po sprawdzeniu warunków wstępnych. Buduje ona obiekt klasy Change (opisanej w punkcie 4.5.2.), który następnie będzie odpowiedzialny za modyfikację kodu źródłowego programu (samo wywołanie metody createChange nie dokonuje żadnych zmian w programie).

4.5.2. Klasa Change

Wszystkie refaktoryzacje są zaimplementowane przy użyciu obiektów klasy Change. Są one przykładami wzorca Command [7] i odpowiadają za modyfikację kodu źródłowego. Wszystkie obiekty klasy Change muszą implementować metodę perform, która przeprowadza modyfikację kodu źródłowego i przekazuje jako wynik obiekt klasy Change, służący do przeprowadzenia modyfikacji odwrotnej.

Do łączenia wielu zmian razem używamy wzorca Composite [7]. Podobnie jak w programie Robertsa, również nasze zmiany są odwracalne, zarówno pojedynczo jak i razem, jako złożenie (odwrócenie sekwencji zmian polega na odwróceniu zmian składowych w odwrotnej kolejności). To czyni je bardzo efektywnym i elastycznym narzędziem. Każda, dowolnie skomplikowana, modyfikacja kodu źródłowego jest wykonywana jako ciąg zmian, z których każda potrafi odwrócić efekt swego działania.

Dodatkowo, każda zmiana może być aktywna lub nieaktywna. Zazwyczaj jest to konsekwencją tego, że użytkownik zdecydował, iż pewne zmiany są niepożądane i nie zgodził się na ich przeprowadzenie (jak wspomnieliśmy, programista ma możliwość zobaczenia wszystkich zmian, jakie narzędzie ma zamiar dokonać i niezgodzenia się na dowolną z nich). Taka zmiana staje się wówczas nieaktywna, co oznacza, że wywołanie jej metody perform nie ma żadnego skutku, a przekazywany jest obiekt klasy NullChange (przykład wzorca NullObject [6]). Dezaktywacja zmian prawie zawsze prowadzi do błędów kompilacji (ponieważ narzędzie sugeruje wprowadzenie zmiany, która jest potrzebna do przeprowadzenia refaktoryzacji, a na którą użytkownik się nie zgadza), musi zatem zawsze zostać dokonana na polecenie użytkownika - wszystkie zmiany są zawsze domyślnie aktywne. Mechanizm wycofywania radzi sobie z wycofaniem dowolnej kombinacji aktywnych i nieaktywnych zmian.

Wszystkie refaktoryzacje zostały zaimplementowane przy użyciu niewielkiej liczby podklas klasy Change. Poniżej podajemy ich spis:

Modyfikacje tekstowe oraz operacje na plikach są niezależne od języka Java - mogą służyć do manipulowania dowolnymi plikami tekstowymi.

4.5.3. Analizator drzew składni

Do analizy wielu warunków wstępnych refaktoryzacji używamy drzew składni. Każdy węzeł takiego drzewa odpowiada pewnemu elementowi programu. Analizator jest przykładem wzorca Visitor [7] - na początku analizy każdego węzła wywoływana jest metoda visit, na końcu zaś endVisit. Odmiennie niż Roberts [19] zdecydowaliśmy się nie używać drzew składni do modyfikacji programu - dokonujemy tego bezpośrednio manipulując zawartościa plików z kodem źródłowym. Język Java ma znacznie bardziej rozbudowaną składnię niż Smalltalk, (którym zajmował się Roberts), wobec czego drzewa składni mają bardziej skomplikowaną budowę. Ponadto drzewa składni, użyte w kompilatorze, którym dysponujemy, nie niosą pełnej informacji o programie - zawierają jedynie te dane, które są niezbędne do analizy typów, generacji kodu wynikowego i tworzenia komunikatów o błędach kompilacji. Informacje o komentarzach, formatowaniu kodu, pustych blokach itp. są nieobecne.

Ze względu na wydajność nie cały program znajduje się jednocześnie w pamięci, a drzewa składni tworzone są jedynie dla tych jednostek kompilacji, dla których tego jawnie zażądamy. Jest to kosztowna operacja (trzeba wczytać jednostkę kompilacji i dokonać jej analizy składniowej; sprawdza się także typy wszystkich odwołań i wyrażeń). Wskazane jest wobec tego, by analiza oparta na drzewach składni ograniczała się do możliwie małej liczby jednostek kompilacji.

4.5.4. Interfejs użytkownika

Przy projektowaniu interfejsu użytkownika staraliśmy się wziąć pod uwagę oczekiwania użytkowników mniej doświadczonych, których podstawowym wymaganiem jest niezawodność programu i prostota jego obsługi, a także bardziej wymagających, dla których istotna jest możliwość kontrolowania działania programu i dopasowania go do własnych potrzeb
.
Zdecydowaliśmy się na użycie asystentów (ang. wizards) - szeroko stosowanego rozwiązania polegającego na prowadzeniu użytkownika przez pewien proces (na niektórych etapach wymagający jego interwencji). W naszym programie pierwszy etap polega na pobraniu od użytkownika informacji niezbędnych do przeprowadzenia refaktoryzacji, następnie sprawdzane są warunki wstępne i prezentowana lista tych, które nie są spełnione. Kolejny etap to prezentacja wszystkich zmian, które muszą zostać wprowadzone do programu w celu przeprowadzenia refaktoryzacji. Zmiany są prezentowane w specjalnym oknie. Użytkownik widzi, dla każdej zmiany osobno, obecny stan systemu i ten, w którym system się znajdzie po przeprowadzeniu refaktoryzacji. Dla zmian złożonych jest to złożenie efektów zmian składowych. Na tym etapie użytkownik może nie zgodzić się na dowolną z proponowanych zmian - nie zostanie ona wówczas wprowadzona do programu.

4.5.5. Wydajność i niezawodność narzędzia

Środowisko IBM WebSphere Studio Workbench jest napisane w języku Java i tworzone przy użyciu samego siebie (4). Umożliwia nam to testowanie wydajności i niezawodności naszego narzędzia na programie bardzo dużej wielkości (ponad 500000 wierszy kodu źródłowego). Nasze doświadczenia wskazują jednoznacznie, że efektywna refaktoryzacja nawet dużych programów, obejmująca rozległą analizę warunków wstępnych, jest możliwa.

Podczas pracy i testowania nie wykryliśmy problemów ze skalowalnością. Jest to, po części, zasługa bardzo wydajnej infrastruktury, jaką dysponujemy (szybki kompilator oraz wyszukiwarka). W pamięci przechowywane są jedynie niezbędne informacje. Nasze obserwacje (niepoparte szczegółową analizą) wskazują, że czas działania refaktoryzacji nie zależy w zauważalny sposób od wielkości programu, lecz jedynie od liczby plików, których dotyczy refaktoryzacja.

Wydajność narzędzia pokażemy na przykładach refaktoryzacji Zmiana Nazwy Pakietu oraz Zmiana Nazwy Typu. Wykonanie refaktoryzacji Zmiana Nazwy Pakietu dla pakietu, do którego istnieje ok. 100 odniesień (5) w całym programie trwa ok. 15-17 sekund (komputer 500MHz CPU, 190MB RAM). Z tego ok. 70\% zajmuje wyszukanie odniesień, analiza warunków wstępnych oraz obliczenie modyfikacji programu niezbędnych do przeprowadzenia refaktoryzacji. Pozostały czas zabiera przeprowadzenie obliczonych zmian w programie i aktualizacja stanu środowiska programistycznego. Na wycofanie tej refaktoryzacji potrzeba tylko ok. 5 sekund - ponieważ nie wymaga to wyszukiwania odniesień ani analizy programu.

Na przeprowadzenie refaktoryzacji Zmiana Nazwy Typu dla często używanej klasy, do której istnieje ok. 500 odniesień potrzeba ok. 40 sekund (30 sekund wyszukiwanie odniesień, analiza warunków wstępnych oraz obliczenie koniecznych modyfikacji programu, 10 sekund wprowadzenie zmian i aktualizacja stanu środowiska). Wycofanie tej refaktoryzacji trwa ok. 7-8 sekund.

Z podanych liczb wynika, że możliwa jest efektywna refaktoryzacja nawet dużych programów, obejmująca całość języka programowania i szczegółową analizę warunków wstępnych. Pokazują one także jak wiele czasu (potrzebnego na identyfikację niezbędnych zmian w programie, ich wprowadzenie i przetestowanie) można zaoszczędzić przeprowadzając refaktoryzację z użyciem wydajnych narzędzi.

Wysoką niezawodność narzędzia zapewniono dzięki używaniu programu na codzień przez wielu programistów (także autorów) oraz zestawowi zautomatyzowanych testów, obejmującemu ok. 100 szczegółowych przypadków wykonania każdej z refaktoryzacji. Podczas pracy nad narzędziem wspomniany zestaw testów przeprowadzano po wprowadzeniu każdej istotnej zmiany do programu - co najmniej kilka razy w tygodniu. Dla każdego wykrytego w programie błędu tworzono przypadek użycia pokazujący jego wystąpienie. Błąd uznawano za naprawiony dopiero wtedy, gdy pokazujący go przypadek użycia oraz wszystkie pozostałe przypadki zawarte w zestawie testów dawały wyniki zgodne z oczekiwaniami. Metoda ta pozwoliła na wyeliminowanie w znacznym stopniu ponownego pojawiania się naprawionych wcześniej błędów. Celem części testów było sprawdzenie zachowania narzędzia w sytuacjach, gdy jeden lub więcej warunków wstępnych refaktoryzacji nie był spełniony. Inne testy służyły do sprawdzenia, czy zmiany wprowadzane w refaktoryzowanym programie były zgodne z przewidywaniami (porównywano w nich, znak po znaku, oczekiwaną zawartość plików z kodem źródłowym z tym, co faktycznie było efektem refaktoryzacji programu.).



(1) Kod źródłowy programu stanowi własność firmy Object Technology International Inc. z siedzibą w Ottawie, 2670 Queensview Drive, Ottawa, Ontario, K2B 8K1, Kanada.
(2) Transakcyjność operacji nie została jeszcze zrealizowana w naszym programie.
(3) Jest to przybliżenie. Niektóre refaktoryzacje wymagają analizy większego, a niektóre mniejszego zbioru jednostek kompilacji.
(4) Pracując przy tworzeniu IBM WebSphere Studio Workbench używamy tego środowiska programistycznego jako narzędzia pracy.
(5) Odniesieniem do pakietu jest deklaracja importu (pojedynczego typu lub na żądanie) typu zdefiniowanego w tym pakiecie lub w pełni kwalifikowane odniesienie do takiego typu.