e-Informatica Software Engineering Journal Automatyczne wykrywanie duplikatów w kodzie programów

Automatyczne wykrywanie duplikatów w kodzie programów

Błażej Pietrzak*,  Jerzy R. Nawrocki**,  Bartosz Walter**[2]
*Instytut Informatyki,  Politechnika Poznańska i TiP Spółka z o.o.
**Instytut Informatyki,  Politechnika Poznańska
blazej.pietrzak@tip.net.pl  jerzy.nawrocki@ put.poznan.pl  bartosz.walter@cs.put.poznan.pl
Streszczenie

Jedną z głównych praktyk Programowania Ekstremalnego jest refaktoryzacja kodu, czyli zmiana kodu bez zmiany jego zachowania. Refaktoryzacja umożliwia bezpieczniejsze wprowadzanie zmian do pielęgnowanego oprogramowania. W literaturze opublikowano ponad 70 przekształceń refaktoryzacyjnych. Każde z nich ma postać warunku i akcji, jaką warto wykonać, jeśli ten warunek jest spełniony. Warunki nazywane są w literaturze “przykrymi zapachami”. Można je wykrywać “ręcznie”, ale im większe jest oprogramowanie, tym trudniejsze staje się wykrywanie przykrych zapachów.W niniejszym rozdziale przedstawiono koncepcję narzędzia XSmells służącego do automatycznego wykrywania przykrych zapachów w kodzie programów i szczegółowo omówiono implementację XSmells na przykładzie wykrywania zduplikowanego kodu. Obecna implementacja XSmells jest ukierunkowana na popularne środowisko programistyczne Eclipse.

1. Wprowadzenie#

Pielęgnacja oprogramowania jest jednym z najważniejszych zagadnień inżynierii oprogramowania. Opiera się ona na modyfikacji kodu. Kod programu trzeba modyfikować, aby dostosować go do zmieniających się wymagań, bądź też usunąć zauważone defekty. Z drugiej strony, modyfikowanie kodu jest bardzo niebezpieczne, gdyż można przy okazji wprowadzić błędy.Aby uczynić modyfikację kodu bardziej bezpieczną, Kent Beck, Martin Fowler i ich koledzy wymyślili technikę zwaną refaktoryzacją [Fowl99]. Opiera się ona na dwóch spostrzeżeniach:

  • Modyfikowany kod po pewnym czasie staje się nieczytelny i zagmatwany. Trudno zrozumieć sposób jego działania. W tej sytuacji kolejna “tradycyjna” jego modyfikacja jest bardzo niebezpieczna. Po bliższym przyjrzeniu się (i zrozumieniu kodu) programista dochodzi do wniosku, że dany fragment kodu można napisać prościej i bardziej elegancko. Pozostaje jednak zawsze pewna wątpliwość, czy dany fragment kodu został dobrze rozumiany i, czy to prostsze i elegantsze rozwiązanie, nie okaże się błędne.
  • Testowanie oprogramowania jest jedną z najstarszych technik zapewniania jego jakości. Takie metodyki jak Programowanie Ekstremalne kładą duży nacisk na automatyczne testy regresyjne. Jeśli programista ma do dyspozycji takie testy, to może łatwo i w krótkim czasie sprawdzić, czy “prostsze i elegantsze” rozwiązanie, które przyszło mu do głowy, jest dobre, czy nie.

Refaktoryzacja jest sposobem na bezpieczniejsze modyfikowanie oprogramowania. Zakłada się, że wraz z kodem powstają zautomatyzowane testy jednostkowe (do automatyzacji wykonania testów można wykorzystać np. narzędzia xUnit [xUnit94]). Przed dostosowaniem kodu do zmienionych wymagań funkcjonalnych usuwa się z niego tzw. przykre zapachy, jak np. zduplikowany kod, nadmiernie długie metody itp. Aby przekonać się, że usunięcie przykrych zapachów nie spowodowało zmian w zachowaniu systemu, wykonuje się automatycznie wszystkie testy jednostkowe. Jeśli wszystkie testy jednostkowe zostały wykonane poprawnie, to można wierzyć, że usunięcie przykrych zapachów zostało dobrze wykonane. Po usunięciu przykrych zapachów kod jest bardziej czytelny i teraz dostosowanie go do zmienionych wymagań powinno być łatwiejsze.

Zduplikowany kod to jeden z najważniejszych przykrych zapachów. Badania dla dużych projektów sugerują, że około 5-10% kodu źródłowego to duplikaty [Baxter95]. Wykrywanie przykrych zapachów w kodzie, takich jak zduplikowany kod, jest tym trudniejsze, im dłuższy jest kod programu. Celem pracy jest zaproponowanie narzędziaXSmells służącego do automatycznego wykrywania przykrych zapachów. XSmells bazuje na analizie składniowej kodu i wybranych metrykach. Został zaimplementowany w postaci wtyczki (ang. plug-in) do popularnego środowiska programistycznego Eclipserozwijanego pod auspicjami IBM [Eclipse03] (Eclipse posiada m.in. zintegrowane narzędzia do automatycznego testowania kodu oraz narzędzia wspomagające refaktoryzację. Środowisko to wyróżnia się dużą liczbą dostępnych przekształceń refaktoryzacyjnych).

