Im Laufe der Ent­wick­lung einer Anwendung sammeln sich in Quell­codes unsauber struk­tu­rier­te Stellen, die die An­wend­bar­keit und Kom­pa­ti­bi­li­tät eines Programms gefährden. Die Lösung ist entweder ein komplett neuer Quellcode oder eine Re­struk­tu­rie­rung in kleinen Schritten. Viele Pro­gram­mie­rer und Un­ter­neh­men ent­schei­den sich zunehmend für das Code-Re­fac­to­ring, um eine funk­tio­nie­ren­de Software lang­fris­tig zu op­ti­mie­ren und für andere Pro­gram­mie­rer lesbarer und klarer zu gestalten.

Beim Re­fac­to­ring stellt sich die Frage, welches Problem im Code durch welche Methode gelöst werden soll. In­zwi­schen gehört Re­fac­to­ring zu den Grund­la­gen beim Pro­gram­mie­ren lernen und gewinnt immer mehr an Bedeutung. Welche Methoden kommen dabei zur Anwendung und welche Vorteile und Nachteile entstehen?

Was ist Re­fac­to­ring?

Die Pro­gram­mie­rung einer Software ist ein lang­wie­ri­ger Prozess, an dem teilweise mehrere Ent­wick­ler arbeiten. Dabei wird ge­schrie­be­ner Quelltext oft über­ar­bei­tet, um­ge­stellt und erweitert. Durch Zeitdruck oder veraltete Praktiken sammeln sich unsaubere Stellen in Quell­codes, so­ge­nann­te Code-Smells. Diese his­to­risch ge­wach­se­nen Schwach­stel­len gefährden die An­wend­bar­keit und Kom­pa­ti­bi­li­tät eines Programms. Um die vor­an­schrei­ten­de Erosion und Ver­schlech­te­rung einer Software zu vermeiden, ist ein Re­fac­to­ring notwendig.

Das Re­fac­to­ring ist mit dem Lek­to­rie­ren eines Buches ver­gleich­bar. Während des Lektorats entsteht kein voll­kom­men neues Buch, sondern ein besser ver­ständ­li­cher Text. So wie es beim Lek­to­rie­ren ver­schie­de­ne Her­an­ge­hens­wei­sen wie Kürzen, Um­for­mu­lie­ren, Streichen oder Um­struk­tu­rie­ren gibt, so gibt es auch beim Code-Re­fac­to­ring Methoden wie Ein­kap­seln, Um­for­ma­tie­ren oder Ex­tra­hie­ren, um einen Code zu op­ti­mie­ren, ohne ihn im Wesen zu verändern.

Dieser Prozess ist deutlich kos­ten­güns­ti­ger als die Er­stel­lung einer komplett neuen Code­struk­tur. Vor allem in der ite­ra­ti­ven und in­kre­men­tel­len Software-Ent­wick­lung sowie der agilen Software-Ent­wick­lung spielt Re­fac­to­ring eine große Rolle, da Pro­gram­mie­rer in diesem zy­kli­schen Modell eine Software immer wieder ändern. Re­fac­to­ring ist dabei ein fester Ar­beits­schritt.

Wenn ein Quellcode erodiert: Spaghetti-Code

Zunächst ist es nötig zu verstehen, wie ein Code altern und zum be­rüch­tig­ten Spaghetti-Code mutieren kann. Ob aus Zeitdruck, durch mangelnde Erfahrung oder unklare An­wei­sun­gen: Die Pro­gram­mie­rung eines Codes führt durch unnötig kom­pli­zier­te Befehle zu Einbußen in der Funk­tio­na­li­tät. Ein Code erodiert zunehmend, je schneller und komplexer sein An­wen­dungs­be­reich ist.

