Der lise Blog:
Einsichten, Ansichten, Aussichten

Legacycodeanalyse mit Doxygen

Das Arbeiten mit alten Quelltexten (auch bekannt als Legacy-Code) ist mitunter ein herausforderndes Unterfangen. Was macht diese Monster-Methode? Welche Effekte haben meine Änderungen? Mache ich etwas kaputt? Ist dieses Stück Code vor mir ein Bug oder gar ein „Common-Law-Feature“?

Jede Entwicklergeneration muss sich mit diesem Thema auseinandersetzen (abgesehen von der Ersten natürlich) und so gibt es auch viele Bücher oder Blog-Beiträge dazu.

Zwei dabei immer wiederkehrende Fragen beim Arbeiten mit Legacy-Code sind:

  • Was macht diese Methode?
  • Welche Effekte werden meine Änderungen haben?

Zur Beantwortung dieser Frage hilft einem das OpenSource-Programm Doxygen weiter: Richtig konfiguriert erstellt es Aufrufhierarchien aller Methoden, die dabei helfen die obigen beiden Fragen zu beantworten.

Doxygen herunterladen und verwenden

Doxygen unterliegt der GPL und kann auf der Doxygen-Downloadseite heruntergeladen werden. Um aus dem Graphen Bilder zu generieren wird des Weiteren GraphViz (wegen dem Programm „Dot“) benötigt. Es ist ebenfalls frei verfügbar (Eclipse Public License).

Zunächst wird Doxygen mit dem Kommandozeilenparameter -g gestartet:

 

doxygen.exe -g

 

Dies erstellt im aktuellen Ordner eine Konfigurationsdatei „Doxyfile“ mit der wir Doxygen steuern können. Doxygen ist sehr mächtig und die Konfigurationsdatei dementsprechend lang (fast 2.500 Zeilen). Daher hier ein kurzer Überblick über Parameter die im Kontext der Codeanalyse für mich relevant sind.

Doxygen konfigurieren

 

Project related configuration options

 

PROJECT_NAME
PROJECT_NUMBER
PROJECT_BRIEF
PROJECT_LOGO

 

(unwichtig)
Kosmetische Angaben der Dokumentation. Für die Codeanalyse nicht weiter relevant.

 

OUTPUT_DIRECTORY

 

(obligatorisch)
Hier sollte auf jeden Fall der Ort angegeben werden, wo das Ergebnis (in Form von HTML-Dateien) gespeichert werden soll.

 

SHORT_NAMES

 

(unwichtig)
Führt dazu, dass Doxygen kurze Dateinamen erstellt. Dies ist beispielweise unter Windows relevant, wenn OUTPUT_DIRECTORY einen sehr langen Namen hat.

 

OPTIMIZE_OUTPUT_FOR_C
OPTIMIZE_OUTPUT_JAVA
OPTIMIZE_FOR_FORTRAN
OPTIMIZE_OUTPUT_VHDL

 

(unwichtig)
Unterstützt Doxygen bei der Dokumentation. Diese Parameter zu setzen schadet auf jeden Fall nicht.

 

Build related configuration options

 

 

EXTRACT_ALL
EXTRACT_PRIVATE
EXTRACT_PACKAGE
EXTRACT_STATIC
EXTRACT_LOCAL_CLASSES
EXTRACT_LOCAL_METHODS
EXTRACT_ANON_NSPACES

 

(wichtig)
Bestimmt, ob Doxygen bestimmte Elemente (z.B. private Member) zusätzlich berücksichtigen soll. Einige dieser Parameter sind standardmäßig auf NO gesetzt. Man mag annehmen, dass dies auch sinnvoll ist, da z.B. private Variablen wenig über die API einer Klasse aussagen (sollten!). Prinizipiell stimme ich dem zu. In einem gut strukturiertem Code ist diese Aussage auch richtig. Aber angesichts der Tatsache, dass es um die Analyse von Legacy-Code geht, halte ich es für ratsam dennoch von diesen Konfigurationsparametern den ein oder anderen aktiv zu setzen. Man weiß nie, welche Überraschung einen bei der Arbeit mit Legacy-Code noch erwartet.

Alles, was möglich ist, wird irgendwo gemacht.

Configuration options related to the input files

 

RECURSIVE

 

(obligatorisch)
Dieser Parameter gibt an, dass Unterordner beim Scannen rekursiv durchsucht werden sollen. Ich empfehle an dieser Stelle RECURSIVE prinzipiell auf YES zu setzen. Man weiß ja nie, was alles in irgendwelchen Unterordnern noch so schlummert… (Plugins? Tests? Alte Versionen des Quellcodes? Eine Ente?)

 