Podrozdział 2. stanowi wprowadzenie do problematyki refaktoryzacji. Podrozdział 3. omawia „przykry zapach”, jakim jest zduplikowany kod. W podrozdziale 4. podano sposób wykrywania tego zapachu. Podrozdział 5. zawiera informacje o sposobie implementacji tego rozwiązania. Analizę wyników uzyskanych w praktycznym jego zastosowaniu znaleźć można w podrozdziale 6.

/%

Spis treści

2. Refaktoryzacja#

Refaktoryzacja (ang. refactoring) to zmiana wewnętrznej struktury kodu źródłowego, bez zmiany jego zachowania, poprawiająca jego strukturę [Fowl99 str. XVI]. Innymi słowy, refaktoryzacja to poprawa projektu kodu po jego napisaniu.

Objawem złej jakości kodu, który sygnalizuje konieczność refaktoryzacji, jest „przykry zapach” (ang. bad smell). Wśród przykładów przykrych zapachów podanych przez Fowlera znajdują się m.in. długie, trudne do zrozumienia metody, duplikaty w kodzie, klasy o za dużej funkcjonalności i inne. Autorem określenia „bad smell” jest Kent Beck, a nawiązuje ono do strategii zmiany pieluch u niemowląt: “when it smells, change it”.

2.1. Po co refaktoryzować?#

Refaktoryzacja, mimo że jest kosztownym procesem, przynosi także istotne korzyści.

Przede wszystkim, refaktoryzacja ułatwia dokonywanie modyfikacji w kodzie.W typowym cyklu wytwarzania oprogramowania przyjęło się, że najpierw powstaje projekt, dopiero potem kod. Z czasem kod jest modyfikowany, a struktura i spójność systemu w stosunku do projektu stopniowo zanika. Wprowadzanie zmian do systemu staje się coraz trudniejsze, a z czasem niemożliwe.

Ponadto, źle zaprojektowany program zawiera zwykle więcej linii kodu źródłowego. Zmniejszając jego rozmiar znacznie ułatwia się jego modyfikacje, ponieważ zmniejsza się rozmiar wiedzy niezbędnej do przyswojenia, a także kod staje się łatwiejszy do zrozumienia, ponieważ jest lepiej zaprojektowany.

Refaktoryzacja ułatwia także znajdywanie błędów. Przeglądając, najczęściej cudzy kod, dokonuje się pewnych założeń na temat jego działania. Zwiększenie czytelności kodu powoduje, że, pewne założenia są odrzucane, co uwidacznia błędy w programie.

Refaktoryzacja umożliwia szybsze tworzenie oprogramowania. Bez dobrego projektu postęp prac początkowo jest szybszy, jednak potem maleje, ponieważ wprowadzenie zmiany jest coraz kosztowniejsze.

Można zatem stwierdzić, że refaktoryzacja poprawia jakość oprogramowania poprzez poprawę projektu, czytelności kodu, a także zmniejsza liczbę błędów. /%

2.2. Proces refaktoryzacji#

Istnieje wiele przekształceń refaktoryzacyjnych. Każde wprowadza jeden rodzaj zmiany i jest  stosowane przy innych warunkach początkowych. Każde przekształcenie składa się z szeregu prostych, dobrze zdefiniowanych, małych zmian, których poprawność można łatwo zweryfikować.W procesie refaktoryzacji czynnikiem o krytycznym znaczeniu jest poprawność wprowadzonych modyfikacji. Po wykonaniu kroku należącego do danego przekształcenia sprawdzana jest jego poprawność, czyli czy nie wprowadziło ono zmian funkcjonalnych do zmienianego kodu. Można tego dokonać na dwa sposoby:

  • poprzez analizę statyczną kodu oraz weryfikację warunków wstępnych oraz końcowych poprawności przekształcenia.
  • poprzez testowanie regresyjne wprowadzonych zmian – przed przystąpieniem do zmiany tworzy się testy jednostkowe dla modyfikowanej części kodu.

3. Zduplikowany kod#

Jednym z przykładów przykrego zapachu, szczególnie często występującego w programach, jest duplikacja kodu.Większość powtórzeń w kodzie programów to efekt stosowania przez programistów zasady „kopiuj-wklej”. Z tego powodu w XSmells przyjęto następującą definicję:

Duplikat (ang. duplicated code, clone code) to fragment kodu, który jest efektem kopiowania i niewielkich modyfikacji nie powodujących zmiany zachowania tego kodu, np. przemianowania nazw zmiennych lub zmiany rodzaju pętli. Sposób działania zmodyfikowanego kodu pozostaje ten sam.