Spaghetti-Code steht für einen ver­wor­re­nen, un­les­ba­ren Quellcode, dessen Sprache für Pro­gram­mie­rer nur schwer ver­ständ­lich ist. Einfache Beispiele für ver­wor­re­nen Code sind über­flüs­si­ge Sprung-Befehle (GOTO), die ein Programm anweisen, im Quellcode hin und her zu springen, oder über­flüs­si­ge for-/while-Schleifen und if-An­wei­sun­gen.

Besonders Projekte, an denen viele Software-Ent­wick­ler arbeiten, neigen zu un­über­sicht­li­chem Quelltext. Geht ein Code durch viele Hände und enthielt das Original bereits Schwach­stel­len, so lässt sich eine zu­neh­men­de Ver­kno­tung durch „Um­schif­fungs­lö­sun­gen“ und ein kost­spie­li­ges Code-Review schwer vermeiden. In den dras­tischs­ten Aus­prä­gun­gen kann Spaghetti-Code die komplette Ent­wick­lung einer Software gefährden. Dann kann selbst Code-Re­fac­to­ring das Problem nicht beheben.

Nicht ganz so drastisch sind Code-Smells und Code-Rot. Mit dem Alter kann ein Code durch unsaubere Stellen im über­tra­ge­nen Sinne zu riechen anfangen. Schlecht ver­ständ­li­che Stellen ver­schlim­mern sich durch Eingriffe anderer Pro­gram­mie­rer oder Er­wei­te­run­gen. Findet bei ersten Anzeichen von Code-Smell kein Re­fac­to­ring statt, so erodiert der Quellcode zusehends und verliert durch Code-Rot (von engl. rot für „verrotten“) seine Funk­tio­na­li­tät.

Was bezweckt ein Re­fac­to­ring?

Die Absicht hinter einem Re­fac­to­ring ist schlicht und einfach ein besserer Code. Durch einen ef­fek­ti­ven Code lassen sich neue Code-Elemente besser in­te­grie­ren, ohne neue Fehler ein­zu­bau­en. Pro­gram­mie­rer, die einen Code mühelos lesen können, finden sich schneller ein und können Bugs leichter entfernen oder vermeiden. Ein anderer Zweck des Re­fac­to­rings ist eine ver­bes­ser­te Feh­ler­ana­ly­se und Wart­bar­keit einer Software. Pro­gram­mie­rer, die einen Code über­prü­fen, haben deutlich weniger Aufwand.

Welche Feh­ler­quel­len behebt Re­fac­to­ring?

Die Techniken, die beim Re­fac­to­ring zum Einsatz kommen, sind so viel­fäl­tig wie die Fehler, die sie beheben sollen. Im Grunde definiert sich Code-Re­fac­to­ring durch seine Fehler und zeigt die nötigen Schritte, um einen Lö­sungs­weg zu verkürzen oder zu entfernen. Feh­ler­quel­len, die Sie durch Methoden des Re­fac­to­rings beheben können, sind u. a.:

  • Ver­wor­re­ne oder zu lange Methoden: Be­fehls­ket­ten und -blöcke sind so lang, dass Au­ßen­ste­hen­den die innere Logik der Software un­ver­ständ­lich ist.
  • Code-Dopp­lun­gen (Red­un­dan­zen): Ein un­über­sicht­li­cher Code enthält oftmals Red­un­dan­zen, die bei Wartungen an jeder Stelle separat geändert werden müssen und damit zeit- und kos­ten­in­ten­siv sind.
  • Zu lange Pa­ra­me­ter­lis­ten: Objekte werden nicht direkt an eine Methode gegeben, sondern ihre Attribute in einer Pa­ra­me­ter­lis­te über­mit­telt.
  • Klassen mit zu vielen Funk­tio­nen: Klassen mit zu vielen als Methode de­fi­nier­ten Funk­tio­nen, auch als Gott-Objekte be­zeich­net, die eine Anpassung der Software fast unmöglich machen.
  • Klassen mit zu wenigen Funk­tio­nen: Klassen mit so wenigen als Methode de­fi­nier­ten Funk­tio­nen, dass sie über­flüs­sig sind.
  • Zu all­ge­mei­ner Code mit Spe­zi­al­fäl­len: Funk­tio­nen mit zu spe­zi­fi­schen Son­der­fäl­len, die kaum oder nicht eintreten und daher das Hin­zu­fü­gen von not­wen­di­gen Er­wei­te­run­gen er­schwe­ren.
  • Middle Man: Eine separate Klasse fungiert als Ver­mitt­ler zwischen Methoden und ver­schie­de­nen Klassen, statt Aufrufe von Methoden direkt an eine Klasse zu leiten.