EXCLUDE
EXCLUDE_SYMLINKS
EXCLUDE_PATTERNS
EXCLUDE_SYMBOLS

 

(wichtig)
Hier kann man Dateien, Pfade, Funktionsnamen, etc. spezifizieren, welche nicht von Doxygen in die Analyse mit aufgenommen werden sollen. Typische Beispiele dafür sind

  • Quelltexte von fremden Softwareprojekten (z.B. OpenSource-Bibliotheken)
  • Utility-Funktionen und -Bibliotheken
  • Test-Code
  • Beispiel-Code

Hier Elemente zu spezifizieren ist jedoch mit Vorsicht zu genießen: Wie bereits beschrieben wird alles irgendwo gemacht, was möglich ist. Ein paar Beispiele zu den vier oben genannten Fällen, die ich entweder schon gesehen habe oder die sogar von mir verbrochen wurden (zu meiner großen Schande!).

  • Manipulation der fremden Softwareprojekte um z.B. Bugs zu „beheben“ oder eigene „Features“ dort zu verstecken
  • Zyklische Abhängigkeiten zwischen Utility-Funktionen und dem eigentlichen Produkt-Code
  • Verwendung von Utility-Funktionen aus Test-Code
  • Verwendung von Utility-Funktionen aus Beispiel-Code

Meine Empfehlung: Zu Beginn sollte man nichts von der Analyse ausschließen. Sobald man mit den von Doxygen generierten Aufrufhierarchien arbeitet, bekommt man schnell ein Gefühl dafür, ob man Fremdbibliotheken oder gewisse Utility-Funktionen ausschließen möchte und kann. Zu dem Punkt kann man die Doxygen-Konfigurationsdatei anpassen und Doxygen neu laufen lassen.

 

Configuration options related to source browsing

 

 

REFERENCED_BY_RELATION
REFERENCES_RELATION

 

(unwichtig)
Werden diese Parameter auf YES gesetzt, so erzeugt Doxygen neben den hübschen Bildern von Aufrufhierarchien auch eine einfache Liste aller aufrufenden bzw. aufgerufenen Funktionen. Ich persönlich finde dies ergänzend zu den Bildern ganz nützlich, da man in den teils sehr großen Aufrufhierarchien schnell den Überblick verliert, ob eine Funktion eine Andere aufruft.

 

Configuration options related to the LaTeX output

 

GENERATE_LATEX

 

(unwichtig)
Ein Ausgabeformat(HTML) reicht an dieser Stelle. Eine zusätzliche LaTeX-Ausgabe halte ich für überflüssig.

 

Configuration options related to the dot tool

 

HAVE_DOT DOT_PATH

 

(obligatorisch)
Dot wird benötigt, um aus den von Doxygen generierten Graphen Bilder zu zeichnen. Der Pfad zum Dot-Programm muss darüber hinaus angegeben werden. dot.exe befindet sich im Installationsverzeichnis von GraphViz unter dem Ordner release\bin.

 

CALL_GRAPH
CALLER_GRAPH

 

(obligatorisch)
Diese Parameter bewirken, dass Doxygen Graphen erstellt, die entweder anzeigen welche Unterfunktionen aufgerufen werden oder wo eine Funktion verwendet wird. Für die Codeanalyse unabdinglich.

 

DOT_IMAGE_FORMAT
INTERACTIVE_SVG

 

(obligatorisch)
Als Bildformat sollte unbedingt svg gewählt werden. Dies ermöglicht es in den Bildern auf die Funktionen zu klicken, um zu deren Dokumentation zu gelangen. Darüber hinaus gefällt mir das Feature der interaktiven SVGs (es werden z.B. Pfade farblich hervorgehoben, wenn man mit der Maus darüber fährt).

 

DOT_GRAPH_MAX_NODES
MAX_DOT_GRAPH_DEPTH

 