3.1. Dlaczego należy usuwać duplikaty?#

Zrozumienie kodu, w którym ma być dokonana modyfikacja, wymaga dużego wysiłku ze strony programisty. Usunięcie duplikatów zmniejsza rozmiar kodu potrzebny do przyswojenia, co ułatwia jego zrozumienie, a w konsekwencji ułatwia modyfikację kodu.Programista, który musi dodać lub zmodyfikować nową funkcję w systemie, zwykle woli napisać ją od nowa, niż analizować ogrom kodu napisany wcześniej przez inną osobę. Skopiowanie części kodu i jego niewielka modyfikacja wprawdzie umożliwia mu szybsze wytwarzanie, ale jednocześnie zwiększa znacznie koszty pielęgnacji oprogramowania. Dlatego usuwanie istniejących już duplikatów zmniejsza prawdopodobieństwo pojawienia się nowych. Ponadto usuwanie duplikatów zmniejsza liczbę błędóww programie, ponieważ usuwane są kopie błędów powstałe w wyniku powielania źle zaimplementowanej struktury kodu.

Badania dla dużych projektów sugerują, że około 5-10% kodu źródłowego to duplikaty [Baxter95]. Zatem usuwanie duplikatów zmniejsza koszty pielęgnacji oprogramo-wania. Niestety, znajdywanie zduplikowanego kodu to trudny i żmudny proces. Dla dużych projektów ręczne wyszukiwanie jest praktycznie niemożliwe, dlatego też konieczne jest wykorzystanie metod automatycznego znajdywania zduplikowanego kodu.

3.2. Strategia usuwania duplikatów#

Proces refaktoryzacji kodu, w tym także usuwania powtórzeń, jest kosztowny. Dlatego warto zastanowić się nad okolicznościami, kiedy go stosować. Ogólne zasady są takie same, jak w przypadku pozostałych przekształceń refaktoryzacyjnych [Fowl99].Duplikaty usuwa się gdy dodaje się funkcję do systemu. Najczęściej dodanie nowej funkcji wymusza modyfikację istniejącego kodu. Na ogół kod, który ma być zmodyfikowany, został napisany dawno temu, lub przez innego programistę i nie do końca jest jasne jak działa. Usunięcie duplikatów zmniejsza rozmiar kodu konieczny do poznania ułatwiając tym samym jego modyfikacje.

Duplikaty usuwa się kiedy trzeba poprawić błąd w systemie. Często fragment kodu, w którym znaleziono błąd jest powielony w wielu miejscach w programie.

Duplikaty usuwa się podczas przeprowadzania przeglądów kodu. Przy przeglądzie cudzego kodu sprawdza się czy dany pomysł łatwo jest zaimplementować, oraz czy łatwo jest zaimplementować po przeprowadzeniu refaktoryzacji. Jeśli tak kod jest refaktory-zowany. Jednym z elementów procesu refaktoryzacji jest usunięcie duplikatów. Duplikaty usuwa się również, gdy środowisko, w którym działa system, ma ograniczenia co do rozmiaru kodu i istnieje ryzyko, że program może ich nie spełnić.

Poza wskazówkami, kiedy należy usuwać duplikaty, istnieją również okoliczności, gdy należy tego unikać.

Nie należy usuwać duplikatów w kodzie kiedy trzeba napisać go od nowa. Decyzję taką należy podjąć gdy łatwiej jest napisać kod od nowa niż zmienić jego strukturę. Inną przyczyną jest bliski termin zakończenia. Wówczas kopiowanie kodu umożliwia szybsze wytwarzanie, ale jednocześnie zwiększa znacznie koszty pielęgnacji oprogramowania w przyszłości.

4. Wykrywanie duplikatów#

Istnieje kilka podejść do wykrywania duplikatów w kodzie. Najprostsze z nich to porów-nywanie łańcuchów znaków. Rozwiązanie to ma tą zaletę, że jest niezależne od języka programowania. Jednak jego skuteczność jest niewielka, choćby ze względu na możli-wość istnienia różnych nazw zmiennych. Są też rozwiązania, bazujące na niewielkiej informacji o języku programowania, które usuwają komentarze i/lub zamieniają nazwy zmiennych na jeden ustalony format, ale i one umożliwiają tylko znalezienie duplikatów powstałych przez skopiowanie i wklejenie fragmentu kodu. Te fragmenty, w których dokonano nawet niewielkich modyfikacji, nie mających zasadniczego wpływu na działanie programu, nie zostaną wykryte przy użyciu tej metody. Przykładem rozwiązania stosującego takie podejście jest MatchLoc [MatchLoc01].Innym rozwiązaniem jest zbudowanie drzewa składniowego fragmentu kodu (ang. syntax tree) dla danego języka programowania. Następnie tak skonstruowane drzewa są ze sobą porównywane, na przykład poprzez odpowiednie metryki. Sposób działania takiego detektora przedstawiono na rysunku 1. Na podstawie kodu źródłowego budowane jest drzewo składniowe. Reprezentuje ono strukturę funkcjonalną kodu i nie posiada elementów niefunkcjonalnych takich jak komentarze. Następnie, podczas przejścia po drzewie, dla każdego fragmentu kodu wyliczane są metryki opisujące dany fragment. Dla każdej pary fragmentów kodu, w których mogły wystąpić powtórzenia następuje porównanie. Fragmenty uznane za podobne oznaczane są jako duplikaty.

