GRUNDLAGEN

Refactoring: Code optimieren - Teil 3

15.05.2006
In dieser Beitragsreihe lernen Sie Möglichkeiten kennen, den Code einer Anwendung zu optimieren. Die vorgestellten Refactoring-Maßnahmen sind praxiserprobt und stammen eigentlich aus der objektorientierten Programmierung. Einige der für VBA interessanten Refactorings haben wir für Sie herausgesucht und mit passenden VBA-Beispielen dekoriert.

Bereits im zweiten Teil der Beitragsreihe haben Sie einige Refactoring-Maßnahmen kennen gelernt und erfahren, unter welchen Voraussetzungen Refactoring Sinn macht. Vor allem sollten Sie sich vor umfangreichen Codeänderungen durch entsprechende Tests absichern. Wie das funktioniert, konnten Sie im Beitrag „Testgetriebene Entwicklung mit Access“ der Ausgabe 4/2006 lesen.

Erklärende Variable einführen

Manche Ausdrücke sind lang und unübersichtlich. Das macht sich vor allem in Bedingungen wie If-Then- oder Select-Case-Konstrukten bemerkbar. Eine bessere Lesbarkeit kann man durch das Einführen einer Variablen erreichen.

Die Vorgehensweise ist einfach: Deklarieren Sie eine Variable, deren Namen den Inhalt des Ausdrucks verständlich macht, weisen Sie der Variablen den Ausdruck zu und verwenden Sie statt des Ausdrucks fortan die entsprechende Variable.

Temporäre Variable zerlegen

Wenn man innerhalb einer Routine mehrere Male das Ergebnis einer Operation einer Variable eines bestimmten Datentyps zuordnet, braucht man dazu theoretisch nur eine einzige Variable zu verwenden.


Dim strTemp As String

strTemp = DLookup(…)

strTemp = GetFirstName(…)

strTemp = …

Allerdings leidet die Verständlichkeit des Codes darunter, und die Gefahr unerwarteter Ergebnisse besteht damit auch. Ersteres resultiert daraus, dass die Variable keine konkrete Aussage über deren Inhalt erlaubt. Wie sollte das auch möglich sein, wenn ein und dieselbe Variable völlig verschiedene Werte enthalten soll? Das zweite Problem entsteht beispielsweise, wenn Sie der Variablen zwischendurch nur unter einer bestimmten Bedingung einen neuen Wert zuweisen, diese nicht zutrifft und die Routine die Variable im Folgenden mit einem Wert weiterverwendet, der bereits lange vorher in einem anderen Zusammenhang gesetzt wurde.

Das Refactoring sieht so aus: Deklarieren Sie für jeden unterschiedlichen Zweck der Variablen eine eigene Variable, die einen auf den Inhalt bezogenen sinnvollen Namen aufweist. Ersetzen Sie dann die zuvor verwendete temporäre Variable durch die neuen Variablen.

Natürlich gibt es Ausnahmen: Manchen Variablen werden innerhalb einer Schleife ebenfalls mehrfach unterschiedliche Werte zugewiesen. In dem Fall ist natürlich kein Refactoring erforderlich – es sei denn, Sie verwenden die Variable vorher oder nachher noch für einen anderen Zweck.

Zuweisungen zu Parametern entfernen

Wenn man einer Funktion eine Variable übergibt, verwendet man diese oft direkt weiter (Listing 1).

