13.2 Warum weigern sich Entwickler, ihre eigenen
13.2.1 Wie und wann refaktorisiert man?
Wie kửnnen Sie lernen zu refaktorisieren? Welche Werkzeuge und Techniken gibt es? Wie kửnnen sie kombiniert werden, um etwas Nỹtzliches zu erreichen? Wann sollten wir sie anwenden? Dieses Buch definiert mehrere Dutzend Refaktorisie- rungen, die Martin Fowler in seiner Arbeit als nỹtzlich kennen gelernt hat. Es prọ- sentiert Beispiele, wie diese Refaktorisierungen angewandt werden kửnnen, um wesentliche Änderungen an Programmen zu unterstützen.
Im Software Refactory-Projekt an der Universitọt von Illinois wọhlten wir einen minimalistischen Ansatz. Wir definierten einen kleineren Satz von Refaktori- sierungen1,3 und zeigten, wie sie angewandt werden kửnnen. Wir gewannen unsere Sammlung von Refaktorisierungen aus unseren eigenen Programmierer- fahrungen. Wir werteten die strukturelle Entwicklung verschiedener objektorien- tierter Frameworks aus, vor allem in C++, sprachen mit Smalltalk-Entwicklern und lasen die Rückblicke verschiedener erfahrener Smalltalk-Entwickler. Die meisten unserer Refaktorisierungen waren so elementar wie das Erstellen oder Entfernen einer Klasse, Variablen oder Funktion; das Ändern der Attribute von Variablen und Funktionen, von Zugriffsrechten (z.B. ửffentlich oder geschỹtzt) und Funktionsar- gumenten, bzw. so elementar, wie das Verschieben von Variablen und Funktionen zwischen Klassen. Ein kleinerer Satz von Refaktorisierungen hửherer Ebene wurde für Operationen benutzt, wie das Bilden einer abstrakten Oberklasse, das Vereinfa- chen einer Klasse durch Unterklassen und das Vereinfachen bedingter Ausdrücke
oder das Abspalten von Teilen einer Klasse, um eine neue, wiederverwendbare Komponente zu erstellen (wobei wir oft zwischen Vererbung und Delegation oder Aggregation hin- und herwechselten). Die komplexeren Refaktorisierungen wur- den mit Hilfe der elementaren formuliert. Unser Ansatz war durch Gesichtspunkte der Automatisierung und der Sicherheit motiviert, die ich spọter erlọutere.
Welche Refaktorisierungen sollen Sie anwenden, wenn Sie ein vorhandenes Pro- gramm haben? Das họngt natỹrlich von Ihren Zielen ab. Ein họufiger Grund, auf den sich dieses Buch konzentriert, ist der Wunsch, ein Programm so zu restruktu- rieren, dass es einfacher wird, (in naher Zukunft) neue Elemente einzufügen. Dies diskutiere ich im nọchsten Abschnitt. Es gibt aber auch andere Grỹnde, warum Sie Refaktorisierungen anwenden kửnnen.
Erfahrene objektorientierte Entwickler und solche, die in Entwurfsmustern und guten Entwurfstechniken geschult wurden, wissen, dass verschiedene strukturelle Eigenschaften und Charakteristika von Programmen erwiesenermaòen die Erwei- terbarkeit und Wiederverwendung unterstützen4-6. Objektorientierte Entwurfs- techniken, wie Klassenkarten (CRC cards)7 konzentrieren sich darauf, Klassen und ihre Protokolle zu definieren. Obwohl der Schwerpunkt hier im Entwurf vor der Implementierung liegt, kann man existierende Programme mit diesen Richtli- nien vergleichen.
Ein automatisiertes Werkzeug kann Ihnen helfen, strukturelle Schwọchen in ei- nem Programm zu identifizieren, wie z.B. Funktionen, die eine extrem groòe Zahl von Argumenten haben. Dies sind Kandidaten für Refaktorisierungen. Ein auto- matisiertes Werkzeug kann auch strukturelle Ähnlichkeiten identifizieren, die auf Redundanzen hindeuten kửnnen. Wenn z.B. zwei Funktionen nahezu identisch sind (was họufig vorkommt, wenn man kopiert und ọndert, um aus einer Funk- tion ein weitere zu machen), so kửnnen solche Ähnlichkeiten entdeckt und Re- faktorisierungen vorgeschlagen werden, die den gemeinsamen Code an einer Stelle zusammenfassen. Wenn zwei Variablen in verschiedenen Teilen des Pro- gramms denselben Namen haben, so kửnnen sie manchmal durch eine einzelne Variable ersetzt werden, die an beiden Stellen geerbt wird. Dies sind nur einige wenige sehr einfache Beispiele. Viele andere, auch komplexere Fọlle kửnnen mit automatisierten Werkzeugen entdeckt und korrigiert werden. Diese strukturellen Abnormalitọten oder strukturellen Ähnlichkeiten bedeuten nicht immer, dass Sie refaktorisieren müssen, aber oft ist es so.
Ein groòer Teil der Arbeit an Entwurfsmustern konzentrierte sich auf guten Pro- grammierstil und nützliche Muster für die Interaktion zwischen verschiedenen Teilen eines Programms, die auf strukturelle Charakteristika und Refaktorisierun-
gen abgebildet werden kửnnen. Der Abschnitt ỹber die Anwendbarkeit des Mus- ters Template-Methode8 bezieht sich z.B. auf unserere Refaktorisierung Abstrakte Oberklasse bilden9.
Ich habe1 einige Heuristiken zusammengestellt, die dabei helfen, Kandidaten für Refaktorisierungen in einem C++-Programm zu identifizieren. John Brant und Don Robert10,11 haben ein Werkzeug entwickelt, das einen umfangreichen Satz von Heuristiken anwendet, um Smalltalk-Programme automatisch zu analysie- ren. Sie schlagen vor, welche Refaktorisierungen den Programmentwurf verbes- sern kửnnen und wie diese anzuwenden sind.
Ein solches Werkzeug einzusetzen, um Ihr Programm zu analysieren, ọhnelt dem Einsatz von lint. Das Werkzeug kann die Bedeutung des Programms nicht verste- hen. Nur einige der Vorschlọge, die es auf Basis der Strukturanalyse des Pro- gramms macht, mửgen Änderungen sein, die Sie tatsọchlich durchfỹhren wollen.
Als Programmierer treffen Sie die Entscheidung. Sie entscheiden, welche Empfeh- lungen Sie auf Ihr Programm anwenden. Diese Änderungen sollten die Struktur Ihres Programms verbessern und zukünftige Änderungen besser unterstützen.
Bevor Programmierer sich davon ỹberzeugen kửnnen, dass sie ihren Code refakto- risieren sollten, müssen sie verstehen, wie und wann man refaktorisiert. Es gibt keinen Ersatz für Erfahrung. Wir nutzten die Einsichten erfahrener objektorien- tierter Entwickler bei unseren Untersuchungen, um einen Satz nützlicher Refakto- risierungen zu finden, und Einsichten darüber, wo sie angewandt werden sollten.
Automatisierte Werkzeuge kửnnen die Struktur eines Programms analysieren und Refaktorisierungen vorschlagen, die die Struktur verbessern kửnnen. Wie in den meisten Fachgebieten kửnnen Werkzeuge und Techniken helfen, aber nur, wenn Sie diese auch einsetzen. Wenn Programmierer ihren Code refaktorisieren, wọchst ihr Verstọndnis.
Refaktorisieren von C++-Programmen von Bill Opdyke Als Ralph Johnson und ich 1989 unsere Forschungen über das Refaktorisieren be- gannen, entwickelte sich die Programmiersprache C++ und wurde unter objekto- rientierten Entwicklern sehr populọr. Die Bedeutung des Refaktorisierens war zu- nọchst in der Smalltalk-Entwicklung erkannt worden. Wir hatten das Gefỹhl, dass es eine grửòere Zahl objektorientierter Entwickler interessieren wỹrde, wenn wir die Fọhigkeiten des Refaktorisierens an C++-Programmen zeigen wỹrden. C++ hat Sprachelemente, vor allem seine statische Typprüfung, die Teile der Programma- nalyse und der Refaktorisierungsaufgaben vereinfachen. Auf der anderen Seite ist
C++ komplex, zum groòen Teil wegen seiner Geschichte und Entwicklung aus der Programmiersprache C. Einige zulọssige Programmierstile in C++ machen es schwierig zu refaktorisieren und ein Programm weiterzuentwickeln.
Sprachelemente und Programmierstile, die das Refaktorisieren unterstützen
Die statische Typprỹfung in C++ macht es relativ einfach mửglich, Referenzen auf den Teil des Programms, den Sie refaktorisieren wollen, einzugrenzen. Um einen einfachen, aber họufigen Fall herauszugreifen, nehmen Sie an, Sie wollen eine Methode (member function) einer C++-Klasse umbenennen. Um die Umbenen- nung korrekt durchzuführen, müssen Sie die Deklaration der Methode und alle Referenzen auf diese Methode ọndern. Das Suchen und Ändern der Referenzen kann schwierig sein, wenn das Programm groò ist.
Im Vergleich mit Smalltalk hat C++ Elemente, um Vererbung und Zugriffsrechte zu steuern (public, protected, private), die es einfacher machen festzustellen, wo es Referenzen auf die umzubenennende Methode geben kann. Ist die Methode als private deklariert, so kửnnen Referenzen auf die Methode nur innerhalb der Klasse selbst erfolgen oder in Klassen, die als friend dieser Klasse deklariert sind.
Wenn die Methode als protected deklariert ist, kửnnen Referenzen nur in dieser Klasse vorkommen, in ihren Unterklassen (und deren Abkửmmlingen) und in Klassen, die als friend dieser Klassen deklariert sind. Wenn die Methode als pu- blic (dem am wenigsten restriktiven Schutzmodus) deklariert ist, kann sich die Analyse immer noch auf die Klassen beschrọnken, die hier fỹr ằgeschỹtzteô Me- thoden aufgeführt wurden, und auf die Operationen auf Objekten der Klasse, die die Methode enthọlt, ihre Unterklassen und Abkửmmlinge.
In einigen sehr groòen Programmen kửnnen Methoden mit dem gleichen Namen an verschiedenen Stellen im Programm deklariert worden sein. In manchen Fọl- len werden zwei oder mehr Methoden besser durch eine einzelne Methode er- setzt; es gibt họufig anwendbare Refaktorisierungen, die diese Änderung vorneh- men. Auf der anderen Seite ist es manchmal der Fall, dass eine Methode umbenannt werden sollte und die andere unverọndert bleibt. In einem Mehrper- sonenprojekt kửnnen zwei oder mehr Programmierer den gleichen Namen fỹr vửllig unabhọngige Methoden verwendet haben. In C++ ist es fast immer einfach festzustellen, welche Referenzen auf die umzubenennende Methode verweisen und welche auf die andere. In Smalltalk ist diese Analyse schwieriger.
Da C++ Unterklassen verwendet, um Untertypen zu implementieren, kann der Gültigkeitsbereich einer Methode meist verallgemeinert oder spezialisiert werden, indem man der Vererbungshierarchie hinauf oder hinunter folgt. Ein Programm zu analysieren und die Refaktorisierungen durchzuführen ist ziemlich einfach.
Verschiedene Prinzipien guten Entwurfs, wọhrend der ursprỹnglichen Entwick- lung und wọhrend des ganzen Softwareentwicklungsprozesses angewendet, er- leichtern den Prozess der Refaktorisierung und machen es leichter, Software wei- terzuentwickeln. Felder und die meisten Methoden als privat zu deklarieren ist eine Technik, die es oft erleichtert, die Interna einer Klasse zu refaktorisieren und die Änderungen an anderen Stellen des Programms zu minimieren. Die Generali- sierungs- und Spezialisierungshierarchien in Vererbungshierarchien zu modellie- ren (wie es in C++ üblich ist) macht es einfach, die Gültigkeitsbereiche von Fel- dern oder Methoden spọter zu verallgemeinern oder zu spezialisieren, indem man Refaktorisierungen verwendet, die diese entlang der Vererbungshierarchien ver- schieben.
Elemente von C++-Entwicklungsumgebungen unterstützen ebenfalls Refaktori- sierungen. Wenn ein Programmierer beim Refaktorisieren einen Fehler macht, er- kennt họufig der C++-Compiler den Fehler. Viele C++-Entwicklungsumgebungen bieten mọchtige Mửglichkeiten fỹr Verwendungsnachweise und Codeansichten.
Sprachelemente und Programmierstile, die das Refaktorisieren erschweren
Die Kompatibilitọt von C++ mit C ist, wie die meisten von Ihnen wissen, ein zwei- schneidiges Schwert. Viele Programme wurden in C geschrieben und viele Pro- grammierer wurden in C ausgebildet, was es (zumindest oberflọchlich betrachtet) einfacher macht, nach C++ zu migrieren als zu einer anderen objektorientierten Programmiersprache. Allerdings unterstützt C++ auch viele Programmierstile, die solide Entwurfsprinzipien verletzen.
Programme, die Elemente von C++ verwenden, wie Zeiger, Cast-Operationen und sizeof(Object), sind schwer zu refaktorisieren. Zeiger und Cast-Operationen füh- ren zu Aliasing, wodurch es schwierig wird, alle Referenzen auf ein Objekt zu be- stimmen, das Sie refaktorisieren wollen. Jedes dieser Elemente legt die interne Darstellung offen, wodurch Abstraktionsprinzipien verletzt werden.
Zum Beispiel verwendet C++ eine V-Table, um Felder in einem ausführbaren Pro- gramm darzustellen. Die vererbten Felder erscheinen zuerst, gefolgt von den lokal definierten Feldern. Eine im Allgemeinen gefahrlose Refaktorisierung besteht da- rin, eine Variable in eine Oberklasse zu verschieben. Da das Feld nun geerbt wird, anstatt lokal in der Unterklasse definiert zu werden, hat sich die physische Posi- tion des Feldes in dem ausführbaren Programm aller Wahrscheinlichkeit nach durch die Refaktorisierung geọndert. Wenn alle Feldzugriffe in dem Programm über die Klassenschnittstelle erfolgen, so wird eine Umordnung der physischen Positionen der Felder das Verhalten des Programms nicht ọndern.
Wenn das Feld aber über Zeigerberechnungen verwendet wird (der Programmie- rer hat z.B einen Zeiger auf das Objekt, weiò, dass das Feld im fỹnften Byte steht, und weist dem fünften Byte über Zeiger einen Wert zu), dann wird das Verschie- ben des Felds in eine Oberklasse hửchstwahrscheinlich das Verhalten des Pro- gramms ọndern. Hat ein Programmierer eine Bedingung der Art if(sizeof(Ob- ject)==15) geschrieben und das Programm refaktorisiert, um ein nicht verwendetes Feld zu entfernen, so ọndert sich die Grửòe eines Objekts und eine Bedingung, die vorher wahr lieferte, ergibt nun falsch.
Es mag jemandem absurd erscheinen, Programme zu schreiben, die aufgrund der Grửòe von Objekten verzweigen oder Zeigerberechnungen verwenden, wenn C++
eine viel bessere Schnittstelle für Felder einer Klasse bietet. Ich will damit sagen, dass diese Elemente (und andere, die von der physischen Struktur eines Objekts abhọngen) Bestandteil von C++ sind und dass es Programmierer gibt, die gewohnt sind, sie zu verwenden. Die Migration von C nach C++ allein macht noch keinen objektorientierten Programmierer oder Designer.
C++ ist eine sehr komplizierte Sprache (verglichen mit Smalltalk und in geringe- rem Maòe mit Java). Es ist deshalb sehr viel schwieriger, die Art von Darstellung einer Programmstruktur zu erstellen, die benửtigt wird, um automatisch zu prỹ- fen, ob eine Refaktorisierung gefahrlos ist und falls ja, die Refaktorisierung durch- zuführen.
Da C++ die meisten Referenzen zur Umwandlungszeit auflửst, erfordert das Refak- torisieren normalerweise das erneute Umwandeln mindestens eines Teils des Pro- gramms und das Linken des ausführbaren Programms, bevor man die Auswirkun- gen testet. Im Unterschied dazu bieten Smalltalk und CLOS (Common Lisp Object System) Umgebungen für die interpretative Ausführung und inkremen- telle Umwandlung. Wọhrend es in Smalltalk und CLOS ziemlich normal ist, eine Reihe von inkrementellen Refaktorisierungen durchzuführen (und zurückzuneh- men), sind die Kosten pro Iteration in C++ (in der Form von neuer Umwandlung und neuem Testen) hửher; daher neigen Programmierer dazu, diese kleinen Ände- rungen weniger gern durchzuführen.
Viele Anwendungen verwenden eine Datenbank. Änderungen der Struktur von Objekten in einem C++-Programm kửnnen entsprechende Änderungen am Da- tenbankschema erfordern. (Viele der Ideen, die ich in meiner Arbeit über das Re- faktorisieren anwandte, stammten aus Untersuchungen über die Entwicklung ob- jektorientierter Datenbankschemata.)
Eine andere Einschrọnkung, die Software-Theoretiker mehr interessieren kửnnte als Software-Praktiker, ist die Tatsache, dass C++ keine Unterstützung für eine Pro-
grammanalyse und -ọnderung auf der Metaebene enthọlt. Es gibt kein Analogon zu dem Metaobjektprotokoll in CLOS. Das Metaobjektprotokoll von CLOS unter- stỹtzt z.B. eine manchmal nỹtzliche Refaktorisierung, um ausgewọhlte Objekte einer Klasse zu Objekten einer anderen Klasse zu machen und alle Referenzen auf die alten Objekte automatisch auf die neuen zu ọndern. Glỹcklicherweise waren die Fọlle, in denen ich diese Elemente benửtigte oder sie mir wỹnschte, sehr dỹnn gesọt.
Abschlussbemerkungen
Refaktorisierungstechniken kửnnen auf C++-Programme angewendet werden, und dies ist in vielen Kontexten bereits geschehen. Von C++-Programmen wird oft erwartet, dass sie ỹber viele Jahre weiterentwickelt werden. Wọhrend dieser Entwicklung kửnnen die Vorteile des Refaktorisierens am leichtesten wahrge- nommen werden. Die Sprache bietet einige Elemente, die Refaktorisierungen er- leichtern, wọhrend andere ihrer Elemente das Refaktorisieren erschweren, wenn sie eingesetzt wurden. Glücklicherweise ist es allgemein anerkannt, dass die Ver- wendung von Elementen wie Berechnungen mit Zeigern eine schlechte Idee ist, so dass die meisten guten objektorientierten Programmierer es vermeiden, sie zu verwenden.
Vielen Dank an Ralph Johnson, Mick Murphy, James Roskind und andere dafür, dass sie mich in die Mọchtigkeit und Komplexitọt von C++ in Bezug auf das Refak- torisieren einführten.