Wie ist die Vor­ge­hens­wei­se bei Re­fac­to­ring?

Re­fac­to­ring sollte stets vor der Änderung einer Pro­gramm­funk­ti­on kommen. Es erfolgt am besten in sehr kleinen Schritten und testet Än­de­run­gen des Codes durch Software-Ent­wick­lungs­pro­zes­se wie Test Driven De­ve­lo­p­ment (TDD) und Con­ti­nuous In­te­gra­ti­on (CI). Kurz gesagt stehen TDD und CI für das kon­ti­nu­ier­li­che Testen von neuen, kleinen Code­ab­schnit­ten, die Pro­gram­mie­rer bauen, in­te­grie­ren und durch oft au­to­ma­ti­sier­te Test­durch­läu­fe auf ihre Funk­tio­na­li­tät hin prüfen.

Es gilt die Regel: Das Programm nur in kleinen Schritten von innen ändern, ohne die äußere Funktion zu be­ein­flus­sen. Nach jeder Änderung sollten Sie einen möglichst au­to­ma­ti­sier­ten Testlauf durch­füh­ren.

Welche Techniken gibt es?

Es gibt zahl­rei­che konkrete Re­fac­to­ring-Techniken. Eine voll­stän­di­ge Übersicht findet sich in dem um­fang­reichs­ten Werk zum Re­fac­to­ring von Martin Fowler und Kent Beck: „Re­fac­to­ring: Improving the Design of Existing Code“. Hier ein kurzer Überblick:

Rot-Grün-Ent­wick­lung

Die Rot-Grün-Ent­wick­lung ist eine test­ge­steu­er­te Methode der agilen Software-Ent­wick­lung. Sie findet Anwendung, wenn man eine neue Funktion in einen be­stehen­den Code in­te­grie­ren möchte. Rot steht für den ersten Test­durch­lauf vor der Im­ple­men­tie­rung einer neuen Funktion in den Code. Grün steht für den ein­fachs­ten möglichen Code­ab­schnitt, der für die Funktion nötig ist, um den Test zu bestehen. Was folgt, ist eine Er­wei­te­rung mit kon­stan­ten Test­durch­läu­fen, um feh­ler­haf­ten Code aus­zu­sor­tie­ren und die Funk­tio­na­li­tät zu erhöhen. Rot-Grün-Ent­wick­lung ist ein Grund­stein für kon­ti­nu­ier­li­ches Re­fac­to­ring bei kon­ti­nu­ier­li­cher Software-Ent­wick­lung.

Branching-by-Abs­trac­tion

Diese Re­fac­to­ring-Methode be­schreibt eine schritt­wei­se Änderung an einem System und die Um­stel­lung alter im­ple­men­tier­ter Code­stel­len auf neue in­te­grier­te Ab­schnit­te. Branching-by-Abs­trac­tion kommt ge­wöhn­lich bei großen Än­de­run­gen zum Einsatz, die Klas­sen­hier­ar­chien, Vererbung und Ex­tra­hie­rung betreffen. Durch die Im­ple­men­tie­rung einer Abs­trak­ti­on, die mit einer alten Im­ple­men­tie­rung verknüpft bleibt, lassen sich andere Methoden und Klassen mit der Abs­trak­ti­on ver­knüp­fen und die Funk­tio­na­li­tät des alten Code­ab­schnitts durch die Abs­trak­ti­on ersetzen.