Public Function DateinameExtrahieren(ByVal strPfad As String)
Dim intLetzterBackslash As Integer
intLetzterBackslash = InStrRev(strPfad, "\")
If intLetzterBackslash > 0 Then
strPfad = Mid(strPfad, intLetzterBackslash + 1)
End If
DateinameExtrahieren = strPfad
End Function

Gerade weil beim Aufruf von Funktionen mit Parametern standardmäßig eine Referenz auf den jeweiligen Wert übergeben wird, kann dies zu unerwartetem Verhalten führen. Die folgenden Zeilen verdeutlichen dies. Die Variable strDateiname wird deklariert und dann mit einem Dateinamen gefüllt. Anschließend wird die Variable – wie standardmäßig festgelegt – als Referenz übergeben. Die Funktion DateinameExtrahieren ändert den Wert des Parameters und damit auch den Wert der Variablen in der aufrufenden Routine. Die beiden Debug.Print-Anweisungen geben beide den Ausdruck Text.txt aus, obwohl das Ändern des Inhalts der Variablen strDateiname sicher nicht beabsichtigt war.

Dim strDateiname As String
strDateiname = "c:\Beispiele\Text.txt"
Debug.Print DateinameExtrahieren(strDateiname)
Debug.Print strDateiname

Dies lässt sich auf zwei Arten verhindern: Entweder man stellt den Typ des Parameters mit dem Schlüsselwort By Value um, wodurch sich Änderungen nicht mehr auf den Wert der Variablen in der aufrufenden Routine auswirken. Der Kopf der Funktion aus Listing 1 sieht dann wie folgt aus:

Public Function DateinameExtrahieren(ByVal strPfad As String)

Die elegantere und sinnvollere Methode ist, den Inhalt des Parameters erst gar nicht zu ändern. Dazu führt man eine neue temporäre Variable ein, in diesem Fall etwa mit dem Namen strDateinameTemp, und weist ihr den Wert des Parameters zu (Listing 2).

Public Function DateinameExtrahieren(strPfad As String)
Dim intLetzterBackslash As Integer
Dim strDateinameTemp As String
strDateinameTemp = strPfad
intLetzterBackslash = InStrRev(strDateinameTemp, "\")
If intLetzterBackslash > 0 Then
strDateinameTemp = Mid(strDateinameTemp, intLetzterBackslash + 1)
End If
DateinameExtrahieren = strDateinameTemp
End Function

Array durch Objekt ersetzen

Arrays verwendet man in VBA meist, um mehrere gleichartige Daten zusammenzufassen und einfach über den Index darauf zugreifen zu können.

Möglicherweise kommt ein Array aber auch zum Einsatz, um verschiedenartige Informationen aufzunehmen, die lediglich den gleichen Datentyp aufweisen. Der Zugriff auf diese Informationen kann allerdings nur über den Index des Arrays erfolgen und ist daher nicht besonders intuitiv zu programmieren. Das Beispiel aus Listing 3 zeigt, dass man Hinweise auf den Inhalt etwa in Kommentaren unterbringen kann.

Public Sub ArrayBeispiel()
Dim strParameter(2) As String
strParameter(0) = "12" 'BenutzerID
strParameter(1) = "Minhorst" 'Benutzername
strParameter(2) = "axvkes" 'Kennwort
End Sub

Für einen solchen Fall bietet sich die Verwendung einer Klasse an, die alle in dem Array gespeicherten Informationen als Eigenschaften enthält. Wie dies aussieht, zeigt Listing 4. Der Code, um mit dieser Klasse zu arbeiten, scheint zwar etwas aufwendiger, aber die Verständlichkeit ist ungleich besser.

Private mBenutzernummer As String
Private mBenutzername As String
Private mKennwort As String
Public Property Let Benutzernummer(strBenutzernummer As String)
mBenutzernummer = strBenutzernummer
End Property
Public Property Get Benutzernummer() As String
Benutzernummer = mBenutzernummer
End Property
Public Property Let Benutzername(strBenutzername As String)
mBenutzername = strBenutzername
End Property
Public Property Get Benutzername() As String
Benutzername = mBenutzername
End Property
Public Property Let Kennwort(strKennwort As String)
mKennwort = strKennwort
End Property
Public Property Get Kennwort() As String
Kennwort = mKennwort
End Property

Listing 5 schließlich zeigt, wie die Klasse statt des Arrays mit den gewünschten Daten gefüllt wird. Die Klasse kann anschließend genau wie ein Array als Parameter einer Routine weitergegeben werden.

Public Sub EigenschaftenklasseBeispiel()
Dim objBenutzerdaten As clsBenutzerdaten
Set objBenutzerdaten = New clsBenutzerdaten
With objBenutzerdaten
.Benutzernummer = "12"
.Benutzername = "Minhorst"
.Kennwort = "axvkes"
End With
Call BenutzerdatenVerarbeiten(objBenutzerdaten)
End Sub

Magische Zahl durch Konstante ersetzen

Unter „magischen Zahlen“ versteht man Zahlen, die entweder in jedem Zusammenhang konstant sind (wie die Kreiszahl Pi oder die Erdbeschleunigung g) oder die zumindest im Kontext der kompletten Anwendung immer gleich sind.

Wenn Sie magische Zahlen an mehreren Stellen innerhalb Ihrer Anwendung einsetzen und immer die konkreten Zahlenwerte verwenden, sind Änderungen an dieser Zahl immer mit sehr hohem Aufwand verbunden. Besser ist es, anstelle der Zahl eine Konstante zu verwenden, die Sie an einer Stelle in der Anwendung festlegen. Dies können Sie beispielsweise in einem Standardmodul erledigen. Wenn Sie beispielsweise den Umfang eines Kreises mit der Funktion aus Listing 6 ermitteln, wird es Zeit für den Einsatz einer Konstanten.

Public Function UmfangBerechnen(sngRadius As Single) As Single
UmfangBerechnen = 2 * sngRadius * 3.14159
End Function

Sie führen dann in einem Standardmodul mit folgender Anweisung eine Konstante namens pi für die Kreiszahl ein:

Public Const pi = 3.14159

Anschließend können Sie wie in der Funktion aus Listing 7 auf die Konstante zugreifen.

Public Function UmfangBerechnenConst(sngRadius As Single) As Single
UmfangBerechnenConst = 2 * sngRadius * pi
End Function

Bedingung zerlegen

Normalerweise sollte man den Code einer Routine von oben nach unten lesen können und dabei verstehen, was passiert. Das ist in Bedingungen oft nicht der Fall. Das liegt daran, dass sowohl die Bedingung als auch die in den einzelnen Zweigen liegenden Anweisungen zu umfangreich sind, um auf einen Blick deren Funktion zu erkennen.

Dieses Refactoring sieht vor, dass man beispielsweise eine If-Then-Bedingung komplett entschlackt, indem man die Bedingung in eine Funktion und die in den Zweigen enthaltenen Anweisungen in eigene Routinen auslagert. Schematisch sieht der Code vor dem Refactoring so aus:

'...
If "<komplizierte Bedingung>" Then
'<komplizierter If-Zweig>
Else
'<komplizierter Then-Zweig>
End If
'...

Nach dem Refactoring haben Sie zwar drei neue Routinen, aber dafür eine absolut übersichtlicheIf-Then-Bedingung:

'...
If KomplizierteBedingung() Then
Call KomplizierterIfZweig
Else
Call KomplizierterThenZweig
End If
'...

Voraussetzung dafür ist natürlich, dass Sie aussagekräftige Namen sowohl für die statt der Bedingung verwendete Funktion als auch für die für die Zweige eingesetzten Routinen finden.

Bedingte Ausdrücke konsolidieren

Wenn mehrere Bedingungen die gleichen Anweisungen bewirken, sollte man sie zusammenfassen. Damit verbessern Sie die Übersichtlichkeit und vermeiden gegebenenfalls auftretende Fehler, wenn Sie Änderungen an den gleichen Zweigen nicht konsistent durchführen. Das Schema sieht wie folgt aus:

'...
If Bedingung1 Then
'Anweisungen
ElseIf Bedingung2 Then
'Anweisungen
End If
'...

Die Bedingungen müssen nur in eine Bedingung zusammengefasst und durch den Or-Operator verbunden werden. Dabei sollten Sie gleich für Ordnung sorgen, indem Sie, wie soeben erprobt, das Refactoring „Bedingung zerlegen“ einsetzen und die Anweisungen des Zweigs gegebenenfalls in eine eigene Routine ausgliedern. Anschließend sieht die obige If-Then-Bedingung so aus:

'...
If Bedingung1 Or Bedingung2 Then
'Anweisungen
End If
'...

Dieses Refactoring lässt sich auch auf verschachtelte If-Then-Bedingungen anwenden. Vor dem Refactoring sieht der betreffende Code wie folgt aus:

'...
If Bedingung1 Then
If Bedingung2 Then
‘Anweisungen
End If
End If
'...

Die beiden verschachtelten Bedingungen lassen sich durch den And-Operator zu einer Bedingung zusammenfassen:

'...
If Bedingung1 And Bedingung2 Then
'Anweisungen
End If
'...

Redundante Bedingungsteile konsolidieren

Dieses Refactoring hat eine ähnliche Ausgangsposition wie „Bedingte Ausdrücke konsolidieren“. Der Unterschied ist, dass nicht alle Anweisungen von mehreren Zweigen identisch sind, sondern nur ein Teil der Anweisungen. Schematisch sieht dies wie folgt aus:

'...
If Bedingung1 Then
Anweisung1
Anweisung2
ElseIf Bedingung2 Then
Anweisung1
Anweisung3
End If
'...

Um den redundanten Code loszuwerden, der sich hinter Anweisung1 befindet, „klammern“ Sie diese Zeile einfach aus der If-Then-Bedingung aus:

'...
If Bedingung1 Then
Anweisung2
ElseIf Bedingung2 Then
Anweisung3
End If
Anweisung1
'...

Methode umbenennen

Im zweiten Teil dieser Beitragsreihe haben Sie bereits erfahren, dass eine Routine möglicherweise zu viele Funktionen hat, wenn der Name der Routine den Zweck der Funktion nicht beschreiben kann. Das kann natürlich auch passieren, wenn man zu Beginn einfach den falschen Namen für die Methode gewählt hat – manchmal ändert sich die in einer Methode enthaltene Funktionalität ja auch im Laufe des Entwicklungsprozesses.

Abhilfe können Sie schaffen, indem Sie die Methode umbenennen. Gleichzeitig müssen Sie natürlich alle Aufrufe dieser Routine auf den neuen Namen umstellen.

Parameter hinzufügen

Änderungen an den Anforderungen einer Routine führen manchmal dazu, dass die Routine zusätzliche Informationen benötigt. Dann fügen Sie einen Parameter zum Aufruf der Routine hinzu. Beachten Sie, dass Sie alle Aufrufe der Routine anpassen müssen.

Parameter entfernen

Wesentlich schwieriger als das Hinzufügen ist das Entfernen eines Parameters. Beim Hinzufügen gibt es immer einen konkreten Anlass in Form einer fehlenden Information. Wenn man aber in einer Routine eine Zeile weglässt, die zufälligerweise als einzige einen der Parameter der Routine verwendet, bekommt man Schwierigkeiten. Kontrollieren Sie also gelegentlich, ob Sie überhaupt alle Parameter einer Routine benötigen – ansonsten tun Sie der Übersichtlichkeit der Routine etwas Gutes, indem Sie die Parameterliste kürzen. Vergessen Sie nicht, alle Aufrufe der Routine anzupassen.

Abfrage von Veränderung trennen

Funktionen sollen normalerweise einen Wert zurückliefern – gegebenenfalls in Abhängigkeit von übergebenen Parametern. Manche Funktionen tun dies und ändern gleichzeitig die übergebenen Parameter oder erledigen andere Aufgaben, die nicht unmittelbar mit der Ermittlung des Rückgabewertes zusammenhängen.

Das ist nicht besonders übersichtlich. Wenn es irgendwie möglich ist, sollten Sie diese beiden Dinge voneinander trennen und eine Funktion zum Ermitteln des gewünschten Wertes und eine weitere Routine zum Ändern des Parameters verwenden.

Als Beispiel dient die bereits weiter oben vorgestellte Routine zum Extrahieren des Dateinamens aus dem kompletten Pfad (Listing 8). Die Routine gibt nicht nur den gewünschten Ausdruck zurück, sondern zeigt eine Meldung an, wenn kein Dateiname vorhanden ist.

Public Function DateinameExtrahieren(strPfad As String) As String
Dim intLetzterBackslash As Integer
Dim strDateinameTemp As String
strDateinameTemp = strPfad
intLetzterBackslash = InStrRev(strDateinameTemp, "\")
strDateinameTemp = Mid(strDateinameTemp, intLetzterBackslash + 1)
If Len(strDateinameTemp) = 0 Then
MsgBox "Ungültiger Dateiname"
End If
DateinameExtrahieren = strDateinameTemp
End Function

Das Anzeigen der Meldung sollte nicht in der Funktion erfolgen. Ob und wie auf einen nicht vorhandenen Dateinamen reagiert werden soll, legt man besser in der aufrufenden Funktion fest:

Dim strDateiname As String
strDateiname = DateinameExtrahieren(strPfad)
If Len(strDateiname) = 0 Then
MsgBox "Ungültiger Dateiname"
Exit Sub
End If

Methode parametrisieren

Wenn zwei oder mehr Routinen die gleichen Anweisungen enthalten, die sich nur unwesentlich voneinander unterscheiden, ersetzen Sie diese durch eine einzige parametrisierte Routine. Ein einfaches, wenn auch nicht besonders praxisnahes Beispiel ist folgendes: Hier berechnen zwei Funktionen den Bruttobetrag mit zwei verschiedenen Mehrwertsteuersätzen (Listing 9).

Public Function BetragInklMwSt16(Betrag As Currency) As Currency
BetragInklMwSt16 = Betrag * 1.16
End Function
Public Function BetragInklMwSt7(Betrag As Currency) As Currency
BetragInklMwSt7 = Betrag * 1.07
End Function

Wären die Funktionen umfangreicher und müssten Sie die gleichen Änderungen an allen gleichartigen Funktionen durchführen, könnten dabei leicht Fehler passieren.

Also ersetzen Sie die beiden Funktionen durch eine einzige Funktion mit einem zusätzlichen Parameter (Listing 10). Passen Sie alle Aufrufe der vorherigen Funktionen an und löschen Sie anschließend die alten Funktionen zum Ermitteln der Mehrwertsteuer

Public Function BetragInklMwSt(Betrag As Currency, MwSt As Single) As Currency
BetragInklMwSt = Betrag * (1 + MwSt)
End Function

Zusammenfassung

Wer seinen Code hin und wieder unter die Lupe nimmt und einem Refactoring unterzieht, kann dessen Qualität sicher verbessern, vor allem im Hinblick auf Wartbarkeit und Verständlichkeit. Damit schaffen Sie gleichzeitig die Voraussetzung für eine einfache Optimierung in anderen Beziehungen, etwa wenn es um die Performance geht.

Wir haben versucht, einige für den Einsatz in VBA wichtige Refactorings auszuwählen, die neben vielen weiteren unter http://www.refactoring. com/catalog/index.html verfügbar sind. Wer sich tiefer in das Thema Refactoring einarbeiten möchte, sollte sich die dort beschriebenen Möglichkeiten ansehen.