fig1.png

Rysunek 1. Zasada działania detektora opartego na analizie drzew składniowych [Rslbrghe02]

W programie XSmells jako bazową metodę wykrywania duplikatów wykorzystano me-tryki zaproponowane przez zespół K. Kontogiannisa [Kntgs96]. Zaproponowali oni metryki  służące do znajdowania fragmentów kodu implementujących ten sam algorytm (ang. programming concept), które są niezależne od języka programowania. Za fragment przyjęli wydzielone bloki kodu typu begin-end. Dla każdego takiego bloku wyznaczane są wektory zbudowane z wartości metryk, nazywane „odciskami palców” (ang. fingerprint). Następnie wektory z porównywanych bloków są ze sobą porównywane.W XSmells jako podstawowy fragment kodu poddawany analizie przyjęto pojedynczą metodę. Na podstawie wyników zawartych w  [Rslbrghe02] zrezygnowano z wykrywania bloków begin-end, ponieważ zawierają one za mało informacji, powodując znaczny wzrost fałszywych trafień.

Dla każdej analizowanej metody wyznaczany jest wektor metryk. Dwie metody są uznawane za podobne, jeśli ich odległość w przestrzeni Euklidesowej jest nie większa niż Δd, gdzie Δd jest pewną konfigurowalną wartością graniczną. W analizie przyjęto Δd = 0.

4.1. Metryki Kontogiannisa#

Metryki zaproponowane przez zespół Kontogiannisa to:Złożoność strukturalna (ang. Structural Complexity)

fig2.png

gdzie fanOut(fragment) to liczba odmiennych wywołań metod (ang. distinct function calls) w ciele analizowanego fragmentu.

Złożoność danych (ang. Data Complexity)

fig3.png

gdzie:

  • globals(fragment) to liczba deklaracji użytych lub zmodyfikowanych globalnych zmiennych w danej metodzie. Globalna zmienna to zmienna, która nie jest zadeklarowana w ciele analizowanego fragmentu.
  • fanOut(fragment) to liczba odmiennych wywołań metod (ang. distinct function calls) w ciele analizowanego fragmentu.

Złożoność cyklomatyczna McCabe’a

fig4.png

gdzie:

  • e to liczba krawędzi w grafie kontroli przepływu (ang. control flow graph) analizowanego fragmentu. Jedna krawędź wychodząca od wyrażenia reprezentuje przepływ, natomiast dwie krawędzie – decyzję.
  • n to liczba wierzchołków w tym grafie. Wierzchołki reprezentują wyrażenia.

Zmodyfikowana metoda punktów funkcyjnych Albrechta

fig5.png

gdzie:

  • globals(fragment) to liczba deklaracji użytych lub zmodyfikowanych globalnych zmiennych w danej metodzie. Globalna zmienna to zmienna, która nie jest zadeklarowana w ciele analizowanego fragmentu.
  • globalsUpdated(fragment) to liczba uaktualnionych pojedynczych deklaracji globalnych zmiennych w ramach analizowanego fragmentu.
  • ParmsByRefUpdated(fragment) to liczba zmiennych typu wskaźnikowego na liście parametrów metody, która jest uaktualniana w analizowanym fragmencie.
  • userInput(fragment) to liczba wyrażeń wejściowych w analizowanym fragmencie.
  • fileInput(fragment) to liczba wyrażeń otwierających pliki w analizowanym fragmencie.

Kontogiannis sugeruje przyjęcie następujących wartości wag:

p1 = 4; p2 = 5; p3 = 4; p4 = 7.

Zmodyfikowana metryka information quality metric Henry-Kafura

fig6.png

gdzie:

  • kafuraIn(fragment) to suma liczby parametrów formalnych, zmiennych użytych, odwołań do metody, do której należy analizowany fragment;
  • kafuraOut(fragment) to suma liczby funkcji wywoływanych, pojedynczych deklaracji zmiennych globalnych, aktualizowanych w analizowanym fragmencie, liczby zmiennych typu wskaźnikowego na liście parametrów formalnych metody, do której należy analizowany fragment, takich, które są aktualizowane w tym fragmencie.

4.2. Dodatkowe metryki proponowane w XSmells#