Oft geschieht dies durch Pull-up- oder Push-down-Methoden. Sie ver­knüp­fen eine neue, bessere Funktion mit der Abs­trak­ti­on und über­tra­gen die Ver­knüp­fun­gen auf diese. Dabei ver­schie­ben Sie entweder eine Un­ter­klas­se in eine höhere Klasse (Pull-up) oder teilen eine höhere Klasse in Un­ter­klas­sen auf (Push-down).

Die alten Funk­tio­nen können Sie schließ­lich löschen, ohne die Ge­samt­funk­tio­na­li­tät zu gefährden. Durch diese klein­tei­li­gen Än­de­run­gen funk­tio­niert das System un­ver­än­dert, während Sie un­sau­be­ren Code Abschnitt für Abschnitt durch sauberen Code ersetzen.

Methoden zu­sam­men­stel­len

Re­fac­to­ring soll die Methoden eines Codes so leicht lesbar wie möglich machen. Bereits beim Lesen er­schließt sich im besten Fall die innere Logik einer Methode auch au­ßen­ste­hen­den Pro­gram­mie­rern. Für das ef­fi­zi­en­te Zu­sam­men­stel­len von Methoden gibt es beim Re­fac­to­ring ver­schie­de­ne Techniken. Ziel jeder Änderung ist es, Methoden zu ver­ein­heit­li­chen, Dopp­lun­gen zu entfernen und zu lange Methoden in eigene Ab­schnit­te zu spalten, um sie für zu­künf­ti­ge Än­de­run­gen zu öffnen.

Techniken dafür sind bei­spiels­wei­se:

  • Me­tho­den­ex­tra­hie­rung
  • Methode inline setzen
  • Ent­fer­nung tem­po­rä­rer Variablen
  • Temporäre Variablen durch An­fra­ge­me­tho­de ersetzen
  • Ein­füh­rung be­schrei­ben­der Variablen
  • Trennung tem­po­rä­rer Variablen
  • Ent­fer­nung von Zu­wei­sun­gen an Pa­ra­me­ter­va­ria­ble
  • Methode durch ein Methoden-Objekt ersetzen
  • Al­go­rith­mus ersetzen

Ei­gen­schaf­ten zwischen Klassen ver­schie­ben

Um einen Code zu ver­bes­sern, müssen Sie mitunter Attribute oder Methoden zwischen Klassen ver­schie­ben. Hierfür kommen folgende Techniken zur Anwendung:

  • Ver­schie­be Methode
  • Ver­schie­be Attribut
  • Ex­tra­hie­re Klasse
  • Setze Klasse inline
  • Verstecke den Delegate
  • Entferne Klasse in der Mitte
  • Führe fremde Methode ein
  • Führe lokale Er­wei­te­rung ein

Da­ten­or­ga­ni­sa­ti­on

Diese Methode hat das Ziel, Daten in Klassen zu un­ter­tei­len und diese möglichst klein und über­sicht­lich zu halten. Unnötige Ver­knüp­fun­gen zwischen Klassen, die bei kleinsten Ver­än­de­run­gen die Funk­tio­na­li­tät der Software be­ein­träch­ti­gen, sollten Sie entfernen und in schlüs­si­ge Klassen teilen.

Beispiele für Techniken sind:

  • Kapseln eigener At­tri­but­zu­grif­fe
  • Eigenes Attribut durch Ob­jekt­ver­weis ersetzen
  • Wert durch Referenz ersetzen
  • Verweis durch Wert ersetzen
  • Ver­kop­pe­lung be­ob­acht­ba­rer Daten
  • Kapselung von At­tri­bu­ten
  • Datensatz durch Da­ten­klas­se ersetzen

Ver­ein­fa­chung bedingter Ausdrücke