(wichtig)
Mit diesen Parametern kann man die erstellten Graphen feintunen. Doxygen bricht die Erstellung des Graphen ab, wenn eine der beiden Schwellen überschritten ist. Dann werden die entsprechenden Funktionen, bei denen noch etwas fehlt, rot umrandet. Standardmäßig ist DOT_GRAPH_MAX_NODES auf 50 eingestellt. Das kann insbesondere bei Legacy-Projekten zu gering sein. Umgekehrt können aber auch zu große Werte zu unübersichtlichen Graphen führen, die die Arbeit nicht wirklich verbessern (weil dann wirklich alles dargestellt wird). Mit diesen Parametern experimentiere ich sehr viel und ich denke, dass sie sehr projektabhängig sein können. Als Startwert halte ich DOT_GRAPH_MAX_NODES=100 für ganz brauchbar. MAX_DOT_GRAPH_DEPTH hatte ich mal mit „3“ statt „0“ (=unbeschränkt) ausprobiert, kam jedoch zu dem Ergebnis, dass manche unerwünschten Abhängigkeiten sich erst in vierter, fünfter oder tieferer Ebene verbergen.

 

DOT_MULTI_TARGETS

 

(unwichtig)
Erhöht die Geschwindigkeit der Ausführung von Doxygen/GraphViz.

 

DOT_CLEANUP

 

(unwichtig)
Löscht am Ende der Ausführung die temporären Dateien, die Dot zum Erstellen der Bilder benötigte. Wenn man diese temporären Dateien nicht löscht, kann man sie nachträglich noch mit anderen Programmen auswerten.

Doxygen starten

Um Doxygen zu starten muss man es in dem Verzeichnis starten, in dem die relevanten Quelltexte liegen. Als ersten Übergabeparameter muss man die zuvor generierte Konfigurationsdatei angeben.

 

C:\Pfad\zu\Qelltexten\src> C:\Pfad\zu\doxygen.exe C:\Pfad\zum\Doxyfile

 

Je nach Umfang und Komplexität benötigt Doxygen gerne mal länger (z.B. 30 Minuten bei ca. 60.000 Zeilen komplexem C-Code). Es lohnt sich daher prinzipiell, Doxygen im Hintergrund oder auf einem Continious-Integration-Server (TeamCity, Jenkins) laufen zu lassen – aber das Ergebnis lohnt sich.

Das Ergebnis: Aufrufhierarchien

Doxygen erstellt einen Ordner mit einer index.html. Die erstellte Dokumentation bietet viele Möglichkeiten sich durch das Projekt zu navigieren. Unter anderem werden dabei auch Abhängigkeiten zwischen Dateien oder Paketen dargestellt.  Für die Legacycode-Analyse relevant sind insbesondere die Aurufhierarchien, die zu jeder Funktion erstellt werden. Durch die o.g. Konfigurationsparameter entstehen interaktive SVGs, wo durch einen Klick auf einen Funktionsblock direkt zur entsprechenden Funktion gesprungen werden kann.

Der CALL-Graph

Der CALL-Graph zeigt alle Funktionen an, die von einer Funktion augerufen werden. In der Praxis ist dies prinzipiell durch das Lesen des Quellcodes machbar. Beim Lesen des Quellcodes werden jedoch nur die Funktionen ersichtlich, die direkt aufgerufen werden. Der CALL-Graph hingegen stellt Funktionen aus allen Hierarchieebenen dar (dies wird eingeschränkt durch die Parameter DOT_GRAPH_MAX_NODES und MAX_DOT_GRAPH_DEPTH. Wenn diese Einschränkungen auftreten, wird ein Knoten rot umrandet dargestellt). Durch die Informationsfülle des CALL-Graphen ist es daher möglich fast die komplette Komplexität zu erfassen, die sich hinter einer Methode verbirgt. „Wird mit der Datenbank kommuniziert?“,  „Gibt es GUI-Aufrufe?“ oder „Wird die API xyz angesprochen?“ sind Fragestellungen, die mit dem CALL-Graphen gut beantwortet werden können.

Der CALL-Graph gibt jedoch leider keine Aussage über die Gesamtheit aus. Funktionszeiger,  in Schnittstellen Implementierte oder durch Message-Events getriggerte Funktionen werden z.B. nicht berücksichtigt. Dennoch bietet der CALL-Graph eine gute Grundlage um zu verstehen, was die Funktion tatsächlich alles macht – unabhängig von veralteten Benennungen oder Kommentaren.

Bewertung des Codes anhand des CALL-Graphen

Durch den CALL-Graphen ist es nicht nur möglich zu verstehen, was die Funktion tut. Darüber hinaus bietet der CALL-Graph die Möglichkeit die Struktur des Codes ausgehend von einer Funktion zu bewerten. Wenn der Code verändert werden muss, unterstützt der  CALL-Graph dabei Stellen zu identifizieren, an denen Refactoring sinnvoll ist.

Verletzung des Single-Responsibility-Prinzips (SRP)