Wstępna analiza wskazała jednak, że dla pewnych przykładów metryki Kontogiannisa mogą dawać fałszywe dopasowania, czyli uznają za duplikat kod, który nim nie jest, bądź nie wykrywają rzeczywistych powtórzeń. Aby zredukować liczbę nieprawidłowych dopasowań do zestawu metryk dodano trzy nowe metryki:

  • Liczba instrukcji pętli w metodzie (NOLOOP)
  • Liczba instrukcji powrotu z metody (NORET)
  • Rozmiar podzbioru wywoływanych metod (CARDMC)

5. Implementacja#

Jako język programowania, w którym zaimplementowano XSmells wybrano Javę, ze względu na jego dużą popularność oraz stosunkowo prostą składnię w porównaniu z innymi językami obiektowymi. Duża popularność tego języka daje możliwość praktycznego sprawdzenia przydatności tego narzędzia. Dobrze zdefiniowana składnia ułatwia także konstrukcję parsera kodu, choć w przypadku XSmells wykorzystano już istniejącą implementację. XSmells stanowi wtyczkę (ang. plug-in) do środowiskaEclipse. Z tego też powodu do parsowania kodu i budowy abstrakcyjnego drzewa składniowego wykorzystano bibliotekę Java development tooling (JDT) wchodzącą w skład tego środowiska.

5.1. Metryki Kontogiannisa#

Ponieważ metryki zaproponowane przez Kontogiannisa są niezależne od języka programowania, dlatego też kluczowa jest ich interpretacja. W XSmells wykorzystano następującą interpretację zaproponowaną w [Rslbrghe02]:fanOut

fanOut(fragment) to liczba odmiennych (ang. distinct function calls) wywołań metod w ciele analizowanego fragmentu.

Identyfikacja metody tylko po nazwie może prowadzić do mniej dokładnych dopasowań, ponieważ metoda o takiej samej nazwie może istnieć w różnych klasach. Ponadto klasy o takich samych nazwach mogą istnieć w różnych pakietach. Dlatego też metoda identyfikowana jest poprzez pakiet, klasę, oraz nazwę. Taka identyfikacja nie rozwiązuje całkowicie problemu, ponieważ mogą istnieć przeciążone metody z różną liczbą argumentów należące do tej samej klasy. Jednak dobrą praktyką programistyczną jest nazywać metody o takiej samej funkcjonalności taką samą nazwą. Z tego też powodu takie rozwiązanie uznano za zadowalające.

globals

globals(fragment) to liczba deklaracji użytych lub zmodyfikowanych zmiennych globalnych w danej metodzie. Globalna zmienna to taka zmienna, która nie jest zadeklarowana w ciele analizowanego fragmentu.

Za zmienne globalne uznano wszystkie pola klasy oraz pole this. Odwołując się poprzez pole this można także zmienić wartości pól w klasie, dlatego też referencja this także uznawana jest za zmienną globalną.

Nazwy zmiennych wykorzystywane są po to, by nie liczyć ilości wystąpień danej zmiennej, tylko liczbę różnych zmiennych. Do nazwy każdej zmiennej doklejana jest nazwa klasy oraz pakiet z jakiego pochodzi.

public void proc1() {
    show(0);
}
private static final
    int PARAM = 0;

public void proc1() {
    show(PARAM);
}
Rysunek 2. Przykład użycia zmiennej jako literału

Za zmienne globalne uznano także parametry metody, ponieważ podobnie jak inne zmienne globalne, stanowią one kanał wejścia/wyjścia dla kodu programu. Do zmiennych globalnych zaliczono również literały, mimo że nie są zmiennymi – nie można zmieniać ich wartości. Na rysunku 2. przedstawiony jest fragment kodu, który uzasadnia takie założenie.Obydwa fragmenty kodu realizują to samo zadanie. Można patrzeć na literał jak na konkretną wartość zmiennej. Praktyczne testy przeprowadzone na oprogramowaniu dowodzą, że potraktowanie literałów jako zmiennych globalnych zwiększa liczbę poprawnych dopasowań zmniejszając tym samym liczbę błędnych. Dokładniejsze informacje można uzyskać w [Rslbrghe02].

globalsUpdated

globalsUpdated(fragment) to liczba uaktualnionych pojedynczych deklaracji globalnych zmiennych w ramach analizowanego fragmentu.

Metryka ta zlicza uaktualniane w ciele metody zmienne globalne.

parmsByRefUpdated

ParmsByRefUpdated(fragment) to liczba zmiennych typu wskaźnikowego na liście parametrów metody, która jest uaktualniana w analizowanym fragmencie.

Ponieważ w języku Java nie ma zmiennych typu wskaźnikowego dlatego wartość tej metryki zawsze wynosi 0.

userInput

userInput(fragment) to liczba wyrażeń wejściowych w analizowanym fragmencie.

Wartość tej metryki zawsze wynosi 0 z uwagi na trudność w określeniu liczby wyrażeń wejściowych użytkownika.

fileInput

fileInput(fragment) to liczba wyrażeń otwierających pliki w analizowanym fragmencie.