Bedingte Ausdrücke sollten Sie während des Re­fac­to­rings so weit wie möglich ver­ein­fa­chen. Hierzu bieten sich folgende Techniken an:

  • Be­din­gun­gen zerlegen
  • Zu­sam­men­füh­ren von bedingten Aus­drü­cken
  • Zu­sam­men­füh­ren wie­der­hol­ter An­wei­sun­gen in bedingten Aus­drü­cken
  • Entfernen von Kon­troll­schal­tern
  • Ersetzen ge­schach­tel­ter Be­din­gun­gen durch Wächter
  • Fall­un­ter­schei­dun­gen durch Po­ly­mor­phie ersetzen
  • Einführen von Null-Objekten

Ver­ein­fa­chung von Me­tho­den­auf­ru­fen

Me­tho­den­auf­ru­fe lassen sich u. a. durch folgende Methoden schneller und leichter durch­füh­ren:

  • Methoden um­be­nen­nen
  • Parameter hin­zu­fü­gen
  • Parameter entfernen
  • Parameter durch explizite Methode ersetzen
  • Feh­ler­codes durch Ausnahmen ersetzen

Re­fac­to­ring-Beispiel: Methoden um­be­nen­nen

Im folgenden Beispiel ist erkennbar, dass im ur­sprüng­li­chen Code die Benennung der Methode deren Funk­tio­na­li­tät nicht eindeutig und schnell ver­ständ­lich macht. Die Methode soll die Post­leit­zahl einer Bü­ro­an­schrift ausgeben, zeigt diese Aufgabe jedoch nicht direkt im Code an. Um den Code klarer zu for­mu­lie­ren, bietet sich beim Code-Re­fac­to­ring eine Um­be­nen­nung der Methode an.

Vorher:

String getPostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getPostalCode());

Nachher:

String getOfficePostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getOfficePostalCode());

Re­fac­to­ring: Welche Vorteile und Nachteile hat es?

Vorteile Nachteile
Bessere Ver­ständ­lich­keit er­leich­tert die Wartung und Er­wei­ter­bar­keit der Software. Ein ungenaues Re­fac­to­ring könnte neue Bugs und Fehler in den Code im­ple­men­tie­ren.
Die Re­struk­tu­rie­rung des Quell­codes ist ohne Ver­än­de­rung der Funk­tio­na­li­tät möglich. Es gibt keine klare De­fi­ni­ti­on für einen „sauberen Code“.
Ver­bes­ser­te Les­bar­keit erhöht die Ver­ständ­lich­keit des Codes für andere Pro­gram­mie­rer. Ein ver­bes­ser­ter Code ist für den Kunden oft nicht erkennbar, da die Funk­tio­na­li­tät identisch bleibt, d. h. der Nutzen ist nicht of­fen­sicht­lich.
Be­sei­tig­te Red­un­dan­zen und Dopp­lun­gen erhöhen die Ef­fek­ti­vi­tät des Codes. Bei größeren Teams, die am Re­fac­to­ring arbeiten, könnte der Ab­stim­mungs­auf­wand un­er­war­tet hoch sein.
In sich ge­schlos­se­ne Methoden ver­hin­dern, dass lokale Än­de­run­gen auf andere Teile des Codes Aus­wir­kun­gen haben.
Ein sauberer Code mit kürzeren, in sich ge­schlos­se­nen Methoden und Klassen zeichnet sich durch bessere Test­bar­keit aus.

Grund­sätz­lich gilt beim Re­fac­to­ring: Führen Sie neue Funk­tio­nen nur ein, wenn der vor­lie­gen­de Quellcode un­ver­än­dert bleibt. Nehmen Sie Ver­än­de­run­gen am Quellcode, also Re­fac­to­ring, nur vor, wenn Sie keine neuen Funk­tio­nen hin­zu­fü­gen.

Zum Hauptmenü