Die reine Anzahl ausgehender Kanten einer Funktion deutet auf eine Verletzung des Single-Responsibility-Prinzips hin: Eine Funktion macht zu viel. Bevor die entsprechende Funktion bearbeitet wird, sollte versucht werden die zu verändernde Funktionalität in eine kleinere Funktion auszulagern. Die neue Funktion kann dann unter anderem erst unter Testkontrolle gebracht werden, ehe sie verändert wird.

Ignorieren von Abstraktionsgrenzen

Dot versucht beim Zeichnen des CALL-Graphen die Knoten hierarchisch zu ordnen. Somit kann man im fertigen Graphen die Abstraktionsebenen zumindest ungefähr erkennen. Diese Unterteilung nach Abstraktionsebenen ist dabei kein explizites Feature, sondern nur ein nützlicher Nebeneffekt und daher nicht verlässlich. Nichtsdestotrotz unterstützt die Darstellungsweise die Suche nach Überschreitungen von Abstraktionsgrenzen: Hat eine Funktion Kanten, die sehr lang sind und weit von links nach rechts zeigen, so deutet dies darauf hin. Bei Code-Anpassungen kann man dies berücksichtigen und die Abstraktionsebenen gerade ziehen.

Starke Kopplung/Enge Abhängigkeiten

Dadurch, dass Dot versucht Kanten im Mittel kurz zu halten, werden zusammenhängende Funktionen bzw. Klassen eng beinander gruppiert. Umgekehrt kann man daraus erkennen, dass Kanten, die über solche Gruppierungen hinweg gehen und andere Kanten kreuzen darauf hinweisen, dass es starke Abhängigkeiten zwischen Teilmodulen gibt. Solche Abhängigkeiten sind nicht immer schlimm, allerdings machen sie Anpassungen schwieriger. Bei Funktionsanpassungen muss gleichzeitig an mehreren Stellen geprüft werden, ob Quereffekte auftreten können. Erkennt man durch den CALL-Graphen solche Abhängigkeiten früher, so kann man sie gezielt entfernen, um Auswirkungen von Funktionsanpassungen besser einschätzen zu können.

Zyklen und Rekursionen

Das Erkennen von indirekten Rekursionen über mehrere Funktionsebenen hinweg ist durch reines Lesen von Code nicht leicht. Alternativ zu IDEs kann auch der von Doxygen generierte CALL-Graph den Entwickler dabei unterstützen. Zyklen über mehrere Abstraktions- oder Funktionsebenen hinweg sind leicht daran zu erkennen, dass es Kanten gibt, die von rechts nach links zeigen.

Der CALLER-Graph

Der CALLER-Graph zeigt an wo die aktuelle Funktion überall verwendet wird. Dies ist direkt nützlich, um die Auswirkungen abzuschätzen, die Veränderungen verursachen können.

Häufig genug zeigt mir der CALLER-Graph von Funktionen an, dass sie nur an wenigen Stellen oder sogar nur an einer einzigen Stelle verwendet werden. Dies ermöglicht es sehr schnell in Erfahrung zu bringen, wie diese Funktion eingesetzt und was von ihr verlangt wird. Mit diesem Wissen lassen sich durchaus Unittests ableiten.

Ein weiterer Mehrwert ist, dass durch den CALLER-Graphen die Ergebnisse von Geschäftsprozessen nachvollzogen werden können. Der CALLER-Graph zeigt an über welche Schritte es möglich ist zu einer gewissen Funktion zu gelangen.

Fazit: Visuelle Analysen mit Doxygen

Durch die CALL- und CALLER-Graphen von Doxygen ist es möglich, visuell die Komplexität von Code zu erfassen, was insbesondere bei Legacy-Code wichtig ist. Abläufe („Was macht diese Funktion?“ oder „Wie komme ich zu diesem Ergebnis?“) können so gut erfasst werden.

Bei CALL-Graphen ist es dadurch leichter Designprobleme zu identifizieren.

Bei CALLER-Graphen hingegen besteht die Möglichkeit, Auswirkungen der eigenen Änderungen abzuschätzen.

Durch die Hyperlinks in den Graphen ist es zudem möglich, schnell durch den gesamten Code zu navigieren.

Allerdings dienen die Graphen nur dazu, die Beziehungen und Abhängigkeiten zwischen den Funktionen festzustellen – nicht wie sie im Detail miteinander interagieren. Dafür muss man nach wie vor den Code selbst studieren.

Diesen Artikel weiterempfehlen

 Teilen  Teilen  Teilen  Teilen