Zliczane są wywołania konstruktorów następujących klas: java.io.RandomAccessFile,java.io.FileInputStream, java.io.Reader oraz klas potomnych tych klas.

Dodatkowo zliczane są wywołania metod: createNewFile, createTempFile klasy java.io.File.

Te konstrukcje pozwalają otworzyć plik w języku Java.

kafuraIn

kafuraIn(fragment) to suma liczby parametrów formalnych, zmiennych użytych, oraz odwołań do metody, do której należy analizowany fragment.

Parametry formalne to parametry metody. Liczba odwołań do metody nie jest liczona ze względu na duży poziom komplikacji obliczeń i nie uznawanie często prawdziwych duplikatów za podobne, dlatego, że zwykle jeden z nich jest wywoływany częściej niż inny.

kafuraOut

kafuraOut(fragment) to suma liczby funkcji wywoływanych, pojedynczych deklaracji zmiennych globalnych, aktualizowanych w analizowanym fragmencie, oraz liczby zmiennych typu wskaźnikowego na liście parametrów formalnych metody, do której należy analizowany fragment takich, które są aktualizowane w tym fragmencie.

fig7.png

McCabe

fig8.png

gdzie:

  • e to liczba krawędzi w grafie kontroli przepływu (ang. control flow graph) analizowanego fragmentu. Jedna krawędź wychodząca od wyrażenia reprezentuje przepływ, dwie krawędzie – decyzję;
  • n to liczba wierzchołków w tym grafie, przy czym wierzchołki reprezentują wyrażenia.

Wszystkie instrukcje pętli, wyrażenia warunkowe, instrukcje return, continue, break, klauzule try-catch, try-catch-finally, try-finally są traktowane jako wierzchołki w grafie kontroli przepływu, natomiast deklaracje zmiennych, odwołania do zmiennych i wywołania metod są pomijane. Z założenia w grafie zawsze występuje przynajmniej jeden wierzchołek — jest nim wierzchołek końca metody.

5.2. Dodatkowe metryki proponowane w XSmells#

W programie zaimplementowano także trzy dodatkowe metryki, które mają na celu poprawienie dokładności dopasowań. Ich interpretacja jest następująca:Liczba instrukcji pętli w metodzie (NOLOOP)

Metryka ta zlicza wystąpienia instrukcji for, do…while oraz while.

Liczba instrukcji powrotu z metody (NORET)

W tej metryce zliczane są wystąpienia instrukcji return, ale tylko dla metod zwracających wartość. Metody typu „void” nie są liczone, ponieważ w nich instrukcja return nie musi występować.

Rozmiar podzbioru wywoływanych metod (CARDMC)

W tym przypadku dla każdej metody pamiętane są nazwy metod przez nią wywoływanych łącznie z nazwą klasy i pakietem, do którego ona należy. Podczas porównywania wyznaczany jest rozmiar zbioru, będącego podzbiorem zbiorów zawierających nazwy wywoływanych metod.

5.3. Porównywanie metod#

Dwie metody są uznawane za podobne, jeśli odległość ich wektorów metryk jest nie większa niż Δd, gdzie Δd jest pewną konfigurowalną wartością graniczną. W analizie przyjęto Δd = 0.W przypadku gdy stosowane są metryki zaproponowane przez zespół K. Kontogiannisa [Kntgs96] to odległość dwóch wektorów metryk porównywanych metod jest odległością w przestrzeni Euklidesowej tych wektorów.

Dla algorytmu wykorzystującego dodatkowe metryki odległość wektorów metryk metodmet1 i met2 przedstawia się następującym wzorem:

fig9.png

6. Analiza uzyskanych wyników#

By zbadać skuteczność tego podejścia, przeprowadzono test na specjalnie przygotowanych przypadkach testowych oraz na dwóch wybranych projektach zrealizowanych w firmie TiP Spółka z o.o. Analizowano tylko skuteczność algorytmu, natomiast nie rozpatrywano złożoności pamięciowej i obliczeniowej.

6.1. Metryki Kontogiannisa#

Poniżej przedstawiono przypadki testowe, dla których metoda Kontogiannisa wykryła fałszywe duplikaty.Przedstawione na rysunkach 3a i 3b fragmenty kodu zostały uznane za duplikaty, ponieważ złożoność cyklomatyczna oraz liczba zmiennych globalnych obu algorytmów jest taka sama i wynosi odpowiednio 2 i 2. Metryki te nie rozróżniają instrukcji warunkowej w pętli od instrukcji warunkowej w bloku kodu.

public int SwitchTest() {
    int i=0;
    switch (i) {
        default: return 1;
    }
}
public int loopFor() {
    for (int i = 0; i < 10; i++);

    return 0;
}
Rysunek 3a. Fałszywy duplikat dla instrukcji switch i for
public int switchTest() {
    int i = 0;
        
    switch (i) {
        default: return 1;
    }
}
public int loopWhile() {
    int i = 0;
        
    while (i < 10) {
        i++;
    }
        
    return i;
}
Rysunek 3b. Fałszywe duplikaty dla instrukcji switch i pętli

Przykład na rysunku 4. pokazuje, że metryki Kontogiannisa nie uwzględniają nazw metod. Liczba wystąpień unikatowych wywołań metod jest taka sama, dlatego metody są uznawane za identyczne.

public void procA() {
    procB();
}
public void proc2() {
    proc1();
}
Rysunek 4. Różne wywołania metod

Metryki Kontogiannisa zliczają tylko unikatowe odwołania do innych metod, natomiast pomijają kolejne wywołania tej samej metody. Dla obu fragmentów kodu na rysunku 5 liczba unikatowych wywołań metod jest równa 1, co powoduje błędną klasyfikację.

public void proc1() {
    show(0);
}
public void proc3() {
    show(0);
    show(0);
    show(0);
}
Rysunek 5. Unikatowe odwołania do innych metod

Przedstawiony na rysunku 6. przykład pokazuje konstrukcję bardzo często występującą w programach napisanych w językach obiektowych. Liczba wykorzystanych zmiennych globalnych jest taka sama w obu przypadkach i równa 2, liczba aktualizowanych zmiennych globalnych także – równa jednak metody w oczywisty sposób różnią się od siebie.

public void setOne() {
    c = 1;
}
public int getNext() {
    c = c + 1;
    return c;
}
Rysunek 6. Przykład metod set i get

W przykładach z rysunku 7. liczba wykorzystanych zmiennych globalnych oraz złożoność cyklomatyczna jest taka sama, dlatego też metody te niesłusznie uznane są za duplikaty. Pokazuje to, że potrzebna jest metryka określająca złożoność wyrażenia.

public int multiply() {
    return c * c;
}
public int add() {
    return c + c;
}

W przeciwieństwie do poprzednich, przykład przedstawiony na rysunku 8. to prawdziwy duplikat, który nie został znaleziony, ponieważ złożoność cyklomatyczna obu metod jest różna, choć ich funkcjonalność jest identyczna.

public void proc4() {
  for (int i=0; i<3; i++){
    show(i);
  }
}
public void proc5() {
    show(0);
    show(1);
    show(2);
}
Rysunek 8. Identyczne metody o różnej złożoności

6.2. Dodatkowe metryki proponowane w XSmells#

By zbadać skuteczność podejścia zastosowanego w XSmells z rozszerzonym zestawem metryk, przeprowadzono test na specjalnie przygotowanych przypadkach testowych, takich samych jak dla wersji programu posiadającej tylko metryki Kontogiannis et al. Ponownie, analizowano tylko skuteczność algorytmu, natomiast nie brano pod uwagę złożoności pamięciowej i obliczeniowej. Podczas przeprowadzanych eksperymentów nie zaobserwowano by algorytm wzbogacony o dodatkowe metryki sprawdzał kod dłużej od algorytmu ich pozbawionego. Czasy były praktycznie identyczne. Po przeprowadzeniu eksperymentu stwierdzono, że XSmells prawidłowo rozpoznał przypadki przedstawione na rysunkach 3a, 3b, 4. i 6. natomiast nadal nie rozpoznał przykładów zaprezentowanych na rysunkach 5., 7. i 8.Dla przykładu z rysunku 5. nie znaleziono metryki, która pozwoliłaby wyeliminować ten błąd. Zliczanie liczby odwołań do danej metody, nie ma sensu, ponieważ może istnieć sytuacja, w której jedno wywołanie tej metody może być równoważne jej większej liczbie wywołań.

Wyznaczenie metryki, która dobrze oddawałaby semantykę wyrażenia, jest zadaniem trudnym. Do momentu pisania tego artykułu nie udało się zastosować z sukcesem żadnej takiej metryki, dlatego też przykład z rysunku 7. nadal jest źle klasyfikowany.

Przykład przedstawiony na rysunku 8. to prawdziwy duplikat, który za takowy nie został uznany. W tym przypadku należałoby „rozwijać” pętle. Jednak jest to zadanie dość trudne, ponieważ mogą istnieć pętle, które nie mają określonej liczby wywołań. Ponadto w pętlach z określoną liczbą wywołań mogą występować instrukcje takie jak breakczy continue powodujące natychmiastowe wyjście z pętli.

6.3. Wykrywanie duplikatów dla wybranych projektów#

By sprawdzić skuteczność obu podejść w praktyce przeprowadzono testy na dwóch projektach zrealizowanych w firmie TiP Spółka z o.o.Pierwszy z projektów to aplikacja przeznaczona na telefony komórkowe wykonana w technologii J2ME. Taka aplikacja musi posiadać niewielki rozmiar, toteż podczas jej wytwarzania kod skracany przeglądany jest wielokrotnie w poszukiwaniu duplikatów. Dzięki temu jest ich stosunkowo niewiele.

Drugim z projektów jest aplikacja sieciowa wykorzystywana wewnątrz firmy. Ze względu na mniejsze ograniczenia co do rozmiaru, kod był rzadziej przeglądany w poszukiwaniu duplikatów, przez co zawiera ich więcej od aplikacji J2ME.

Tabela 1. Wyniki testów dla metryk K. Kontogiannisa
Projekt LOC Liczba metod Liczba wykrytych duplikatów (poprawnych dopasowań) Liczba fałszywych dopasowań ogółem Liczba fałszywych dopasowań dla LOC < 4
Aplikacja J2ME 441 65 6 16 6
Aplikacja sieciowa 1249 101 32 49 42

Tabele 1. i 2. przedstawiają wyniki testu. Jak widać obie metody wykrywają tyle samo poprawnych duplikatów, natomiast różnią się liczbą fałszywych dopasowań. Warto zaznaczyć, że większość z tych fałszywych dopasowań to metody, które zawierają poniżej 4 linii kodu.

Tabela 2. Wyniki testów dla dodatkowych metryk zaproponowanych w XSmells
Projekt LOC Liczba metod Liczba wykrytych duplikatów (poprawnych dopasowań) Liczba fałszywych dopasowań ogółem Liczba fałszywych dopasowań dla LOC < 4
Aplikacja J2ME 441 65 6 6 6
Aplikacja sieciowa 1249 101 32 12 12

Dla dodatkowych metryk zaproponowanych w XSmells liczba fałszywych dopasowań o rozmiarze poniżej 4 linii kodu wynosi tyle ile liczba wszystkich fałszywych dopasowań. Gdyby brać pod uwagę tylko te metody, których rozmiar to przynajmniej 4 linie kodu, to dla tych projektów i podejścia zaproponowanego w XSmells trafność wynosiłaby 100%.

7. Podsumowanie#

Wyszukiwanie duplikatów w kodzie programu to zadanie nietrywialne. Okazuje się, że metryki Kontogiannisa nie oddają w pełni struktury kodu i powodują nieprawidłową klasyfikację niektórych konstrukcji. By zmniejszyć liczbę nieprawidłowych dopasowań wprowadzono dodatkowe metryki. Eksperymenty pokazały, że dla 4 z 7 rozpatrywanych przypadków nowe metryki pozwalają prawidłowo je zaklasyfikować. Nadal jednak niektóre konstrukcje są błędnie interpretowane. Należą do nich wielokrotne wywołania tej samej metody, semantyka wyrażeń oraz, co naturalne, alternatywnych implementacji tych samych algorytmów. Zagadnienia te stanowią ciekawy kierunek do dalszych badań.Osobnym problemem, który pozostaje nierozwiązany, jest algorytm dopasowania dwóch fragmentów kodu. W aktualnej implementacji XSmells wykorzystano rozwiązanie wykorzystujące odległość wektorów metryk w przestrzeni Euklidesowej.

Oprócz trafności dopasowań ważna jest także skalowalność algorytmu, złożoność obliczeniowa i pamięciowa, które nie były w ogóle poruszone w tym artykule. Jest to szczególnie ważne dla dużych projektów, gdzie rozmiar kodu jest znaczny. Dlatego jest to kolejny kierunek dalszych badań.

Narzędzie wyszukujące „przykre zapachy” w kodzie może w znacznym stopniu skrócić czas niezbędny na przeglądy, a stosowane regularnie, skrócić czas związany z refaktoryzacją. Dzięki niemu można szybciej i taniej rozbudowywać kod oraz dodawać do niego nowe funkcje, a w dalszej kolejności skrócić czas konieczny do zapoznania się z kodem.

Bibliografia#

[Baxter95] I. D. Baxter, I. D. Yahin, I. D. Moura, I. D. Sant’Anna i I. D. Bier, Clone Detection using Abstract Syntax Trees, WCRE, 1995.
[Eclipse03] Object Technology International , Inc., Eclipse Platform Technical Overview, 2003, http://www.eclipse.org/whitepapers/eclipse-overview.pdf.
[Fowl99] M. Fowler, Refactoring, Improving the Design of Existing Code,Addison-Wesley, 1999.
[Kntgs96] K. Kontogiannis, Pattern matching for clone and concept detection,Automated Software Engineering, vol. 3, no. 1, 1996.
[MatchLoc01] G. Van den Heuvel, Parameterized matching: a technique for the detection of duplicated code, master thesis, University of Antwerp, 2001.
[Rslbrghe02] F. Van Rysselberghe, Finding Duplicated Code Using Metric Fingerprinting, master thesis, University of Antwerp, 2002.
[xUnit94] K. Beck, Simple Smalltalk Testing: With Patterns, Smalltalk Report, 1994.

[#1] Praca finansowana przez Komitet Badań Naukowych w ramach grantu 4 T11F 001 23 realizowanego w latach 2002-2005.
[#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.