FernUniversität in Hagen Fakultät für Mathematik und Informatik Lehrgebiet Programmiersysteme Prof. Dr. F. Steimann Sprachübergreifendes Refaktorisieren zwischen Ruby und Java Bachelorarbeit Antje Osten Matrikelnummer q6632386 2. April 2012 Erklärung Hiermit erkläre ich, dass ich diese Abschlussarbeit selbstständig verfasst, noch nicht anderweitig für Prüfungszwecke vorgelegt, keine anderen als die angegebenen Quellen und Hilfsmittel benutzt sowie wörtliche und sinngemäße Zitate als solche gekennzeichnet habe. Berlin, 2. April 2012 Antje Osten Zusammenfassung Refactoring (Refaktorisierung) beschreibt eine Strukturänderung von Programm-Quelltexten unter Beibehaltung des beobachtbaren Verhaltens. Refaktorisierungen sind Teil der Implementierungsphase. Werkzeuge sollen helfen, die Entwickler und Entwicklerinnen in der Komplexität des Refaktorisierungsvorgangs zu unterstützen. Bisher existieren entwicklungsumgebungs- und programmiersprachenabhängige Refaktorisierungswerkzeuge. Auf Basis des Frameworks Refacola wird ein sprachübergreifender Ansatz diskutiert, in dem nicht nur mehrere Sprachen in einem Projekt erlaubt sind, sondern darüber hinaus Ideen für eine Refaktorisierung zwischen statisch getypten und dynamischen Programmiersprachen aufgezeigt und programmiertechnisch konkretisiert werden. Inhaltsverzeichnis Erklärung 2 Zusammenfassung 3 1 Einleitung 1.1 Refaktorisierung – ein in der Softwareentwicklung integrierter Prozess . 1.2 Werkzeuge für den Refaktorisierungsprozess . . . . . . . . . . . . . . . . 1.2.1 Refaktorisierungswerkzeuge aus historischem Blickwinkel . . . . 1.2.2 Refaktorisierungswerkzeuge als eigenes Fachgebiet . . . . . . . . 1.3 Flexibilität durch eine programmiersprachen-übergreifende Entwicklung 1.3.1 Vorteile der Interaktion mit dynamischen Programmiersprachen . 1.3.2 Java Virtual Machine als Basis mehrerer Programmiersprachen . 1.4 Sprachübergreifende Refaktorisierungswerkzeuge . . . . . . . . . . . . . . . . . . . . . 6 7 9 10 12 13 14 15 17 2 Entwicklungsumgebung und mehrsprachige Projekte 19 2.1 Eclipse Dynamic Language Toolkit . . . . . . . . . . . . . . . . . . . . . . 19 2.2 Verbindung eines Java- und Ruby-Projekts in der Eclipse . . . . . . . . . 19 3 Interoperabilität der Programmiersprachen Ruby und Java 3.1 Einordnung der Programmiersprache Ruby . . . . . . . . . 3.1.1 Die Ideale des Ruby-Erfinders . . . . . . . . . . . . . 3.1.2 Merkmale der Programmiersprache Ruby . . . . . . 3.1.3 Dynamische Aspekte der Programmiersprache Ruby 3.2 Methodenaufrufe zwischen Ruby und Java . . . . . . . . . . 3.2.1 Methodenaufruf von Java nach Ruby . . . . . . . . . 3.2.2 Methodenaufruf von Ruby nach Java . . . . . . . . . 3.3 Konvertierungsregeln . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Konvertierungsregeln für Sichtbarkeiten . . . . . . . 3.3.2 Regeln für die Abbildung von Datentypen . . . . . . 3.4 Bedingungen für eine Refaktorisierung . . . . . . . . . . . . 3.4.1 Sichtbarkeiten sind nicht relevant . . . . . . . . . . . 3.4.2 Getter und Setter . . . . . . . . . . . . . . . . . . . 3.4.3 to_s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 21 22 25 27 27 28 28 28 29 30 31 31 32 4 Refacola – Constraint-Sprache und Framework 34 4.1 Die Sprache Refacola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 4.1.1 Sprachdefinition . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 4.1.2 Definition der Constraintregeln . . . . . . . . . . . . . . . . . . . . 36 4 4.2 4.1.3 Definition von Refaktorisierungen . . . . . . . . . . . . . . . . . . . 37 Definition eines Verbots für das Refaktorisieren sprachübergreifender Methodenaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5 Codeanalyse als Voraussetzung für Refaktorisierung 5.1 Statische Codeanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Exkurs – Mehrdeutigkeit in Ruby-Quelltexten . . . . . . . . . . . 5.1.2 Mehrdeutigkeit bei Sprachübergriffen zwischen Ruby und Java . 5.1.3 Endgültiges Versagen der Typanalyse bei dynamischen Klassennamen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.4 Vorläufiger Ablauf einer statischen Codeanalyse . . . . . . . . . . 5.2 Test-Suite zum Sammeln von Informationen für eine Refaktorisierung . 5.2.1 Test-Suite mit modifiziertem JRuby-Interpreter . . . . . . . . . . 5.2.2 Test-Suite mit Instrumentierung . . . . . . . . . . . . . . . . . . 5.3 Ausblick auf eine sinnvollen Verbindung der unterschiedlichen Ansätze . 6 Beschreibung der Software 6.1 Das Plugin AST4CongenJRubyView . . . 6.2 XRefact . . . . . . . . . . . . . . . . . . . 6.2.1 Modifizierter JRuby-Interpreter . . 6.2.2 Modifiziertes Refacola-Framework 6.2.3 Instrumentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Auflistung, Installations- und Bedienungsanleitung der A.1 Beigelegte Software . . . . . . . . . . . . . . . . . . A.2 Installation und Bedienung . . . . . . . . . . . . . A.2.1 XRefact . . . . . . . . . . . . . . . . . . . . A.2.2 Plugin AST4CongenJRubyView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . beigelegten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 . 42 . 42 . 45 . . . . . . 48 48 49 51 52 52 . . . . . 54 54 55 57 58 58 Software . . . . . . . . . . . . . . . . . . . . . . . . 60 60 60 60 61 . . . . . . . . . . . . . . . . . . . . . . . . . B Abkürzungen 62 C Liste bekannter Refaktorisierungswerkzeuge 63 Abbildungsverzeichnis 64 Listingverzeichnis 65 Literaturverzeichnis 66 5 1 Einleitung Refactoring (Refaktorisierung) gilt heute als eine grundlegende Disziplin zur Realisierung eines guten Designs. Der Begriff steht für eine manuell oder automatisiert durchgeführte Strukturänderung von Programm-Quelltexten unter Beibehaltung des beobachtbaren Verhaltens der jeweiligen Software (vgl. u. a. [Ste10], [Fow05]). Andere variable Faktoren, beispielsweise eine veränderte Laufzeit, müssen mit vorher spezifizierten Anforderungen an die Software abgeglichen werden. Als Ergebnis des überarbeiteten Designs sollen, neben einer verbesserten Lesbarkeit und Verständlichkeit der Quelltexte, Module leichter isolierbar und damit besser testbar werden, wodurch der Aufwand für Fehleranalysen gesenkt wird und Softwaresysteme leichter wartbar werden. Zusätzlich wird für die Zukunft erhofft, durch Refaktorisierung existierende Programme problemlos erweitern zu können. Hinsichtlich der erwarteten Ergebnisse ist eine umfassende Auseinandersetzung mit dem Begriff Refaktorisierung im Hinblick auf heutige Softwareprojekte sinnvoll. Ziel muss sein, die Integration eines Refaktorisierungsvorgangs in die konkrete Softwareentwicklung möglichst einfach zu gestalten. Dafür bietet sich eine Automatisierung in Form von Refaktorisierungswerkzeugen an. In der vorliegenden Arbeit soll ausgelotet werden, wie automatisierte Refaktorisierungen für programmiersprachenübergreifende Softwareprojekte erfolgen können. Der Fokus wird auf solche Stellen gelegt, an denen die unterschiedlichen Programmiersprachen miteinander interagieren. Jedoch muss deutlich unterschieden werden zwischen der Zusammenarbeit von ausschließlich solchen Programmiersprachen, die ein statisches Typsystem besitzen, und der Zusammenarbeit von statisch typgeprüften Sprachen mit Programmiersprachen, die hier als zweckmäßige Einordnung mit dem Begriff „dynamisch“ belegt werden. Statisch typgeprüfte Programmiersprachen, in denen die Typregeln schon während des Übersetzungsvorgangs kontrolliert werden, harmonieren besser mit dem Prozess des Sammelns von notwendigen Informationen für einen Refaktorisierungsvorgang als dynamische Sprachen. Beim Refaktorisieren werden Informationen genutzt, die in statisch typgeprüften Sprachen nahezu vollständig (mit wenigen Ausnahmen) zur Verfügung stehen. Beispielsweise kann die Bindung einer Methode an ihre Definition statisch abgeleitet werden. Dies ist in einer nicht statisch typgeprüften Sprache ungleich schwerer. Hier kann zwar die Reihenfolge der Anweisungen abgebildet werden, aber spätestens bei Aufruf einer Methode, die zur Laufzeit an jeweils zwei unterschiedliche Objekte gebunden werden kann, sind die gefundenen Informationen über das Programm nicht mehr eindeutig. Damit stehen die notwendigen Informationen für eine automatisierte Refaktorisierung nicht in der Klarheit zur Verfügung, wie es in statisch typgeprüften Sprachen der Fall ist. 6 Das Thema dieser Ausarbeitung wird eingegrenzt auf solche sprachübergreifende Projekte, in denen statisch typisierte und dynamische Programmiersprachen in einer Software genutzt werden. In der Einleitung wird zuerst skizziert, zu welchem Zeitpunkt Refaktorisierungen einsetzen sollen. Hier wird der Refaktorisierungsprozess auf startbare Programme mit beobachtbarem Verhalten eingegrenzt. Weiter werden exemplarisch Refaktorisierungswerkzeuge historisch verfolgt, um Ideen und Ansätze, aber auch existierende Schwierigkeiten solcher Werkzeuge in die weitere Diskussion mit einbeziehen zu können. Zur Konkretisierung des Themas der vorliegenden Arbeit wird dann eine aktuelle Forschung1 vorgestellt, in der Refaktorisierungswerkzeuge als eigenes Forschungsgebiet diskutiert werden. Dieser Ansatz wird als Grundlage der Ausarbeitung und der beigelegten Software genutzt. Als Motivation für die Notwendigkeit, Refaktorisierungswerkzeuge für sprachübergreifende Projekte zu entwickeln, werden Vorteile solcher mehrsprachigen Projekte beschrieben. Am Ende der Einleitung werden die herangezogene Entwicklungsumgebung und die verwendeten Programmiersprachen vorgestellt und der Aufbau der Ausarbeitung skizziert. 1.1 Refaktorisierung – ein in der Softwareentwicklung integrierter Prozess Bei klassischen Vorgehensmodellen der Softwareentwicklung kann Flexibilität oft schwer realisiert werden. Es lässt sich kaum verhindern, dass in Projekten mit einem anfänglich ausgefeilten Entwurf eine nachträgliche Änderung der Spezifikation ein notwendiges Redesign mit sich bringt, das im schlechtesten Fall dazu führt, dass große Teile neu programmiert werden müssen oder das gesamte Produkt neu entworfen werden muss. Workarounds, also Programmcodes, durch die ein Projekt nachträglich an neue Ziele angepasst werden soll, machen den Quellcode oft unübersichtlich und lassen ihn mit der Zeit strukturlos und unwartbar werden. Martin Fowler, der unterstützt von Kent Beck, William Opdyke, Don Roberts, John Brant und Erich Gamma im Jahr 1999 einen Satz von Refaktorisierungsregeln aus dem Erfahrungsfundus von Entwicklern und Entwicklerinnen katalogisiert hat, folgert hieraus: „Ohne Refaktorisieren zerfällt das Design eines Programms mit der Zeit.“ [Fow05, S. 43] Eine stufenweise Refaktorisierung im Nachhinein könnte in dieser Phase helfen. Jedoch existiert in diesem Zusammenhang die Angst vor hohen Kosten und großem Zeitaufwand. Andere Softwareentwicklungsprozesse, die unter dem Begriff Agile Softwareentwicklung zusammengefasst werden, integrieren von vornherein den Prozess der Refaktorisierung in den Ablauf der Softwareerstellung. Im agilen Ansatz wird es sogar als Ziel formuliert, Software flexibel zu halten (vgl. [Bec00]). In der Phase der Entwicklung sollen fortwährend neue Ideen für ein Produkt einfließen können. Refaktorisierung des Quellcodes setzt dem agilen Ansatz folgend nicht erst nach Fertigstellen eines Programms ein. Genauso wenig wird die Strukturänderung von Codes als paralleles Vorhaben neben (verstanden als abgelöst von) der konkreten Softwareentwicklung betrachtet. Refaktorisierung gilt im agilen Ansatz als ein elementarer Prozess, der (eng verknüpft) mit zur 1 Vgl. http://www.fernuni-hagen.de/ps/prjs 7 Entwicklung gehört. In der Praxis ist der Refaktorisierungsprozess mit der Implementierung kombiniert. Um den Begriff Refaktorisierung inhaltlich zu konkretisieren, soll noch einmal Fowler herangezogen werden. Er beschreibt Refaktorisierung zum einen mit Fokus auf den Gebrauch als Substantiv (Refaktorisierung) und zum anderen auf den Gebrauch als Verb (refaktorisieren): Refaktorisierung in Substantivform wurde zuvor schon aufgegriffen als eine Änderung an der internen Struktur zur besseren Verständlichkeit (vgl. [Fow05, S. 41]). Erweiternd fügt Fowler dem Begriff eine Aktion hinzu: „Refaktorisieren(Verb): Eine Software umstrukturieren, ohne ihr beobachtbares Verhalten zu ändern, indem man eine Reihe von Refaktorisierungen anwendet.“ [Fow05, ebd.]) Fowler zeigt damit eine Technik. Die Änderungen sollen in kleinen, abgeschlossenen Einheiten vorgenommen werden. Zwingend ist, nach jedem einzelnen Schritt zu testen, ob das Verhalten gleich geblieben ist. Mit dieser Technik weist Fowler den Weg, den Refaktorisierungsprozess manuell – parallel zur Softwareentwicklung – vorzunehmen. Mit Hilfe eines Katalogs von Regeln kann das Design nun verbessert werden, nachdem größere Teile der Software bereits implementiert sind. Fowler stellt sich damit dem angesprochenen Problem, dass ein Softwaredesign vorher zwar geplant wird, im Zuge der Implementierung jedoch oft verloren geht. Spätestens dann, wenn sogenannte „Code Smells“ (vgl. [Fow05, S. 67-82] oder als erweiterter Katalog [Mar09, S. 337-372]) in einer Software entdeckt würden, könne durch ein schrittweises Refaktorisieren ermöglicht werden, die Struktur im Nachhinein zu verbessern, und die Wahrscheinlichkeit gering gehalten werden, dabei neue Fehler einzuführen. Fowler geht sogar soweit, dass er behauptet, ein mit einem chaotischen Design begonnenes Projekt könne durch das schrittweise Refaktorisieren radikal verbessert werden, und spitzt diese Aussage noch einmal darauf zu, dass ein von vornherein vollständig durchdachtes Design gar nicht notwendig sei: „Sie werden feststellen, dass das Design, anstatt vollständig vorher zu erfolgen, kontinuierlich während der Entwicklung stattfindet.“ [Fow05, S. xix] Fowler formuliert dann für den aktiven Refaktorisierungsvorgang (refaktorisieren) die Aussage: „Refaktorisieren ist ein Prozess, ein Softwaresystem so zu verändern, dass das externe Verhalten nicht geändert wird, der Code aber eine bessere interne Struktur erhält.“ [Fow05, S. xviii] Stefan Roock und Martin Lippert fassen die Refaktorisierung ebenso als kontinuierliches Entwerfen auf und sehen im Jahr 2004 darüber hinaus Refaktorisierung mittlerweile als festen Bestandteil, vor allem in groß angelegten, agilen Entwicklungsprojekten: „Mit dem ständigen Refactoring der Software gehen wir in die entgegengesetzte Richtung: Refactoring und Entwerfen werden zum Bestandteil der täglichen Entwicklungsarbeit. Es wird damit nicht weniger entworfen als in klassischen Projekten. Die Entwurfsaufwände werden lediglich gleichmäßiger über den Entwicklungsprozess verteilt.“ [RL04, S. 13] Roock und Lippert möchten ein – eventuell durch Schwachstellen notwendig gewordenes – Redesign des Gesamtsystems verhindern. Sie fordern, dass eine Refaktorisierung der Software in den Entwicklungsprozess mit eingebaut wird. Bei auftretenden Schwachstellen soll, ebenso wie bei Fowler, in möglichst kleinen Schritten restrukturiert werden (vgl. [RL04, S. 18-25]). Als Prämisse der vorliegenden Ausarbeitung soll die Implementierungsphase als zen- 8 trales „Paket“ verstanden werden, in dem das Refaktorisieren, das Programmieren und darüber hinaus das heute vor allem in agilen Projekten eingesetzte Testen von Modulen2 und Verhalten (Behavior Driven Development3 ) gebündelt wird. Damit entfällt jegliche Idee einer eigenständigen „Phase der Refaktorisierung“, die zusätzlich Zeit fordern würde. Aus dem agilen Ansatz wird also übernommen, dass das Refaktorisieren einhergehend mit der Implementierung stattfindet. Aus der Perspektive des oder der Programmierenden wird der Refaktorisierungsprozess in dieser Phase in einen Handlungsablauf eingefasst und der Vorgang systematisiert. Abgegrenzt wird der Begriff Refaktorisierung von seiner Verwendung in Phasen der Software-Erstellung, in denen noch kein sichtbares Verhalten existiert, beispielsweise, wenn ein Programm noch nicht lauffähig und damit der Programmablauf noch nicht beobachtbar ist. Damit fallen Designänderungen vor der Implementierung genauso wenig unter den hier verwendeten Begriff von Refaktorisierung wie Architekturänderungen. Grenzfälle, beispielsweise eine Änderung von Quellcode, ohne dass ein Programm lauffähig ist, werden durch die klare Definition herausgefiltert, dass das Verhalten beobachtbar sein muss. Strukturänderungen am Ende der Implementierung werden durch die vorherige Aussage eingeschlossen. Refaktorisierung impliziert nicht zwingend das formulierte Ziel einer Strukturverbesserung. Nur die Programmierenden selbst können entscheiden, welche Umstrukturierungen sinnvoll sind im Hinblick auf die von ihnen erstellte Software. Anhaltspunkte hierfür können sich aus den festgehaltenen Erfahrungen ergeben, die in den letzten Jahren von unterschiedlichen Autoren und Autorinnen veröffentlicht wurden. Hier existieren automatisierte Ansätze, beispielsweise Smell Detection (vgl. u. a. [Sli05]), auf die an dieser Stelle jedoch nicht weiter eingegangen wird. 1.2 Werkzeuge für den Refaktorisierungsprozess Selbst wenn zu jeder Zeit eine wünschenswerte Strukturänderung von Quellcode erfolgt, die dann im positiven Fall zur Übersichtlichkeit beiträgt, zeigt sich in einer manuellen Refaktorisierung das Problem, dass Abhängigkeiten im Code nicht unmittelbar und damit mühelos von den Programmierenden nachverfolgbar sind. Denn den Überblick über Quellcode zu behalten, der auf mehrere Dateien in unterschiedlichen Ordnern bzw. Paketen verteilt und dazu mit programmiersprachenspezifischen Sichtbarkeitsregeln belegt wurde, ist spätestens in einem Team von mehreren Entwickelnden schwer zu leisten. Mit anderen Worten: Manuelles Refaktorisieren ist fehleranfällig. Durch den skizzierten Ablauf, die Struktur in kleinen Schritten zu ändern, und durch die jeweils sofort notwendigen Tests auf semantische Gleichheit ist eine manuelle Refaktorisierung zudem sehr zeitaufwändig. Wie schon durch den Begriff „automatisiert“ angedeutet, wurden und werden daher im Zuge der Diskussion um Refaktorisierung Werkzeuge entwickelt, die den Prozess der Strukturänderung von Quellcode unterstützen. Programmierenden werden für Änderungswünsche automatisierte Angebote gemacht oder die gewünschte 2 3 Exemplarisch http://www.junit.org, http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/ Exemplarisch http://cukes.info 9 Änderung vollständig übernommen. Die im Folgenden exemplarisch herausgegriffenen frühen Werkzeuge für eine Änderung der Struktur eines Quellcodes sollen den Blick auf die Entstehung heutiger und kommender Automatisierungen von Refaktorisierungsvorgängen lenken, mit dem Ziel, Refaktorisierungswerkzeuge im Kontext sprachübergreifender Projekte im weiteren Verlauf einordnen und damit Möglichkeiten und Grenzen eines solchen Werkzeugs auch historisch diskutieren zu können. Die historische Sicht führt darüber hinaus zu der Frage nach dem Stand der Entwicklung heutiger Werkzeuge. Denn die zuvor skizzierten Probleme der Fehleranfälligkeit und natürlich auch des immensen Zeitaufwandes durch manuelles Refaktorisieren machen es notwendig, existierende Werkzeuge, wenn möglich, weiter zu entwickeln und zu verbessern. In einem Folgeabschnitt wird dazu der für diese Ausarbeitung herangezogene Ansatz vorgestellt, in dem nach einem einheitlichen Standard für Refaktorisierungswerkzeuge gesucht wird. 1.2.1 Refaktorisierungswerkzeuge aus historischem Blickwinkel Nach einer Abhandlung über Strukturierungswerkzeuge für Softwaresysteme von Thomas Dudziak und Jan Wloka aus dem Jahre 2002 ist der Anfang der Auseinandersetzung mit dem skizzierten Begriff Refaktorisierung zeitlich zumindest ab Anfang der 70er Jahre einzuordnen, als die Diskussion über das GOTO-Statement in imperativen Programmen ihren Höhepunkt hatte, wenn nicht sogar davor: „Refactoring is not new - in fact it has been done unknowingly at least since the introduction of structured programming, if not earlier.“ [DW02, S. 10] Sie begründen diese Aussage damit, dass die Sprunganweisung GOTO ersetzt wurde, ohne das Verhalten der jeweiligen Software zu verändern. Diese Regel, Restrukturieren ohne Verhaltensänderung, entspricht zwar dem heute eingegrenzten Begriff der Refaktorisierung. Ob damalige Restrukturierungen im Zuge der Diskussion um GOTO jedoch schon als ein Anfang von Refaktorisierung verstanden werden kann, ist m. E. zu diskutieren. Denn Dudziak und Wloka ignorieren an dieser Stelle, dass es damals im Grunde nur um die Beweisführung ging, dass die Sprunganweisung GOTO durch die Kontrollstruktur Schleife, ggf. durch die Einführung von booleschen Variablen, ersetzt und dann mit dem endgültig veränderten Code weiter gearbeitet werden kann. Den Quellcode selbst leserlicher zu gestalten, war für diesen Beweis nicht relevant: „In [2] Guiseppe Jacopini seems to have proved the (logical) superfluousness of the go to statement. The exercise to translate an arbitrary flow diagram more or less mechanically into a jump-less one, however, is not to be recommended. Then the resulting flow diagram cannot be expected to be more transparent than the original one.“ [Dij68] Jedoch soll im Hinblick auf eine automatisierte Refaktorisierung die Aufmerksamkeit darauf gelenkt werden, dass zu jener Zeit im Zuge der Auseinandersetzung mit Restrukturierungsprozessen erste Werkzeuge entwickelt wurden, die eine automatisierte Strukturänderung boten. Eines der ersten entstandenen Werkzeuge war Mitte der 70er Jahre die „Structuring Engine“ für die Sprache Fortran, die jedoch den Objekt- 10 code und die Laufzeit der Software stark anwachsen ließ. Stefan Eickner und Thomas Schnieder vermuten dazu in einem Arbeitsbericht aus dem Jahr 1992, dass sich das Werkzeug deswegen wahrscheinlich nicht durchsetzen konnte: „Das Tool zerlegt das Programm in Prozedurblöcke und ersetzt vorwärtsgerichtete Goto-Anweisungen durch Ifthen-else-Konstrukte, rückwärtsgerichtete Goto-Anweisungen durch Do-until-Schleifen sowie Goto-Anweisungen mit Sprungzielen außerhalb der Blöcke durch Prozeduraufrufe. Der Restrukturierungsprozess lässt den Objektcode um 10% - 40% und auch die Laufzeit des Programmes um bis zu 8% anwachsen. Dies ist als ein wesentlicher Grund dafür anzusehen, daß sich das Tool in der Praxis nicht durchgesetzt hat.“ [ES92, S. 17-18] Ab Mitte der 80er Jahre wurde die Restrukturierung von prozeduralen und objektorientierten Softwaresystemen weiterverfolgt. Für imperative Programmiersprachen untersuchte William G. Griswold das Restrukturieren ohne Verhaltensänderung. Er verwendete die semi-funktionale Programmiersprache Scheme4 . Die Programmiersprache bot sich an, da sie imperative Merkmale und darüber hinaus eine relativ einfache Syntax besitzt (vgl. [Gri91, S. 20]). Mit Hilfe eines für Scheme als Paket angebotenen Abhängigkeitsgraphen entwickelte Griswold einen Ansatz für ein Werkzeug zur Restrukturierung von Quellcode. Die Grundlage für automatisierte Refaktorisierungen objektorientierter Systeme schufen u. a. Ralph E. Johnson und William Opdyke Anfang des Jahres 1990. Sie gingen davon aus, dass objektorientierte Frameworks nicht von vornherein vollständig geplant werden könnten und es daher einer Umstrukturierung bedürfte. Sie untersuchten die Umstrukturierungsmöglichkeiten bezüglich Aggregation und Vererbung5 für die Sprache C++. Als Vorteil gegenüber prozedural aufgebauter Software nahmen Johnson und Opdyke an, dass bei einer notwendigen Erweiterung oder Modifizierung bestehender Software objektorientiert aufgebaute Systeme geeigneter wären, da hier oft durch ein Hinzufügen von Klassen die Abhängigkeiten aufgelöst werden könnten. Jedoch würden trotzdem ebenso Veränderungen objektorientierter Software nötig (vgl. [JO93, S. 1ff.]). Opdyke ermittelte aus der Beobachtung von Refaktorisierungen von Programmen Eigenschaften, die bei einer automatisierten Refaktorisierung nicht verletzt werden dürfen. Er befürchtete, dass ein erneutes Übersetzen des Quellcodes nach der Refaktorisierung zwar die meisten Fehler, jedoch nicht eine Veränderung der Semantik von Referenzen und Operationen vor und nach der Refaktorisierung entdecke (vgl. [Opd92, S. 39-42]). Eines der bekanntesten frühen Werkzeuge, das auf der Grundlage der Arbeiten von Johnson und Opdyke entwickelt wurde, ist der von John Brant und Donald Bradley Roberts veröffentlichte Refactoring Browser6 für Smalltalk-Programme. Roberts lieferte die theoretischen Grundlagen zu dem Browser in einer Dissertation (vgl. [Rob99]). Der Refactoring Browser nimmt Methodenumbenennungen zur Laufzeit vor und basiert damit auf einem anderen Konzept als die eingangs skizzierte Idee von Refaktorisierungen, in denen die Quelltexte verändert werden, ohne dass das jeweilige Programm ausge4 Eine eigentlich funktionale Sprache, in der auch imperativ programmiert werden kann, vgl. http://www.cs.hut.fi/Studies/T-93.210/schemetutorial/node9.html, Abruf am 27.01.2012. 5 Mit der Einschränkung, dass Mehrfachvererbung und Methodenüberladung ausgeschlossen wurden (vgl. [Opd92, S. 35]). 6 http://st-www.cs.illinois.edu/users/brant/Refactory/RefactoringBrowser.html 11 führt wird. Da Smalltalk eine dynamische Programmiersprache ist, werden Ideen des Refactoring Browsers im Folgenden weiter untersucht (s. Abschnitt 5.2). Die heute etablierten Werkzeuge zur Unterstützung von Refaktorisierungen sind oft in Entwicklungsumgebungen integriert oder als Plugin einladbar. Darüber hinaus stehen für einige Programmiersprachen eigene Programme in Anlehnung an den Refactoring Browser für Smalltalk zur Verfügung.7 Bisher sind die Refaktorisierungswerkzeuge sehr eng verknüpft mit der jeweiligen Entwicklungsumgebung (IDE) und Programmiersprache. Für sprachübergreifende Werkzeuge muss jedoch nach einer allgemeinen Basis gesucht werden. Interessant werden hier exemplarisch Entwicklungsumgebungen wie Eclipse, Netbeans oder RubyMine,8 in denen von Haus aus mehrere Sprachen integriert sind und die deswegen dafür prädestiniert sind, Refaktorisierungsumgebungen zu vereinheitlichen oder sogar sprachübergreifende Refaktorisierungswerkzeuge anzubieten. 1.2.2 Refaktorisierungswerkzeuge als eigenes Fachgebiet Friedrich Steimann wirft im Jahr 2010 in einem Artikel zu korrekten Refaktorisierungen ein Problem auf: Existierende Refaktorisierungswerkzeuge würden nicht immer korrekt arbeiten (vgl. [Ste10, S. 24-25]). Er spricht sich, als Lösungsansatz, für den Bau von Refaktorisierungswerkzeugen als eigenständige Disziplin aus. Denn verglichen mit dem Bau von Compilern fehlen ihm anerkannte Ansätze, die identifizierten Refaktorisierungsprobleme zu lösen. Er bezieht sich auf in Entwicklungsumgebungen integrierte Werkzeuge, die trotz hohem Qualitätsstandard dem Praxistest nicht standhalten würden: „Vollständige und korrekte Spezifikationen von Refaktorisierungen sind also nur im Rahmen der Entwicklung von Refaktorisierungswerkzeugen sinnvoll. Leider sind auch die heute verfügbaren Refaktorisierungswerkzeuge alles andere als korrekt [...].“ [Ste10, S. 24] Zusammengefasst schätzt Steimann existierende Werkzeuge als bisher nicht ausreichend ein und fordert eine Standardisierung, damit auf Dauer die notwendige Qualität erzielt werden könne (vgl. [Ste10, S. 29]). Daraus folgt die Frage, welche Lösungsansätze an dieser Stelle als weiterführend betrachtet werden können. Steimann bevorzugt einen constraint-basierten Ansatz. Denn der Versuch, die existierenden Leitfäden und Muster direkt in Refaktorisierungsprogrammen umzusetzen, beinhaltet für Steimann die Schwierigkeit, sich in zu vielen Fallunterscheidungen zu verlieren. Ebenso distanziert er sich von dem Ansatz, Programme als Graphen darzustellen und damit Refaktorisierungen als Graphentransformationen durchzuführen (vgl. [Ste10, S. 27]). Constraints seien dagegen lesbar, da in den Regeln beschrieben wird, was erfüllt werden muss. Steimann sieht für die Zukunft ein Baukastenprinzip: „Anstatt – wie bisher – die Vorbedingungen und die zur Durchführung notwendigen Schritte für jede Refaktorisierung getrennt zu formulieren und anschließend mit einigem Aufwand in den Kontext der jeweiligen Entwicklungsumgebung einzubetten, wäre es hilfreich, wenn Refaktorisierungswerkzeuge nach dem Baukastenprinzip aus erprobten Komponenten zusammenge7 8 Eine Liste gängiger Refaktorisierungswerkzeuge im Anhang. http://www.eclipse.org, http://www.netbeans.org, http://www.jetbrains.com/ruby/ 12 stellt werden könnten und wenn sie von den Entwicklungsumgebungen stets die gleiche Infrastruktur benötigen.“ [Ste10, S. 29] Dazu will Steimann ein Framework nutzen, in dem Constraintregeln einheitlich für alle Sprachen formuliert werden können. Des Weiteren müssen die veränderten Programme getestet werden können. Infolgedessen und als Basis der vorliegenden Arbeit über Refaktorisierungswerkzeuge für sprachübergreifende Projekte wird die deklarative Sprache Refacola9 (für constraintbasierte Refaktorisierungen) herangezogen. Refacola ist ein Schritt in die von Steimann vorgeschlagene Richtung. Refacola soll für verschiedene Programmiersprachen genutzt werden können; exemplarisch werden momentan Eiffel und Java unterstützt. Sprachspezifisch müssen die Programmelemente mit ihren Eigenschaften und Wertebereichen bestimmt werden, einschließlich der Beziehungen, die sich hieraus ergeben, und den Constraintregeln, die aufgestellt werden müssen (vgl. [SKP11]). Ebenso auf Basis von Constraints arbeiten Daniel Speicher, Tobias Rho, Günter Kniesel an einem Werkzeug für eine logikbasierte Infrastruktur zur Codeanalyse (vgl. [SRK07]). Das entwickelte Werkzeug JTransformer basiert auf Ausarbeitungen von Tip, Kiezun und Bäume (vgl. [TKB03]). Tip et al. bearbeiteten das Konzept für Refaktorisierungen weiter und integrierten Ideen u. a. in die Eclipse Entwicklungsumgebung (vgl. [TFKEBS09] und [SDSTT10]). Im Hinblick auf die vorliegende Ausarbeitung muss gefragt werden, ob die von Steimann, Speicher et al. und Tip et al. vorgeschlagenen Ansätze auch dann greifen, wenn mit Hilfe eines Refaktorisierungswerkzeugs sprachübergreifende Projekte refaktorisiert werden sollen, vor allem mit Blick darauf, dass die Sprachen sich in ihren Konzepten ggf. grundlegend unterscheiden. Dass solche Werkzeuge heute notwendig sind, weil sprachübergreifende Projekte genutzt werden, soll im Folgenden dargestellt werden. 1.3 Flexibilität durch eine programmiersprachen-übergreifende Entwicklung Für die vorliegende Ausarbeitung wird die Java Virtual Machine (JVM) als Plattform für eine programmiersprachenübergreifende Entwicklung herangezogen. Die Java Virtual Machine und die ursprünglich dafür entwickelte Programmiersprache Java bieten sich im Hinblick auf sprachübergreifende Refaktorisierungswerkzeuge zum einen deswegen an, da es für Java bereits Werkzeuge für eine automatisierte Umstrukturierung gibt, auch wenn diese oft als nicht ausreichend kritisiert werden und zum anderen, da heute viele weitere Programmiersprachen existieren, die auf dieser Plattform lauffähig sind. Ebenso könnte an dieser Stelle jedoch auch das .NET Framework als Basis herangezogen werden. Auf der .NET Plattform laufen genauso unterschiedliche Programmiersprachen, beispielsweise die zu den objektorientierten Programmiersprachen zählende Sprache C# oder die funktionale Programmiersprache F# (an OCaml angelehnt). In einem MSDN-Artikel wird vermerkt: „Auch wenn Programmierer, die über beide Ohren in der objektorientierten Entwicklung stecken, möglicherweise eine andere Ansicht haben, sind funktionale Programme häufig für bestimmte Arten von Anwendungen einfacher zu 9 http://www.feu.de/ps/prjs/refacola 13 schreiben und zu verwalten.“ [New08] Es lohnt sich also, innerhalb eines Projektes zielgerichtet unterschiedliche Programmiersprachen zu nutzen, nämlich solche, durch die sich die in der Softwareerstellung jeweils auftretenden Probleme einfacher lösen lassen. Das Verflechten mehrerer Programmiersprachen in einem Projekt soll im Weiteren verfolgt werden. Im Hinblick auf den eingangs festgelegten Schwerpunkt der Arbeit, dem Zusammenspiel sogenannter dynamischer und statisch typgeprüfter Programmiersprachen, folgt eine Diskussion des Begriffs dynamisch, eine Skizze der Vorteile der Interaktion unterschiedlicher Programmiersprachen und ein Überblick über die große Anzahl schon existierender Sprachen, die auf der JVM laufen. Damit soll die Notwendigkeit der zukünftigen Entwicklung programmiersprachenübergreifender Refaktorisierungswerkzeuge aufgezeigt werden. 1.3.1 Vorteile der Interaktion mit dynamischen Programmiersprachen Um den Nutzen einer Interaktion statisch typgeprüfter und dynamischer Programmiersprachen zu erkunden, muss m. E. der Begriff dynamische Programmiersprache genauer bestimmt werden, denn es existiert bis heute keine einheitliche Definition dieses Begriffs. Kontrovers diskutiert,10 aber dennoch ein eventuell für diese Arbeit aufschlussreicher Aufsatz wurde im Jahr 2005 von Oscar Nierstrasz, Alexandre Bergel, Marcus Denker, Stephane Ducasse, Markus Gälli und Roel Wuyts veröffentlicht. Sie behaupten, dass statische Sprachen von Natur aus nicht geeignet sind für die Realisierung von Anwendungen mit dynamischen Anforderungen: „Inherently static languages will always pose an obstacle to the effective realization of real applications with essentially dynamic requirements.“ [NBDDGW05, S. 1] Sie schließen daraus, dass dringend eine Forschung nach Programmiersprachen notwendig ist, die Veränderungen zur Laufzeit eines Programms unterstützen (vgl. ebd., S. 2). Denn die Forschung sei eingefahren auf statische Sprachen (vgl. ebd., S. 10). Es stellt sich daraus folgend die Frage, wo der Unterschied zwischen dynamischen und statisch typgeprüften Sprachen liegt. Allgemein gilt als grundlegendes Kennzeichen dynamischer Programmiersprachen, dass solche Sprachen zur Laufzeit Tätigkeiten ausführen, die andere Programmiersprachen nicht oder zur Übersetzungszeit umsetzen. Einerseits werden Programmiersprachen grob eingeordnet nach den Konzepten, die die jeweilige Sprache implizit anbietet. Wird eine Reihe elementarer (folgend umrissener) dynamischer Konzepte11 unkompliziert unterstützt, gilt die Sprache als dynamische Programmiersprache. Unkompliziert deswegen, weil sich eine dynamische Programmiersprache nicht unbedingt dadurch abgrenzt, dass eine dagegen stehende, nicht-dynamische Sprache eine der im Folgenden skizzierten Techniken nicht beherrscht, sondern die Abgrenzung erfolgt durch die Natürlichkeit des Einsatzes dieser Technik in der jeweiligen Programmiersprache. Es ist daher schwer, den Begriff zu fassen. 10 Vgl. dazu die Diskussion: http://lambda-the-ultimate.org/node/852#comment-7783, Abruf am 18.01.2012. 11 Exemplarisch: http://www.lesscode.de/initiative/dynamische-programmiersprachen 14 Als dynamische Konzepte gelten beispielsweise solche, in denen es möglich ist, eine Variable zur Laufzeit an Werte mit unterschiedlichen Typen zu binden, in denen es möglich ist, über eine Eingabe direkt Codes zur Ausführung zu bringen („eval“) oder in denen Reflexion existiert. Reflexion bietet dann weiter die Möglichkeit, Strukturen von u. a. Klassen oder Funktionen zur Laufzeit zu untersuchen und dynamisch zu nutzen oder entsprechende Informationen über Objekte zu erhalten. Jedoch ist das Aufzählen solcher Konzepte nicht ausreichend, um den Begriff dynamische Programmiersprache klar zu umreißen, da nicht alle als dynamisch geltenden Sprachen alle Konzepte unterstützen. Als einfache Möglichkeit soll deshalb von der konkreten Seite des Nutzens einer Programmiersprache ausgegangen werden, indem gefragt wird, wo solche Programmiersprachen heute typischerweise eingesetzt werden. Damit lassen sich eine Reihe von Programmiersprachen einordnen nach ihrer Möglichkeit, elegante Lösungen für Probleme zu bieten, die bei statisch getypten Sprachen nur umständlich gelöst werden können. Beispiele wären Verbindungen zwischen Schnittstellen (Glue Code), Entwicklung von Benutzeroberflächen, Einsatz von Webservices, schnelles Erstellen von Prototypen, Verwalten von Projekt-Infrastrukturen mittels Build-Skripten, Entwickeln von Testskripten u. v. m. Wenn also in einem einzigen Projekt mehrere Programmiersprachen genutzt werden, können die Vorteile und Stärken der jeweiligen Sprachen genutzt werden. Für die Java Virtual Machine wurden vorwiegend dynamische Programmiersprachen portiert oder entwickelt, um mit der statisch typgeprüften Sprache Java in Kommunikation zu treten.12 1.3.2 Java Virtual Machine als Basis mehrerer Programmiersprachen Die weite Verbreitung der Java Virtual Machine und die Abstraktion der darunter liegenden Maschinen-Architektur ermöglicht die Entwicklung und den Einsatz von Komponenten, Frameworks und Services auf unterschiedlichen Betriebssystemen. Jedoch wird in vielen Gruppen im Umfeld von Softwareentwicklung diskutiert, dass die ursprünglich für die Plattform entwickelte Programmiersprache Java selbst für bestimmte Einsatzbereiche nicht flexibel genug sei.13 Darüber hinaus gilt Java verglichen mit Sprachen wie Ruby, Python, Lisp oder Smalltalk als weniger elegant und ausdrucksstark. Als eine der ersten neu entwickelten Programmiersprachen versuchte Groovy14 im Jahr 2003, diesen Nachteil zu beheben. Groovy ist dynamisch typisiert und bietet Closures, Operatorüberladung, eine native Darstellung für BigDecimal und BigInteger u. v. m. Groovy wird in Bytecode übersetzt und ausgeführt. Java-Klassen können daher GroovyKlassen nutzen und umgekehrt 12 Es existieren jedoch auch Ausnahmen wie die Programmiersprache Scala, die durch ein statisches Typsystem nicht in diese Kategorie passt. 13 Vgl. u. a. http://www.oio.de/public/java/groovy/groovy-einfuehrung.htm, http://openbook.galileocomputing.de/javainsel/javainsel_01_002.html, http://it-republik.de/jaxenter/artikel/Groovy-fuer-Java-Entwickler-DynamischeProgrammierung-auf-der-Java-Plattform-1065.html, alle Abruf am 18.01.2012. 14 http://groovy.codehaus.org 15 Etwas früher, im Jahr 2000, entstand Jython. Diese Programmiersprache ist nicht neu entwickelt wie Groovy, sondern eine reine Java-Implementierung der seit Anfang 1990 existierenden Programmiersprache Python15 . Jython ermöglicht die Ausführung von Python-Programmen auf der Java-Plattform und sämtliche Java-Bibliotheken können in Python-Programme importiert und genutzt werden. Im Kontext aktueller, auf der Java Virtual Machine lauffähiger Sprachen werden an dieser Stelle noch die im Jahr 2003 als Forschungssprache entwickelte und als Multiparadigmen-Sprache bezeichnete Sprache Scala16 und die im Jahr 2007 erschienene Sprache Clojure17 hervorgehoben. Scala ist im Kern eine objektorientierte Sprache mit statischem Typsystem, besitzt jedoch darüber hinaus funktionale Möglichkeiten. Clojure ist ein speziell für die Java Virtual Machine entwickelter Lisp-Dialekt, der insbesondere durch seine Unterstützung für die Entwicklung von nebenläufigen Anwendungen heraussticht. Aus dem existierenden Pool der JVM-fähigen Programmiersprachen wird für diese Arbeit exemplarisch JRuby18 (eine Implementierung des Ruby-Interpreters in Java) herangezogen. JRuby schafft eine Brücke zwischen Ruby und Java und vereint damit die Vorzüge dieser beiden Sprachen. Die Sprache Ruby19 selbst erschien im Jahr 1995. Sie gilt als dynamische Sprache. Ruby ist rein objektorientiert und wurde darüber hinaus als Multiparadigmen-Sprache entworfen. Damit steht es dem/der Entwickelnden offen, weitere Programmierparadigmen zur Erstellung seiner/ihrer Programme zu nutzen (s. Kapitel 3). „The Ruby programming language was released to the public in 1995 and gained widespread adoption in 2006. A multipurpose language that focuses on simplicity and productivity, it combines the best features of many compiled and interpreted languages, such as easy development of large programs, rapid prototyping, almost-real-time development, and compact code. Ruby is a reflective, dynamic, and interpreted objectoriented scripting language, and JRuby is a Java programming language implementation of the Ruby language syntax, core libraries, and standard libraries.“ [Paw07] Mit JRuby wird, vergleichbar mit den oben genannten Sprachen, die Nutzung der Java Virtual Machine für die Programmiersprache Ruby ermöglicht. Der Sprachumfang von Ruby wird von JRuby nahezu vollständig implementiert. JRuby ermöglicht darüber hinaus die Interaktion von Java nach Ruby und von Ruby nach Java. JRuby wird momentan ständig weiterentwickelt und an gegenwärtige Anforderungen angepasst. Im Jahr 2009 wurde eine Version von JRuby vorgestellt, die auf der Software-Plattform für mobile Geräte, Android, ausführbar ist.20 Wenn Entwickler und Entwicklerinnen heute für konkrete Problemstellungen nach einfachen und effizienten Lösungen suchen und sich dafür auf unterschiedliche Sprachen mit jeweils unterschiedlichen Sprachkonzepten stützen, stellt sich die Frage, ob für sprachübergreifende Projekte Refaktorisierungswerkzeuge im Einsatz sind, die den 15 http://www.python.org http://www.scala-lang.org 17 http://clojure.org 18 http://www.jruby.org 19 http://www.ruby-lang.org 20 http://ruboto.org 16 16 unterschiedlichen Sprachkonzepten gerecht werden. Dass sprachübergreifende Refaktorisierungswerkzeuge eine besondere Herausforderung darstellen, soll durch das Beispiel motiviert werden, dass eine Variable in Ruby keinen Typ besitzt. „Der Typ von Objekten wird zur Laufzeit überprüft, allerdings sind Variablen typenlos (sozusagen void). Dementsprechend gibt es keine Type-Casts und keine typedefs.“21 Das macht Ruby als dynamische Sprache flexibel, eröffnet jedoch ein komplexes Problemfeld für sprachübergreifende Aufrufe zu einer statisch typgeprüften Sprache wie Java. Denn es kann durch eine Analyse des Quellcodes nicht entschieden werden, auf welche Objekte die Ruby-Variable zur Laufzeit referenzieren wird. 1.4 Sprachübergreifende Refaktorisierungswerkzeuge JRuby als Brücke zwischen Java und Ruby birgt, wie zuvor skizziert, Vorzüge für Projekte, in denen für bestimmte Problemstellungen nach einem ausdrucksstarken Code gesucht wird und trotzdem im vollen Umfang auf existierende Java-Bibliotheken zurückgegriffen werden soll. Für mehrsprachige Projekte werden dafür nun geeignete sprachübergreifende Refaktorisierungswerkzeuge benötigt. Wenn jedoch schon Refaktorisierungswerkzeuge für einzelne Programmiersprachen nicht ausreichend untersucht sind und bisher nur unzureichend Unterstützung anbieten, ist abzusehen, dass solche Werkzeuge für sprachübergreifende Projekte noch am Anfang stehen. Andreas Thies et al. beschreiben in einem Projekt „Cross-Language Refactoring: The CLaRe Research Project“22 die Situation folgendermaßen: „In fact, the different sets of constraint rules required for the different languages mirror commonalities and differences of the language specifications in a concise way. It is an open research question whether cross-language refactorings will require additional constraint rules reflecting the conditions of accessing program elements across specific language boundaries, or whether the sharing of constraint variables (including a mapping of their values to the different domains required by the different languages) suffices. We expect that the number of constraint rules required for cross-language refactoring will be a linear function of the numbers of constraint rules required for each individual language.“23 Für die vorliegende Ausarbeitung soll als Prämisse gelten, dass es eindeutig der falsche Weg ist, eine der beteiligten Sprachen auf bestimmte, von der/den anderen Sprachen verlangte Konstrukte, mit dem Ziel, dass eine Refaktorisierung möglich wird, einzugrenzen. Dieser Ansatz, eine Sprache wie Ruby auf ausschließlich die Möglichkeiten, die auch Java bietet, einzuengen, beispielsweise sie mit (wie auch immer gearbeiteten) Typen zu versehen, wird hier als absolut falsch begriffen. Denn nur die skizzierte Unterschiedlichkeit der Sprachen ermöglicht einen produktiven, effizienten und ausdrucksvollen Einsatz. Im Folgenden wird daher versucht, die Unterschiede explizit auszuloten, um den Blick auf sprachübergreifende Refaktorisierungen zu lenken, die ohne diese Einschränkung ablaufen können. 21 http://www.ruby-lang.org/de/documentation/ruby-from-other-languages/to-ruby-from-cand-c-, Abruf am 10.06.2011. 22 http://www.fernuni-hagen.de/ps/prjs/CLaRe/ 23 Ebd. 17 Beschränkt wird sich an dieser Stelle auf die Umgebung Eclipse und das im Weiteren vorgestellte Dynamic Languages Toolkit (DLTK)24 . Das Eclipse Framework wird als Open-Source-Umgebung für viele Projekte genutzt. Es unterstützt gängige Programmiersprachen.25 Im Folgenden wird als Grundlage eine Möglichkeit der Zusammenarbeit von unterschiedlichen Sprachen im Eclipse-Framework beschrieben. Danach wird die Sprache Ruby und die Regeln der Interaktion von Ruby und Java vorgestellt. Mit Blick auf einen constraint-basierten Ansatz wird dann das Framework und die Sprache Refacola vorgestellt und exemplarisch eine Regel für einen sprachübergreifenden Methodenaufruf entwickelt. Anhand des Methodenaufrufs wird folgend untersucht, welche Voraussetzungen für ein sprachübergreifendes Refaktorisieren mit dem constraint-basierten Ansatz gelten müssen. Die Ausarbeitung schließt mit der Beschreibung der entwickelten Software, in der die gefundenen Ideen konkret umgesetzt wurden. 24 25 http://www.eclipse.org/dltk/ Gewählt wurde die Umgebung nicht zuletzt, weil u. a. das aktuelle Projekt: Ruboto (JRuby für Android) existiert und Android von dem Eclipse Framework unterstützt wird. 18 2 Entwicklungsumgebung und mehrsprachige Projekte Für sprachübergreifende Softwareentwicklung ist es sinnvoll, die unterschiedlichen Programmiersprachen in einem einzigen Projekt vereinen zu können. Dies ist in der für diese Arbeit genutzten Entwicklungsumgebung Eclipse aktuell noch nicht möglich, jedoch in Planung. Im Folgenden wird die Unterstützung von Ruby (hier in der JavaImplementierung JRuby) in Eclipse skizziert und der Umgang mit den existierenden Schwierigkeiten der Nutzung zweier unterschiedlicher Projekte (Java und Ruby) aufgezeigt. 2.1 Eclipse Dynamic Language Toolkit Das Eclipse-Projekt bietet die Möglichkeit, über das Dynamic Language Toolkit (DLTK) komplette Umgebungen für Programmiersprachen zu erstellen. Das Core-Framework von DLTK stellt dafür sprachunabhängige Bausteine bereit. Entwicklungsumgebungen existieren bereits für verschiedene dynamische Sprachen, so auch für Ruby. Es fehlt jedoch bis heute eine Möglichkeit, mehrere Sprachen in einem einzigen Projekt gemeinsam zu integrieren. Daher existiert beispielsweise kein übergreifender Debugger. Im Jahr 2009 sagte Andrey Platov (Projektleitung) in einem Interview: „Mithilfe von DLTK ist es zwar bereits möglich, Entwicklungstools für neue Sprachen sehr schnell umzusetzen. Es fehlt allerdings noch an einer Unterstützung der Interoperabilität zwischen verschiedenen Programmiersprachen, was insbesondere sehr wichtig für Sprachen ist, die auf der Java Virtual Machine laufen. Hier gibt es ein großes Bedürfnis an Werkzeugen, die mit diesen unterschiedlichen Sprachen umgehen können.“1 2.2 Verbindung eines Java- und Ruby-Projekts in der Eclipse Die Entwicklungsumgebung Eclipse bietet für Java wie für Ruby (über das DLTK) die Möglichkeit, sprachenspezifische Projekte anzulegen. Ein einziges Projekt für beide Sprachen anzulegen, ist bis zu diesem Zeitpunkt jedoch nicht möglich. Eine provisorische Lösung für die Interaktion zwischen den Sprachen ist, zwei Projekte anzulegen und damit vorerst die existierenden Features (Syntax Highlighting, Debugger, Pretty Printer) für die jeweilige Programmiersprache zu nutzen (s. Abbildung 2.1). Ein sprachübergreifendes Verwenden von Werkzeugen ist dann jedoch nicht möglich. 1 http://it-republik.de/jaxenter/news/Eclipse-DLTK-Komplette-IDEs-fuer-dynamischeSprachen-bauen-050094.html, Abruf 9.01.2012. 19 Abbildung 2.1: Projekte im Package Explorer Bekannt gemacht werden können die Projekte dann über den Klassenpfad, der zum Ruby-Skript hinzugefügt wird. 1 2 3 require " java " $CLASSPATH<<" / j a v a p r o j / b i n " ; import " a . J a v a C l a s s " Listing 2.1: Bekanntmachen des Javaprojektes Zumindest sind nun die grundlegenden Voraussetzungen für eine sprachübergreifende Software hergestellt, auch wenn viele der üblich genutzten Features noch fehlen. 20 3 Interoperabilität der Programmiersprachen Ruby und Java Um eine Auseinandersetzung über Refaktorisierungswerkzeuge für eine Software, in der beide Programmiersprachen, Ruby und Java, genutzt werden, führen zu können, muss die Fähigkeit der Zusammenarbeit zwischen diesen Sprachen untersucht werden. Im Folgenden werden Ruby vorgestellt und elementare für die Zusammenarbeit der beiden Sprachen wichtige Konvertierungsregeln beschrieben. 3.1 Einordnung der Programmiersprache Ruby 3.1.1 Die Ideale des Ruby-Erfinders Ruby1 ist eine objektorientierte Programmiersprache, die interpretiert wird.2 Der Erfinder der Sprache, Yukihiro Matsumoto, begann im Jahr 1993 aus Unzufriedenheit über verfügbare Skriptsprachen an einer eigenen Sprache zu arbeiten und gab 1995 die erste Ruby-Version frei. Er nutzte Ideen verschiedener Sprachen (Perl, Smalltalk, Eiffel, Ada und Lisp)3 und formte daraus eine Programmiersprache, die neben der Objektorientierung mehrere weitere Programmierparadigmen unterstützt, unter anderem prozedurale und funktionale Programmierung. Ruby bietet darüber hinaus Garbage Collection, Ausnahmen (Exceptions), Reguläre Ausdrücke, Introspektion, Code-Blöcke als Parameter für Iteratoren und Methoden, die Erweiterung von Klassen zur Laufzeit, Threads und vieles mehr. Damit ist diese Sprache für unterschiedlichste Problemstellungen einsetzbar. Matsumoto folgte verschiedenen Design-Prinzipien4 . Beispielsweise können Programmierende gleiche Probleme mit unterschiedlichen Sprachmitteln lösen, was dem – aus der Programmiersprache Perl entlehnten – Prinzip: „There is more than one way to do it“ (TIMTOWTDI) entspricht und Ruby ausdrucksstark machen soll. Eine ebenso im Sprachdesign angelegte Flexibilität bietet Matsumoto durch das Konzept „Duck Typing“5 . Ein Objekt kann über Operationen, die es zur Verfügung stellt, angesprochen werden. In dem Kontext, in dem das Objekt benutzt wird, gilt der Typ dann als korrekt, wenn das Objekt die erwarteten Operationen anbietet. Eines der elementaren Prinzipien, das Matsumoto im Design berücksichtigt hat, ist das „Principle of least surprise“ (POLS)6 . Im Hinblick auf Programmiersprachen besagt es, dass (erfahrene) Entwickelnde relativ intuitiv programmieren können sollen, ohne durch beispielsweise unverständliche Methoden oder Bibliotheksnamen überrascht zu 1 Ruby steht für Rubin in Anspielung auf die Programmiersprache Perl (vgl. [SM01]). Die JRuby-Implementierung enthält seit 2007 zusätzlich einen Compiler (vgl. [NSEBD11, S. 78-96]). 3 Vgl. http://www.ruby-lang.org/en/about/ 4 Vgl. http://wiki.ruby-portal.de/Rubys_Prinzipien, Abruf am 10.03.2012. 5 Vgl. ebd. 6 http://www.canonical.org/~kragen/tao-of-programming.html, Abruf am 17.02.2012. 2 21 werden. Darüber hinaus wurde Ruby als eine Sprache entworfen, die auch für NichtProgrammierende lesbar sein soll. (vgl. [MO08, S. 29]) Vor allem das Prinzip POLS zeigt, dass der Entwurf der Sprache darauf ausgerichtet wurde, dass bei der konkreten Programmierung das Problem und nicht die Sprache selbst im Vordergrund steht. Matsumoto entwickelte Ruby mit Emacs7 und gcc8 unter Linux und stellte seine Programmiersprache (mit Quelltext) als freie Software9 zur Verfügung. Ruby ist heute für alle gängigen Betriebssysteme10 verfügbar. Der Ruby-Interpreter und die Standardbibliothek sind unter den Bedingungen der GNU General Public License (GPL)11 nutzbar. 3.1.2 Merkmale der Programmiersprache Ruby Ruby wurde als Multiparadigmen-Sprache entworfen. Das heißt, dass es dem/der Entwickelnden offen steht, neben der Objektorientierung weitere Programmierparadigmen zur Erstellung der Programme zu nutzen. Die Umsetzung der verbreitetsten Paradigmen in Ruby soll im Folgenden beschrieben werden. Objektorientierte Programmierung In Anlehnung an Smalltalk ist Ruby rein objektorientiert. Matsumoto verfolgte im RubySprachdesign die Zielsetzung: Everything is an Object (EIAO)12 . Beispielsweise sind auch die – in vielen anderen Sprachen als primitive Typen geltenden – Zahlen und Zeichenketten Objekte (s. Listing 3.1 und 3.2). p u t s 3 . 1 4 1 5 . c l a s s # Ausgabe => float Listing 3.1: Zahlen sind Objekte ’A B C ’ . s p l i t ( ’ ’ ) # Ergebnis = >[ ’A ’, ’B ’, ’C ’] Listing 3.2: Zeichenketten sind Objekte Ruby unterstützt verschiedene Ansätze der Objektorientierung: • Klassenbasierte Objektorientierung: Eine Klasse dient als Vorlage. Von ihr werden Instanzen erstellt. Klassen können von anderen abgeleitet werden. 7 http://www.gnu.org/software/emacs/ GNU Compiler Collection (http://gcc.gnu.org/). 9 http://www.ruby-lang.org/de/about/license.txt, Abruf am 15.02.2012. 10 Alle Windows-Versionen ab Windows 95 und NT 4.0, MS-Dos, Mac OS und Mac OS X, IMB OS/2, alle Linux Distributionen, FreeBSD, OpenBSD, NetBSD und Sun Solaris u. v. m. 11 http://www.gnu.org/licenses/gpl-3.0.html, Abruf am 14.02.2012. 12 http://wiki.ruby-portal.de/EIAO, Abruf am 16.02.2012. 8 22 1 2 3 4 5 6 class A def m @x=1 # Membervariable p u t s @x end end 7 8 9 c l a s s B < A # Ableitung end 10 11 12 b = B . new b .m # Ausgabe = >1 Listing 3.3: Ruby klassenbasiert • Prototypenbasierte Objektorientierung: Objekte werden durch das Klonen bereits existierender Objekte erzeugt. 1 2 3 4 o1 = Obj ect . new def o1 . m1 # definiere Methode fuer Objekt puts 1 end 5 6 7 8 9 o2 = o1 . c l o n e def o2 . m2 puts 2 end 10 11 12 o2 . m1 # Ausgabe = >1 o2 . m2 # Ausgabe = >2 Listing 3.4: Ruby prototypbasiert • Objektorientierung mit Mixins: Das Modul MA aus Listing 3.5 kann mit Objekten über extend und Klassen über include kombiniert werden. 1 2 3 4 5 module MA def m p u t s ’m ’ end end 6 7 8 9 10 # mit Objekten kombiniert o = O bjec t . new o . e x t e nd MA o .m # Aufruf m aus MA 11 23 12 13 14 15 # mit Klassen kombiniert class A i n c l u d e MA end 16 17 (A. new ) .m # Aufruf m aus MA Listing 3.5: Mixins mit Klassen und Objekten Prozedurale Programmierung Im Gegensatz zu Sprachen wie Java und C# ist es in Ruby nicht notwendig, Klassen zu verwenden. Jedes Ruby-Programm liegt implizit in einem globalen main-Objekt. Damit kann ein Programm scheinbar aus globalen Prozeduren aufgebaut werden, die jedoch Methoden des globalen main-Objekts sind. 1 2 3 def globales_m puts ’ g l o b a l ’ end Listing 3.6: Ruby prozedural Funktionale Programmierung Ruby ermöglicht funktionale Programmierung. Es enthält anonyme Funktionen in Form von Blöcken und Closures. Methoden können, wie in Listing 3.7, mit Blöcken aufgerufen werden. 1 2 3 def f ( a , x ) a . map { | a | a+x} # Closure , schliesst x mit ein end 4 5 6 array = [ 2 , 3 , 4 ] p u t s f ( a r r a y , 2 5 ) # Ausgabe : [27 ,28 ,29] Listing 3.7: Ruby funktional Weitere Paradigmen Ruby unterstützt Metaprogrammierung, aspektorientierte und kontextorientierte13 Programmierung. 13 http://contextr.rubyforge.org/ 24 3.1.3 Dynamische Aspekte der Programmiersprache Ruby In Ruby werden Klassen oder Module zur Laufzeit aufgebaut. Daher können zur Laufzeit Manipulationen vorgenommen werden. Im Unterschied zu einer statisch typgeprüften Sprache können beispielsweise neue Definitionen bekannt gemacht oder die Sichtbarkeiten (public, private, protected) von Methoden verändert werden. Solche Aspekte erschweren oder verhindern das Ableiten semantischer Informationen (s. Kapitel 5). An dieser Stelle sollen exemplarisch Möglichkeiten der Veränderung des Ruby-Quelltextes vorgestellt werden. Die Methoden instance_eval und class_eval Durch instance_eval können einer Instanz Methoden hinzugefügt werden. Wie in Listing 3.8 gezeigt wird, gehört die Methode newMethodInstance zum Objekt r. Die Klasse RubyClass selbst kennt diese Methode jedoch nicht. Anders bei der Nutzung von class_eval: Hier wird die Klasse RubyClass um die Methode newMethod erweitert. 1 2 3 4 5 c l a s s RubyClass def m puts 1 end end 6 7 r = RubyClass . new 8 9 10 11 12 13 r . i n s t a n c e _ e v a l do # fuege r eine instanzspezifische Methode hinzu def newMethodInstance p u t s ’ I n s t a n z s p e z i f i s c h e Methode ’ end end 14 15 16 17 18 19 RubyClass . c l a s s _ e v a l do # fuege RubyClass eine Methode hinzu def newMethod p u t s ’ Methode ’ end end 20 21 22 r . newMethod # Ergebnis => Klassenmethode r . newMethodInstance # Ergebnis => Instanznmethode 23 24 25 ( RubyClass . new ) . newMethodForClass # Ausgabe => Methode ( RubyClass . new ) . newMethodInstance # Ausgabe => undefined method Listing 3.8: instance_eval und class_eval 25 Die Sichtbarkeiten public, protected und private Mit Ausnahme der privaten Methode initialize sind alle Methoden einer Klasse per Default öffentlich. Die Sichtbarkeit einer Methode kann durch eine Angabe von public, protected oder private festgelegt werden.14 Sichtbarkeiten sind in Ruby folgendermaßen definiert: • public heißt öffentlich, die Methode kann von überall aufgerufen werden. • protected heißt, die Methode kann nur von Instanzen der eigenen oder abgeleiteter Klassen aufgerufen werden. • private heißt, dass Methoden nicht mit einem Empfänger aufgerufen werden dürfen, also der Empfänger implizit immer self ist. Daher kann eine private Methode nur innerhalb der Klasse, aber auch innerhalb abgeleiteter Klassen aufgerufen werden.15 1 2 3 4 5 6 7 8 9 10 c l a s s RubyClass def m 1 end def ==(o t h e r ) m==o t h e r .m end # private :m # nicht erlaubt p r o t e c t e d :m # erlaubt end 11 12 13 14 r 1 = RubyClass . new r 2 = RubyClass . new p u t s r 1==r 2 # Ausgabe => true Listing 3.9: Unterschied protected und private In Ruby können Sichtbarkeiten zur Laufzeit geändert werden (s. Listing 3.10). 1 2 3 4 5 6 c l a s s RubyClass private def m puts 1 end end 7 8 x = g e t s . chomp 9 10 i f (1==x . to_i ) # Eingabe ... 11 # Erweitere die Klasse , m wird public 12 14 15 Wegen fehlender Pakete existiert die Sichtbarkeit „package“ nicht. Der einfache Zugriff ist jedoch trotzdem über (RubyClass.new).send(:m) möglich. 26 13 14 15 16 RubyClass . c l a s s _ e v a l do p u b l i c :m end end 17 18 ( RubyClass . new ) .m Listing 3.10: Sichtbarkeiten ändern 3.2 Methodenaufrufe zwischen Ruby und Java Einleitend wurde JRuby, eine Ruby-Implementierung für die Java Virtual Machine vorgestellt. Für diese Arbeit wird JRuby als Brücke zwischen Ruby und Java herangezogen.16 Wie Java und Ruby programmiersprachenübergreifend zusammenarbeiten, wird im Folgenden exemplarisch anhand von Methodenaufrufen vorgestellt. 3.2.1 Methodenaufruf von Java nach Ruby Im Listing 3.11 wird ein Methodenaufruf von Java nach Ruby gezeigt. Java bietet über die Java Scripting API17 den Zugriff auf JRuby. 1 import j a v a x . s c r i p t . ∗ ; 2 3 public c l a s s J a v a C l a s s { 4 public s t a t i c void main ( S t r i n g [ ] a r g s ) { 5 6 S c r i p t E n gi n e M a n a g e r f a c t o r y = new S c r i p t E n gi n e M a n a g e r ( ) ; S c r i p t E n g i n e e n g i n e = f a c t o r y . getEngineByName ( " j r u b y " ) ; 7 8 9 try { e n g i n e . e v a l ( " c l a s s RubyCl ; d e f m; p u t s 1 ; end ; end ; ( RubyCl . new ) .m" ) ; // Ausgabe = >1 } catch ( S c r i p t E x c e p t i o n e x c e p t i o n ) { exception . printStackTrace () ; } 10 11 12 13 14 15 } 16 17 } Listing 3.11: Aufruf einer Ruby-Methode aus Java 16 Weitere Implementierungen für die Java Virtual Machine, beispielsweise die Ruby Java Bridge (RJB), http://rjb.rubyforge.org/, werden in dieser Abeit nicht berücksichtigt. 17 http://docs.oracle.com/javase/6/docs/technotes/guides/scripting/programmer_guide/ index.html, Abruf am 16.02.2012. 27 3.2.2 Methodenaufruf von Ruby nach Java Von Ruby aus können Methoden der Java-Standardbibliothek oder eigene in Java implementierte Methoden genutzt werden. Exemplarisch für einen Java-Aufruf wird im Listing 3.12 println gerufen.18 1 2 3 4 5 6 7 require ’ java ’ i n c l u d e _ c l a s s ’ j a v a . l a n g . System ’ c l a s s JavaTest def m j a v a . l a n g . System : : out . p r i n t l n ( 1 ) end end 8 9 ( JavaTest . new) .m # Ausgabe=>1 Listing 3.12: Aufruf einer Java-Methode aus Ruby 3.3 Konvertierungsregeln Für eine automatisierte Refaktorisierung sprachübergreifender Software ist eine Auseinandersetzung mit Konvertierungsregeln notwendig. Es muss beispielsweise die Frage beantwortet werden, welche Auswirkung eine Sichtbarkeitsänderung oder Verschiebung einer Methode in Java hat, wenn die jeweilige Methode in Ruby genutzt wird. Die Zusammenarbeit von Java und Ruby ist weitgehend geregelt: Es existieren Vorschriften für die Abbildung spezifischer Sprachelemente auf die jeweils andere Sprache. Jedoch sind diese Regeln nicht allein abhängig von der Implementierung des JRubyInterpreters19 ; zusätzlich können Sicherheitseinstellungen der Java Virtual Machine zu unterschiedlichen Ergebnissen führen. Im Folgenden sollen elementare Regeln herausgegriffen und beschrieben werden, beispielsweise die Abbildung von Datentypen auf die jeweils andere Sprache oder der Umgang mit den oben genannten sprachspezifisch unterschiedlichen Sichtbarkeitsregeln.20 Weitere Konvertierungsregeln sind in [NSEBD11] und http://kenai.com/projects/ jruby/pages/CallingJavaFromJRuby zu finden. 3.3.1 Konvertierungsregeln für Sichtbarkeiten JRuby erlaubt den Zugriff von Ruby auf Java-Methoden, unabhängig davon, welche Sichtbarkeit diese haben, sofern der Zugriff auf private nicht durch eine Sicherheitseinstellung explizit verboten wird. Damit können mit private und protected gekennzeichnete Methoden ebenso aufgerufen werden wie Methoden, die öffentlich sind. Exemplarisch wird in Listing 3.14 eine Ruby-Klasse von der Java-Klasse des Listings 3.13 abgeleitet. Die private Java-Methode privateJavaMethod wird in der abgeleiteten 18 Weitere Beispiele für Methodenaufrufe in Abschnitt 5. Für die folgenden Beispiele wurde die Version JRuby 1.6.5 herangezogen. 20 Sichtbarkeiten besitzen in Java eine andere Bedeutung als in Ruby (s. Abschnitt 3.1.3). 19 28 Ruby-Klasse gesehen und kann über das Objekt r gerufen werden. Ein folgender Test mit Hilfe der Ruby-Methode private_method_defined? zeigt, dass die private JavaMethode vorhanden ist, jedoch die Sichtbarkeit, die die Methode in Java besaß, nicht bekannt ist. Analog verhält es sich mit Methoden, die in Java mit protected gekennzeichnet sind. 1 class JavaClass { 2 private void privateJavaMethod ( ) { System . out . p r i n t l n ( " privateJavaMethod " ) ; } 3 4 5 6 } Listing 3.13: Private Java-Methode 1 2 c l a s s RubyClass<J a v a C l a s s end 3 4 5 6 r = RubyClass . new r . privateJavaMethod # Ausgabe : " privateJavaMethod " p u t s RubyClass . private_method_defined ? " privateJavaMethod " # Ausgabe=>f a l s e Listing 3.14: Sichtbarkeitstest einer Methode Felder werden nicht abgebildet: „On its own, JRuby doesn’t seek out a class’s fields and try to map them to Ruby attributes.“ [NSEBD11, S. 45] Jedoch kann ein Feld im Nachhinein zugreifbar gemacht werden (s. Listing 3.15 und 3.16). 1 2 3 class JavaClass { private i n t p r i v a t e J a v a F i e l d = 1 ; } Listing 3.15: Privates Java-Feld 1 2 3 c l a s s RubyClass<J a v a C l a s s f i e l d _ a c c e s s o r : p r i v a t e J a v a F i e l d=> : myField end 4 5 6 r = RubyClass . new p u t s r . myField # Ausgabe=>1 Listing 3.16: Zugriff auf das private Java-Feld 3.3.2 Regeln für die Abbildung von Datentypen Die Abbildungen von Datentypen in die jeweils andere Sprache sind wie folgt festgelegt21 : 21 Vgl. https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby, Abruf am 20.03.2012. 29 Ruby nach Java Ruby Typ "foo" 1 1.0 true/false 1 << 128 Java nach Java Typ public String public byte public short public int public long public float public double Java Typ java.lang.String java.lang.Long java.lang.Double java.lang.Boolean java.math.BigInteger Ruby Ruby Typ String Fixnum Fixnum Fixnum Fixnum Float Float Abbildung 3.1: Abbildung von Datentypen über JRuby Problematisch werden die tabellarisch dargestellten Abbildungsregeln bei der Nutzung von überladenen Methoden in Java. Zwar findet jeder Datentyp eine Entsprechung, jedoch kann es durch eine Erweiterung durch Überladen von Methoden in Java ggf. zu nicht bedachten Auflösungen kommen (s. Listing 3.18). public void m( i n t i ) { System . out . p r i n t l n ( " i n t " ) ; } public void m( long l ) { System . out . p r i n t l n ( " l o n g " ) ; 1 2 3 4 5 6 } Listing 3.17: Überladene Methoden 1 ( J a v a C l a s s . new) .m 3 # Ausgabe=>long Listing 3.18: Problematik bei überladenen Methoden Ausgangspunkt ist an dieser Stelle, dass eine ganze Zahl von JRuby grundsätzlich auf Long abgebildet wird. Wird das Verhalten gewünscht, dass eine Zahl als Integer eingeordnet wird, muss, wie in Listing 3.19 gezeigt, mit java_send der Typ angegeben werden. 1 ( J a v a C l a s s . new) . java_send :m, [ Java : : i n t ] , 3 Listing 3.19: Zahl wird als Integer erkannt Der/die Programmierende ist an dieser Stelle gefordert, die richtige Syntax für sein/ihr Vorhaben zu wählen. 3.4 Bedingungen für eine Refaktorisierung Für die im Folgenden als Grundlage einer automatisierten Refaktorisierung vorgestellte Constraint-Sprache (s. Kapitel 4) müssen für die beteiligten Programmiersprachen und 30 für die Übergriffe auf die jeweils andere Sprache Bedingungen dafür herausgearbeitet werden, wann eine automatisierte sprachübergreifende Refaktorisierung erlaubt werden kann. Einleitend wurde jedoch das Problem aufgezeigt, dass semantische Informationen nur für statisch typgeprüfte Sprachen ohne Weiteres zur Verfügung stehen. Daher wird im Folgenden der Schwerpunkt darauf gelegt, solche Informationen auch für dynamische Sprachen zu finden. Im beiliegenden Prototyp wird eine Refaktorisierung erlaubt, wenn versucht wird, Java-Quellcode zu refaktorisieren, ohne dass Zugriffe aus Ruby enthalten sind. Werden Zugriffe aus Ruby gefunden, wird als Zwischenlösung die Refaktorisierung verboten (s. Abschnitt 4.2). Das Refaktorisieren von Ruby-Quellcode wurde nicht in die Diskussion mit einbezogen. Trotz dieser Eingrenzung des Themas sollen im folgenden Exkurs einige markante Bedingungen für Sprachübergriffe von Java und Ruby als Ausblick skizziert werden. Denn das endgültige Ziel ist ein Rückschreiben des korrekt veränderten Quellcodes. Die Diskussion beschränkt sich auf Java-Refaktorisierungen, die ggf. Auswirkungen auf den Ruby-Quellcode haben. Genutzt wird ausschließlich JRuby.22 3.4.1 Sichtbarkeiten sind nicht relevant Im Abschnitt 3.3.1 wurde beschrieben, dass JRuby den Zugriff auf Java-Methoden über Objekte erlaubt, unabhängig davon, welche Sichtbarkeit die Methoden haben. Konkret werden alle public und protected Methoden über Java-Reflexion gesucht. (vgl. [NSEBD11, S. 290]) Je nach Java-Sicherheitseinstellungen werden auch die privaten Methoden in die Auswahl mit einbezogen. Danach wird der passende Parametertyp nach der obigen Tabelle Abbildung 3.1 ausgewählt. Die Sichtbarkeiten selbst sind an dieser Stelle nicht mehr relevant. Würde eine Refaktorisierung in Java erfolgen, die eine öffentliche Methode auf protected setzt, würde aus Ruby heraus diese Methode trotzdem weiter aufgerufen. Ob beim Auffinden eines sprachübergreifenden Aufrufs dieser Methode die Sichtbarkeitsumbenennung verboten werden muss, also die Methode weiter public bleiben sollte, um hier eine Klarheit zu erhalten, muss diskutiert werden. Denn es ist ja ausdrücklich erlaubt, protected Methoden über Objekte in Ruby aufzurufen. 3.4.2 Getter und Setter Methoden, die die Zugriffe auf Membervariablen haben, besitzen in Ruby einen besonderen Status. Da ein direkter Zugriff nicht möglich ist, werden Methoden gleichen Namens definiert, die in Ausdrücken und auf der linken Seite von Zuweisungen genutzt werden können (s. Listing 3.20). 22 Andere Portierungen von Ruby auf die Java Virtual Machine mögen andere Regeln haben, wurden hier jedoch nicht berücksichtigt. 31 1 2 3 4 5 6 7 8 c l a s s RubyClass def m @m end d e f m=(x ) @m=x end end 9 10 11 12 r=RubyClass . new r .m=4 p u t s r .m # Ausgabe=>4 Listing 3.20: Zugriff auf Membervariablen in Ruby Die in Java üblichen „Getter“ und „Setter“ (setM, getM) werden von JRuby auf die gleiche Weise abgebildet: „As an added bonus, Java-style getters and setters are callable as Ruby-style attribute accessors.“ [NSEBD11, S. 44] Die folgenden Aufrufe in Listing 3.21 sind für die Java-Methode setM(long l) äquivalent: setM ( 1 ) m=1 Listing 3.21: Äquivalente Zugriffe auf eine Java-Membervariable von Ruby aus Für den Wunsch, die Methode setM in einem Refaktorisierungsprozess umzubenennen, heißt das, dass im Ruby-Quellcode nicht allein der eigentliche Name der Methode, sondern ebenso das Wort hinter „set“ (in Kleinschrift) einschließlich Zuweisungoperator beachtet werden muss. 3.4.3 to_s Folgende Sonderregelung muss beachtet werden, wenn in Java eine Methode nach to_s umbenannt werden soll: bei dem Versuch, die String-Repräsentation eines Java-Objektes in Ruby auszugeben, sucht JRuby zuerst die Methode to_s. Ist to_s nicht vorhanden und wird eine Methode toString gefunden, wird diese genutzt. Findet JRuby keine der beiden Methoden, wird eine Java-String-Repräsentation des Objektes ausgegeben. 1 2 3 4 5 6 7 8 9 class JavaClass { public S t r i n g to_s ( ) { return " to_s " ; } // Ueberschreibe die Methode toString public S t r i n g t o S t r i n g ( ) { return " t o S t r i n g " ; } } Listing 3.22: to_s und toString 32 1 p u t s J a v a C l a s s . new # Ausgabe=>to_s Listing 3.23: Ausgabe einer String-Repräsentation eines Java-Objektes in Ruby Unabhängig vom Rückgabetyp wird to_s als erstes gerufen, wenn das Java-Objekt in Ruby ausgegeben wird. Das bedeutet für eine Refaktorisierung, dass durch ein Umbenennen einer Methode nach to_s das normale Verhalten der Erzeugung einer String-Repräsentation überdeckt wird. Wurde in Ruby die vorherige Methode genutzt, verhält sich das Programm nach Umbenennen anders. 33 4 Refacola – Constraint-Sprache und Framework Als Basis für Refaktorisierungen in sprachübergreifender Software wird im Folgenden ein existierendes Projekt: Refacola1 genutzt. Der Begriff „Refacola“ wird in zwei Bedeutungen verwendet: Zum einen als Bezeichnung einer deklarativen Sprache, die am Lehrgebiet Programmiersysteme der FernUniversität Hagen zur Spezifizierung constraintbasierter Refaktorisierungen entwickelt wurde (vgl. [SKP11]). Zum anderen bezeichnet „Refacola“ ein dort erstelltes programmiersprachenunabhängiges Framework für Refaktorisierungsprozesse. Als Technologien werden die Werkzeuge Xtext2 /Xtend3 , ein Open-Source-Framework für die Entwicklung von Programmiersprachen und domänenspezifischer Sprachen, sowie Xpand4 aus dem Eclipse Modeling Project5 genutzt. Xpand, eine statisch typisierte Templatesprache, generiert auf Basis sprachenspezifischer Templates aus vorher definierten Constraintregeln Java-Quellcode. Zusammen mit dem Refacola Framework und einem Constraint Solver können Refaktorisierungen angelegt und durchgeführt werden (vgl. [SKP11, S. 13]). Im Hinblick auf ein Entwerfen einer sprachübergreifenden Software werden in diesem Kapitel die wichtigsten Sprachmittel von Refacola skizziert. Exemplarisch wird eine Constraint-Regel formuliert, durch die das Umbenennen einer Java-Methode verboten wird, wenn die Methode von Ruby aus aufgerufen wird. Als Grundlage für eine Refaktorisierung sprachübergreifender Projekte mit dynamisch und statisch typgeprüften Sprachen können perspektivisch weitere Constraint-Regeln definiert werden. 4.1 Die Sprache Refacola Die Sprache Refacola besitzt drei Arten von Modulen, die für die zu refaktorisierende Sprache definiert werden müssen: • Language Definition (languages) • Rule Definitions (rulesets) • Refactoring Definitions (refactorings) 1 http://www.fernuni-hagen.de/ps/ http://www.eclipse.org/Xtext/ 3 http://www.eclipse.org/Xtext/#xtend2 4 http://www.eclipse.org/modeling/m2t/?project=xpand 5 http://www.eclipse.org/modeling/ 2 34 4.1.1 Sprachdefinition Die Sprachdefinition (Language Definition) besteht aus Programmelementen, die je nach Programmiersprache unterschiedlicher Art sein können. Für die meisten objektorientierten Sprachen sind es Klassen, Felder, Methoden, Referenzen und lokale Programmelemente wie lokale Variablen und Parameter (vgl. [SKP11, S. 4]). Daneben besitzt jedes Programmelement Eigenschaften (properties), beispielsweise Bezeichner, Sichtbarkeit oder die umgebene Klasse. Die Eigenschaften werden auf Constraintvariablen abgebildet und mit Wertebereichen (domains) verknüpft (s. Listing 4.1). Durch Refaktorisierungsvorgänge werden die Eigenschaften verändert, nicht die Programmelemente selbst (vgl. [SKP11, S. 4ff.]). Als Beispiel für Wertebereiche von Eigenschaften führen Steimann et al. die Menge aller gültigen Bezeichner für die Eigenschaft identifier oder die Menge {private, package, protected, public} für die Eigenschaft Sichtbarkeit an. 1 2 3 4 /* * Refacola Language Definition */ language Java 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 kinds abstract F i e l d <: TypedMember , HideableMember , NamedEntity /* ... */ I n s t a n c e F i e l d <: InstanceMember , F i e l d S t a t i c F i e l d <: StaticMember , F i e l d /* ... */ properties identifier " \\ i o t a " : I d e n t i f i e r a c c e s s i b i l i t y " \\ a l p h a " : A c c e s s M o d i f i e r owner " \\ c h i " : Type " " t l o w n e r " \\ lambda " : LocationType " " h os t Package " \\ p i " : L o c a t i o n P a c k a g e " " d e c l a r e d T y p e " \\ tau " : Type inferredType " \\ tau " : Type domains A c c e s s M o d i f i e r = { p r i v a t e , package , p r o t e c t e d , p u b l i c } LocationType = [ TopLevelType ] queries binds ( r e f e r e n c e : Reference , e n t i t y : Entity ) " r e f e r e n c e binds to e n t i t y " /* ... */ Listing 4.1: Auszug aus der Refacola Language Definition: java.language.refacola Neben den dargestellten Wertebereichen existieren sogenannte programmabhängige Wertebereiche (Program-dependent domains), die dann auftreten, wenn sich Wertebereiche erst aus dem Programm selbst ergeben. Beispielsweise kann ein Programmelement der Art Klasse ebenso Wert einer Eigenschaft eines Programmelements Methode sein, die in dieser konkreten Klasse definiert wurde. Der Bereich oder die Stelle (location) ist eine Eigenschaft, die nun als Wert die konkrete Klasse erhält. Aufgelöst wird der 35 Zusammenhang durch eine Abbildung von der Art eines Programmelements auf den entsprechenden Wertebereich. Wenn für Wertebereiche eine explizite Ordnung (Ordered Program-Dependent Domains) benötigt wird, also wenn in programmabhängigen Wertebereichen beispielsweise eine Vererbungshierarchie existiert, wird dies mit berücksichtigt (vgl. [SKP11, S. 5]). 4.1.2 Definition der Constraintregeln In Refacola werden Constraints über Regeln definiert. Nur wenn die Bedingung erfüllt ist, wird der unter then definierte Constraint dem Constraintproblem hinzugefügt. Jede Constraintregel hat im Allgemeinen folgendes Aussehen: program queries constraints Für das Listing 4.2 würde die Regel in folgender Formel (4.2) abstrahiert: binds(r, d) r.ι = d.ι r.τ = d.τ 1 (4.1) (4.2) Java . language . r e f a c o l a 2 3 4 r u l e s e t RubyJavaRules language Java 5 6 7 8 9 10 11 12 13 14 15 16 17 rules exampleRule for a l l r : Java . NamedReference d : Java . NamedEntity do if Java . b i n d s ( r , d ) then r . identifier = d. identifier , r . inferredType = d . declaredType end Listing 4.2: Beispiel-Regel Die Query binds(r, d) ergibt ’true’, wenn die Referenz r an das Feld d gebunden ist. Die Constraints r.ι = d.ι r.τ = d.τ erzwingen die Gleichheit der beiden Bezeichner (Identifier ι) und die Gleichheit der Typen (Declared/Inferred Type τ ). Die Formel (4.2) beschreibt, dass, wenn die Referenz r an die Entity d gebunden ist, der Identifier und der Typ gleich sein müssen. 36 4.1.3 Definition von Refaktorisierungen Neben einer vollständigen Sprachdefinition für eine Programmiersprache und ConstraintRegeln, durch die die notwendigen Constraints erzeugt werden können, muss definiert werden, welche Eigenschaften geändert werden müssen. Unterschieden wird zwischen Eigenschaften, deren Änderungen erzwungen werden (forced), und Eigenschaften, deren Änderungen erlaubt werden (allowed). Alle weiteren Eigenschaften werden nicht geändert. In Listing 4.6 werden über import die Sprachspezifikation und Constraintregeln eingebunden. Dem Refactoring wird ein Name gegeben (refactoring), die Programmiersprache (language) und die Constraintregeln (uses) festgelegt. 4.2 Definition eines Verbots für das Refaktorisieren sprachübergreifender Methodenaufrufe Ziel ist, das Umbenennen von Java-Methoden zu verbieten, wenn sie von Ruby aus aufgerufen werden, d. h. das Constraint-Problem darf dann keine Lösung haben. Bei einem Aufruf einer Java-Methode aus Ruby wird die Gleichheit des Originalnamens mit dem Methodennamen verlangt (s. Listing 4.3). Nun muss jede Lösung des Constraint-Problems diese Bedingung erfüllen. Lautet das Ziel der Refaktorisierung, die Methode nach einem neuen Namen umzubenennen, hat das Constraint-Problem keine Lösung und das Verbot ist durchgesetzt. Die Regel besagt, dass für jede Methode mit Hilfe einer einstelligen Query isCalledFromRuby geprüft wird, ob sie aus Ruby aufgerufen wird. Wenn ja, wird das obige Constraint aktiv. methodRenameNotAllowedCollision for a l l m: Java . Method do if Java . isCalledFromRuby (m) then m. i d e n t i f i e r =m. i d e n t i f i e r . o r i g i n a l # Pseudocode end 1 2 3 4 5 6 7 8 9 Listing 4.3: Methodenumbenennung verbieten: einstellige Query Da der Originalidentifier nicht trivial zu erhalten war, wird ein neues Programmelement RubyReference eingeführt, das für den Aufruf aus Ruby steht und als Eigenschaft nur den Identifier hat. Die Query isCalledFromRuby wird nun durch eine zweistellige Query bindsFromRuby ersetzt. Diese Query bekommt als Übergabe die eingeführte RubyReference und die Methode m und liefert true, wenn die RubyReference ein Aufruf dieser Methode darstellt. Nun wird nicht mehr Gleichheit mit dem Originalnamen, sondern mit der RubyReference verlangt.6 6 Die Sonderregeln aus den Abschnitten 3.4.2 und 3.4.3 wurden noch nicht berücksichtigt. 37 Folgende Regel wurde definiert: bindsF romRuby(r, m) r.ι = m.ι (4.3) In das Listing 4.1 wurde die Art ReferenceFromRuby und die Query bindsFromRuby aufgenommen, s. Listing 4.4. 1 2 3 4 /* * Refacola Language Definition */ language Java 5 6 7 8 9 10 11 kinds /* ... */ ReferenceFromRuby <: NamedReference { } //Ruby queries /* ... */ bindsFromRuby ( r e f e r e n c e : ReferenceFromRuby , method : Method ) //Ruby Listing 4.4: Auszug aus der Refacola Language Definition: java.language.refacola Als Regel wurde die methodRenameNotAllowedCollision definiert, s. Listing 4.5. 1 2 3 4 5 6 7 8 9 10 methodRenameNotAllowedCollision for a l l r : Java . ReferenceFromRuby m: Java . Method do if Java . bindsFromRuby ( r , m) then r . i d e n t i f i e r = m. i d e n t i f i e r end Listing 4.5: Methodenumbenennung verbieten: java.ruleset.refacola Als Definition der Refaktorisierung wurde der Identifier der Java.Method auf forced gesetzt, s. Listing 4.6. 1 2 import " Java . l a n g u a g e . r e f a c o l a " import " RubyJavaRules . r u l e s e t . r e f a c o l a " 3 4 5 6 refactoring RubyJava language Java uses RubyJavaRules 7 8 9 forced i d e n t i f i e r o f Java . Method a s X 10 38 11 12 allowed /* .. */ Listing 4.6: Definition von Refaktorisierungen: java.refactoring.refacola Wird dieser Constraint erzeugt, wird ein identifizierter Java-Methodenaufruf aus Ruby verboten. 39 5 Codeanalyse als Voraussetzung für Refaktorisierung Für eine automatisierte Refaktorisierung wird ein Modell benötigt, das den Programmcode mit den für die Refaktorisierung notwendigen Informationen repräsentiert. Meist wird dies in Form eines abstrakten Syntaxbaums (Abstract Syntax Tree, AST) realisiert. Ein AST bildet den Programmcode in einer Baumstruktur ab (vgl. [ASU99, S. 60]). Für eine Refaktorisierung müssen Programmelemente, beispielsweise Klassen, Methoden oder Felder, als Knoten im AST vorliegen. Darüber hinaus müssen spezielle Eigenschaften der jeweiligen Elemente bekannt sein. Beispiele hierfür sind der Typ des jeweiligen Programmelements oder der Bereich, in dem das Element definiert wurde. Erst wenn die spezifizierten Eigenschaften von Programmelementen vollständig vorliegen, kann eine Entscheidung getroffen werden, in welcher Form refaktorisiert wird bzw. ob die gewünschte Refaktorisierung überhaupt erlaubt ist. Im Framework Refacola werden die gefundenen Informationen in einer Faktendatenbank gespeichert, die als Basis für Abfragen genutzt wird. In Abhängigkeit von den Abfragen (Queries) werden Constraints erzeugt, die später dem Constraintsolver übergeben werden (vgl. [SKP11, S. 6]). In der statisch typgeprüften Sprache Java sind solche Informationen statisch verfügbar, d. h. der jeweilige Quellcode kann eingelesen, ein AST kann aufgebaut und Knoten können mit den in einer semantischen Analyse gefundenen Eigenschaften attributiert werden. Es stellt sich jedoch die Frage, wie diese für eine Refaktorisierung notwendigen Informationen gefunden werden können, wenn Java mit der dynamischen Sprache Ruby in einem Projekt verwoben ist. Denn nun müssen über die Quelltexte beider Sprachen Informationen vorliegen, aus denen eine Faktendatenbank als Grundlage für die Constraintgenerierung erstellt wird.1 Zur Verdeutlichung sollen im Folgenden anhand des Refaktorisierungswunsches, eine Java-Methode umzubenennen (s. Listing 5.1 und Abbildung 5.1), die existierenden Probleme umrissen werden. 1 2 3 4 5 public c l a s s J a v a C l a s s { public void m( ) { System . out . p r i n t l n ( " Java m" ) ; } } Listing 5.1: Java-Klasse mit Methode 1 Darüber hinaus müssen von beiden Sprachen die Sprachdefinitionen und die Constraintregeln vorliegen (vgl. [SKP11]). 40 Abbildung 5.1: Umbenennen der Java-Methode im Framework Refacola In Listing 5.2 wird gezeigt, dass über einen AST nicht eindeutig bestimmt werden kann, ob die Methode m über ein Java- oder ein Rubyobjekt gerufen wird, da über eine Eingabe entschieden wird, ob der Wert von o ein Java- oder Ruby-Objekt ist. Diese Möglichkeit in Ruby basiert auf dem Prinzip Duck Typing (s. Abschnitt 3.1.1). In Java wäre der Typ der Variablen o durch das statische Typsystem bekannt. Eine Übersetzung des Quellcodes wäre dann nur mit einem Interface möglich, das von den Klassen RubyClass und JavaClass implementiert wird. 1 o = RubyClass . new 2 3 4 5 i f ( 1 == e i n g a b e ) o = J a v a C l a s s . new end 6 7 o .m # Uneindeutig Listing 5.2: Bindung an unterschiedliche Objekte mit Ruby und Java Dieses Beispiel zeigt, dass hier zwar ein AST für Ruby und Java aufgebaut werden kann, die semantischen Inhalte der Programmelemente jedoch statisch nicht eindeutig identifiziert werden können. An dieser Stelle soll das eigentliche Problem schon genannt werden: Die in einem AST mit semantischer Analyse über einen Ruby-Quelltext gesammelten Informationen sind prinzipiell unzureichend. Zwar können für einen Ruby-Quellcode die Reihenfolge der Anweisungen identifiziert und Programmelemente gefunden werden, wenn jedoch in Ruby zur Laufzeit neue Klassen bekannt gemacht oder neue Sichtbarkeiten (public, private, protected) bestimmt werden oder darüber hinaus die Programme selbst insgesamt abgeändert werden können (s. Abschnitt 3.1.3), dann wird eine semantische Analyse extrem schwierig oder versagt insgesamt (s. Abschnitt 5.1.3). Jedoch soll nochmals darauf hingewiesen werden, dass solche Sprachmöglichkeiten Ausdruck der Dynamik von Ruby sind und die eingangs beschriebene gewünschte Flexibilität dieser Sprache ausmacht; ein Grund, weshalb Ruby in Verbindung mit Java für Software-Projekte herangezogen wird. Im Folgenden wird als erstes eine statische Codeanalyse diskutiert und die Probleme des Ansatzes aufgezeigt. Danach wird ein zweiter Ansatz vorgestellt, der von der Programmiersprache Smalltalk und dem damals für diese Sprache entwickelten Refactoring 41 Browser inspiriert ist: das Sammeln der notwendigen Informationen wird losgelöst von der statischen Analyse des Quellcodes und in die Laufzeit verschoben. Abschließend werden die Ansätze miteinander verbunden, mit dem Ziel, dass eine Refaktorisierung weder Gefahr läuft, die Semantik des Ursprungsprogramms durch eine falsche Umstrukturierung zu verändern, noch dass eine Refaktorisierung sofort verboten wird, obwohl sie im Grunde möglich gewesen wäre. Die Ansätze sind prototypisch als Software umgesetzt worden, die in einem Folgekapitel vorgestellt wird. 5.1 Statische Codeanalyse Eingangs wurde der Blick darauf gelenkt, dass eine statische Codeanalyse von RubyQuelltexten zumindest schwierig, wenn nicht sogar unmöglich ist. Aufgrund dieser Problematik stellt sich die grundsätzliche Frage, ob es überhaupt sinnvoll ist, eine statische Codeanalyse in die Diskussion einer automatisierten Refaktorisierung sprachübergreifender Software mit einzubeziehen. Hier wird eine mögliche sinnvolle Verbindung zwischen den im Folgenden skizzierten, unterschiedlichen Ansätzen einschließlich der statischen Analyse in Aussicht gestellt (s. Abschnitt 5.3). Daher wird zunächst ein Weg beschrieben, wie durch eine statische Codeanalyse eines Ruby-Skripts die für einen sprachübergreifenden Refaktorisierungsvorgang notwendigen Informationen zumindest begrenzt gesammelt werden können. Natürlich kann aus einem Ruby-Quelltext ein AST aufgebaut und dann versucht werden, per semantischer Analyse entsprechend der Sprachdefinition die jeweiligen Eigenschaften zu finden. Dafür kann ausgenutzt werden, dass statische Informationen in einer Entwicklungsumgebung meist ohnehin zumindest in Ansätzen vorhanden sind, z. B. um Syntaxhighlighting, Auto-Vervollständigung und Quellcode-Navigation anbieten zu können.2 Im Folgenden sollen Möglichkeiten, aber auch auftretende Probleme einer statischen Codeanalyse anhand von Beispielen gezeigt werden. Der einleitend beschriebene Refaktorisierungswunsch, eine Java-Methode umzubenennen, dient als Faden, nach dem die Beispiele ausgesucht wurden. Das Eclipse-Plugin AST4CongenJRubyView3 wurde entwickelt, um diese statische Codeanalyse prototypisch auszuloten.4 5.1.1 Exkurs – Mehrdeutigkeit in Ruby-Quelltexten Im folgenden Exkurs werden Ruby-Quelltexte noch ohne Zugriff auf Java behandelt. Es soll gezeigt werden, dass statisch nicht entschieden werden kann, welche Typen die 2 In der Entwicklungsumgebung Eclipse werden beispielsweise über das Paket DLTK Möglichkeiten dazu bereit gestellt. 3 Das Plugin AST4CongenJRubyView wurde im Zuge der Untersuchung entwickelt, um die Möglichkeiten eines statischen AST-Aufbaus zu testen. Eine Beschreibung der Möglichkeiten und Grenzen des Plugins findet sich in Abschnitt 6.1. 4 Die Erkenntnis, dass ein solches Vorhaben für eine dynamische Sprache in eine Sisyphusarbeit mündet, da eine Unzahl von sprachspezifischen Fällen berücksichtigt werden müssen, wird im Abschnitt 6.1 mit einbezogen. 42 Objekte besitzen, die die Variablen im Verlauf des Programms referenzieren. Für einen Refaktorisierungswunsch in einem Ruby-Projekt müssen daher über eine statische Codeanalyse ggf. viele unterschiedliche Programmabläufe im Quellcode nachverfolgt werden. Je mehr im Programm zur Laufzeit entschieden wird, desto komplexer wird der Versuch, die entsprechenden Fakten statisch zu sammeln. Dieser Exkurs dient u. a. als Grundlage, dasselbe Verhalten später im Hinblick auf Zugriffe von Ruby auf Java diskutieren zu können. Als Beispiel wird ein Programm vorgestellt, in dem erst zur Laufzeit entschieden wird, welchen Typs die Objekte sind, an die die Variable o gebunden wird (s. Listing 5.3). Variablen sind in Ruby typenlos. Spätestens bei einer Nutzereingabe, die den Ablauf verändern kann, ist es nicht mehr möglich, eine Aussage über den darauf folgenden Variablenzustand zu treffen. 1 2 3 4 5 c l a s s RubyClass1 def m puts ’ 1 ’ end end 6 7 8 9 10 11 c l a s s RubyClass2 def m puts ’ 2 ’ end end 12 13 o = RubyClass1 . new 14 15 x = g e t s . chomp 16 17 18 19 i f (2==x . to_i ) # Eingabe ... o = RubyClass2 . new end 20 21 o .m # Ueber Eingabe wurde das Objekt bestimmt , das nun o referenziert Listing 5.3: Bindung an unterschiedliche Objekte in Ruby Nach Nutzereingabe wird entweder die Klasse RubyClass2 instanziiert oder nicht. Über welches Objekt am Ende des Listings 5.3 die Methode m wirklich gerufen wird, ist nicht vorhersehbar. Mit Hilfe des Plugins AST4CongenJRubyView können der AST des Programms und die Eigenschaften beobachtet werden (s. Abbildung 5.2). Die Variable o wird mit den entsprechend möglichen Klassennamen der Objekte angezeigt, die sie referenzieren könnte. 43 Abbildung 5.2: Ruby AST Darstellung des AST4CongenJRubyView-Plugins mit geöffnetem Informationsfeld Innerhalb dieses Ruby-Quelltextes kommt es zu Mehrdeutigkeiten, die beispielsweise vom DLTK5 nachverfolgt und dort als „Ambigous“[sic] gekennzeichnet werden. Über ein Durchlaufen der Kontrollfluss-Pfade werden die Java-Methodenaufrufe verfolgt und darüber die jeweiligen Typen der Empfänger gefunden. Mehrdeutigkeiten werden gespeichert und können als Informationen für einen Refaktorisierungsprozess genutzt werden.6 Für den Refaktorisierungswunsch, die Methode m der Klasse RubyClass1 umzubenennen, müsste diese Methode ebenso in der Klasse RubyClass2 umbenannt werden (<RubyClass2,RubyClass1>, s. Abbildung 5.2).7 Im Grunde ist also eine Refaktorisierung für dieses einfache Beispiel (noch) möglich. Jedoch lässt sich erahnen, dass schon ein etwas komplexerer Code die Analyse immens erschweren würde. Denn dann muss nach weiteren möglichen Aufrufen der Methode m gesucht werden, um diese Methode dort ebenso umzubenennen. Exemplarisch wäre folgendes Szenario möglich: Eine Klasse RubyClass3 existiert und hat ebenfalls eine Methode m definiert. Würde an einer weiteren Aufrufstelle eine Mehrdeutigkeit zwischen RubyClass3 und RubyClass2 existieren, zeigt sich, dass es nicht ausreicht, nur die 5 org.eclipse.dltk.evaluation.types Eine prototypische eigene Implementierung wird in Abschnitt 6.1 skizziert. 7 Da an dieser Stelle noch kein sprachübergreifender Methodenaufruf existiert, wird als Äquivalent für das einleitende Beispiel exemplarisch mit einen Ruby-Methodenaufruf gearbeitet. 6 44 anfangs gefundenen Klassen RubyClass1 und RubyClass2 zu beachten. Nun muss der Vorgang für RubyClass3 wiederholt werden, also auch für diese Klasse Methodenaufrufe gesucht und umbenannt werden. Und auch nun können wiederum weitere Abhängigkeiten auftreten. Ein anschließendes Problem ist, dass die existierenden DLTK-Pakete zwar für Rubyquellcode implementiert wurden,8 der Javaquellcode von dem DLTK jedoch ignoriert wird, was im Folgenden gezeigt wird. 5.1.2 Mehrdeutigkeit bei Sprachübergriffen zwischen Ruby und Java In folgenden Listing 5.4 wird ein Beispiel für Mehrdeutigkeit in sprachübergreifenden Projekten gezeigt. Die Variable o referenziert entweder auf ein Java- oder auf ein RubyObjekt, die Auswahl erfolgt wie im vorigen Beispiel über Eingabe. 1 2 3 4 5 6 import " a . J a v a C l a s s " c l a s s RubyClass def m puts ’ 1 ’ end end 7 8 9 o = RubyClass . new x = g e t s . chomp 10 11 12 13 i f (2==x . to_i ) # Eingabe o = J a v a C l a s s . new end 14 15 o .m # Es wird entweder ein Java - oder ein Rubyobjekt referenziert Listing 5.4: Bindung an unterschiedliche Objekte in Ruby und Java Hier versagt das DLTK. Wie in Abbildung 5.3 ersichtlich, wird nun nur noch die RubyKlasse RubyClass erkannt, die Java-Klasse wird ignoriert. Dadurch wird die Mehrdeutigkeit nicht erkannt. Das DLTK bietet also momentan keine Möglichkeit, sprachübergreifende Methodenaufrufe zu erfassen. 8 Stand Februar 2012. 45 Abbildung 5.3: Informationsfeld des AST4CongenJRubyView-Plugins Für JRuby und sprachübergreifende Methodenaufrufe ist dementsprechend eine Erweiterung nötig: In Ruby-Quelltexten müssen die Java-Aufrufe eindeutig identifiziert und im AST des DLTK mit notiert werden. In dem Plugin AST4CongenJRubyView wurden die Voraussetzungen dafür prototypisch implementiert: hier werden die Kontrollfluss-Pfade von Java-Methodenaufrufen verfolgt. Referenzen auf Java-Objekte werden festgehalten und die Methodenaufrufe über das jeweilige Objekt im AST mit abgespeichert (s. Abschnitt 6.1). Visualisiert zeigt das Plugin AST4CongenJRubyView die gefundenen Methodenaufrufe von Ruby nach Java durch einen Pfeil an. Weitere Informationen über den Java-Aufruf befinden sich in dem zum Knoten gehörigen Informationsfeld (s. Abbildung 5.4). 46 Abbildung 5.4: Ruby AST mit visualisierten Zugriffen nach Java Damit ist eine Verbindung der Java-Aufrufe mit der zuvor beschriebenen Möglichkeit, mit Hilfe des DLTK Mehrdeutigkeiten in Ruby-Quelltexten zu finden, hergestellt. Ist der Java-Methodenaufruf eindeutig identifiziert, müssten für den eingangs formulierten Wunsch, die Methode m im Java-Quellcode umzubenennen, nun die abstrakten Syntaxbäume beider Quellcodes mit entsprechenden semantischen Informationen ähnlich dem Exkurs im Abschnitt 5.1.1 für eine Refaktorisierung herangezogen werden.9 Jedoch tritt im folgenden Beispiel eine Verschärfung durch ein Laden von Klassen mit zur Laufzeit generierten Namen, die z. B. aus einem externen Speicher wie einer Konfigurationsdatei geladen werden, ein. Das ist ein nicht unübliches Vorgehen in Softwareprojekten.10 9 In der vorliegenden Ausarbeitung wird nicht mit dem Aufbau eines Java-ASTs, sondern mit Reflexion der Java-Klassen gearbeitet. Dieses Verfahren wird genutzt, weil sprachübergreifende Aufrufe vorerst nur gefunden werden sollen, ein Rückschreiben eines refaktorisierten Quellcodes jedoch nicht Ziel dieser Ausarbeitung ist und daher das Refaktorisieren per Regel verboten wird (s. Abschnitt 4.2). Ein positiver Nebeneffekt ist, dass zum Testen der Binärcode einer Java Klasse ausreicht. Da für JRuby ebenso nur der Binärcode bei Sprachübergriffen für die Lauffähigkeit von Programmen erforderlich ist, kann ggf. die übersetzte Java-Klasse oder die jar-Datei in das Ruby-Projekt gelegt werden. Für ein endgültiges, im Zuge dieser Arbeit nur angedachtes, Rückschreiben von refaktorisiertem Code müssen dann die jeweiligen Quelltexte der beteiligten Sprachen zur Verfügung stehen. Da bisher noch kein einheitliches Projekt angelegt werden kann (s. Abschnitt 2.2), müssten hier über Konfigurationsdateien die jeweiligen Projektpfade angepasst werden. 10 Vgl. Dependency-Injection. 47 5.1.3 Endgültiges Versagen der Typanalyse bei dynamischen Klassennamen In Listing 5.5 kommen die Klassennamen exemplarisch aus einer Datei oder Datenbank und werden über eval ausgeführt. Die Klassennamen selbst, die Anzahl und die Reihenfolge sind vorher nicht bestimmbar. 1 2 # Aus einer Datenbank / XML - Datei . Die Zeichenkette wird hier zum Klassenobjekt c l a s s e s =[ " RubyClass1 " , " RubyClass2 " , " J a v a C l a s s " ] 3 4 5 6 7 x # # # = g e t s . chomp Eingabe 0 => Ausgabe RubyClass1 Eingabe 1 => Ausgabe RubyClass2 Eingabe 2 => Ausgabe JavaClass 8 9 10 # Das Klassenobjekt wird genutzt o = e v a l ( c l a s s e s [ x . to_i ] ) . new 11 12 o .m Listing 5.5: Zeichenkette als Klassenname Wenn nun die Klassennamen nicht mehr aus einer Datenbank kämen, sondern dynamisch über einen nicht trivialen Algorithmus errechnet würden, würde eine automatisierte Codeanalyse versagen.11 Damit tritt der Fall ein, dass im Grunde alle Möglichkeiten in Betracht kämen: Über welches Objekt nun der Aufruf der Methode m erfolgt, ist nicht nachzuvollziehen. 5.1.4 Vorläufiger Ablauf einer statischen Codeanalyse Ein Refaktorisierungsprozess (ohne Rückschreiben) in Refacola, in der eine Java-Methode umbenannt werden soll, benötigt mit statischer Codeanalyse folgenden Aufbau: • Ein AST vom Ruby-Quellcode wird erstellt. • Eine semantische Analyse des Programmcodes wird nach der Ruby-Sprachdefinition durchgeführt. • Aufrufe nach Java werden über einen AST des Javaquelltextes oder über Reflexion unter Zuhilfenahme der schon übersetzt vorliegenden Class-Datei überprüft und mit in den Ruby-AST eingehängt. • Die Informationen werden genutzt, um Fakten bzw. Constraints für Refacola zu generieren. Im Ganzen fasst der Ansatz der statischen Codeanalyse zu kurz. Denn für eine automatisierte Umstrukturierung muss der gesamte Ruby-Code mit den korrekten Eigenschaften 11 Theoretisch ist dieses Problem nicht entscheidbar. 48 als Modell zur Verfügung stehen. Da in einem Ruby-Programm beispielsweise der Programmablauf zur Laufzeit geändert werden kann (s. Abschnitt 3.1.2) und darüber hinaus über Eingabe oder Algorithmus Klassennamen abgeholt oder generiert werden können, die dann geladen werden, ensteht eine schwer nachverfolgbare Komplexität bis hin zu einer nicht mehr entscheidbaren Situation. Das erschwert oder verhindert, die notwendigen Eigenschaften für eine Refaktorisierung statisch zu sammeln. Eine statische Codeanalyse (alleine) reicht also für eine automatisierte Refaktorisierung nicht aus. 5.2 Test-Suite zum Sammeln von Informationen für eine Refaktorisierung Weiterführend für eine Refaktorisierung der dynamischen Sprache Ruby (und vermutlich weiterer dynamischer Sprachen) und daraus folgend ebenso für sprachübergreifende Projekte ist ein Ansatz, der in Anlehnung an den zuvor genannten Refactoring Browser für Smalltalk12 entwickelt wird. Die ursprüngliche Idee stammt von Don Roberts, John Brant und Ralph Johnson, die diesen Ansatz wie folgt beschrieben: „The Refactoring Browser uses method wrappers to collect runtime information. These wrappers are activated when the wrapped method is called and when it returns. The wrapper can execute an arbitrary block of Smalltalk code. To perform the rename method refactoring dynamically, the Refactoring Browser renames the initial method and then puts a method wrapper on the original method. As the program runs, the wrapper detects sites that call the original method. Whenever a call to the old method is detected, the method wrapper suspends execution of the program, goes up the call stack to the sender and changes the source code to refer to the new, renamed method. Therefore, as the program is exercised, it converges towards a correctly refactored program.“ [RBJ97, S. 12] Für diese Arbeit soll jedoch keine Umbenennung der originalen Methode erfolgen, sondern eine von Roberts an anderer Stelle formulierte Idee genutzt werden, dass Programme zur Laufzeit beobachtet und Informationen über das Programm gesammelt werden können: „The analysis then consists of data that was observed in a running program. Since it is impossible to observe every possible run of the program, the result is still an approximation.“ [Rob99, S. 64] Denn Smalltalk ist im Unterschied zu Ruby imagebasiert. Ruby basiert auf Dateien. Das Image von Smalltalk wird zur Laufzeit verändert. In Ruby müssten die Umbenennungen in Dateien rückgeschrieben werden, was einen immensen Aufwand mit sich bringen würde. Daher wird hier der Weg eingeschlagen, Programme zur Laufzeit zu beobachten. Dafür wird eine in vielen Projekten schon vorhandene Test-Suite eingesetzt.13 Idee ist, während eines Testdurchlaufs die für eine Refaktorisierung notwendigen Informationen zu sammeln. 12 Ein gleichnamiger Ruby Refactoring Browser ist für Emacs entwickelt worden, jedoch seit 2007 anscheinend nicht mehr aktualisiert worden. Vgl. http://www.xref.sk/about.html, Abruf 28.02.2011. 13 Vorteil des Heranziehens von existierenden Test-Suites ist, dass hier kein Mehraufwand für die Programmierenden eintritt, da das Testen in vielen heutigen Projekten zum Standard gehört. 49 Um dieses Vorgehen für ein sprachübergreifendes Projekt sinnvoll einsetzen zu können, wird es ausdrücklich Pflicht für die Programmierenden, eine notwendige Test-Suite zum jeweiligen Projekt zu entwickeln. Im Zuge eines Refaktorisierungsprozesses wird diese dann vor dem Refaktorisierungsprozess durchlaufen, damit alle potentiellen Java-Aufrufe auch tatsächlich stattfinden. Das Durchlaufen der Test-Suite ist ein Äquivalent zum Ausführen des Smalltalkprogramms im Refactoring Browser. Für das eingangs formulierte Beispiel, den Refaktorisierungswunsch, eine Methode im Java-Quellcode umzubenennen, gilt als Zwischenziel, diejenigen Stellen zu finden, an denen diese Java-Methode im Ruby-Quelltext gerufen wird. Diesmal wird statt der statischen Codeanalyse (über Java-AST oder Reflexion) ein Testvorgang vorgeschaltet, mit dem die Java-Methodenaufrufe im Ruby-Quelltext gefunden und dann gespeichert werden. Damit sind die für einen Refaktorisierungsvorgang notwendigen Informationen schließlich vorhanden. Als vorläufiges Ergebnis der prototypischen Implementierung dieser Ausarbeitung werden diese gespeicherten Informationen genutzt, um – mit Hilfe einer entwickelten Software XRefact14 in Verbindung mit dem existierenden Framework Refacola – eine Methodenumbenennung in Java zu verbieten (s. Abbildung 5.5), wenn diese Methode von Ruby aus aufgerufen wird. Ansonsten wird die Methode umbenannt (s. Abbildung 5.6). Abbildung 5.5: Refacola – Methodenumbenennung verboten 14 XRefact wurde als Arbeitstitel der Software gewählt. Paket de.feu.ps.refacola.jruby mit Modifizierung der Java Sprachdefinition des Refacola-Frameworks (s. Abschnitt 6.2). 50 Abbildung 5.6: Refacola – Methodenumbenennung erlaubt Schwerpunkt muss nun die Suche nach praktikablen Möglichkeiten sein, die sprachübergreifenden Aufrufe während der Laufzeit zu erkennen. Im Folgenden werden zwei Ansätze vorgestellt, die für eine Refaktorisierung notwendigen Informationen über ein zuvor erfolgtes Durchlaufen der möglichen Programmpfade zu erhalten: zum einen das Nutzen eines modifizierten JRuby-Interpreters, zum anderen das Verwenden eines JavaPakets, durch das Methodenaufrufe von Ruby nach Java auf der Java Virtual Machine beobachtet werden können. Die Vor- und Nachteile dieser Ansätze werden diskutiert und im Folgekapitel die prototypischen Umsetzungen der Ansätze beschrieben (s. Kapitel 6). 5.2.1 Test-Suite mit modifiziertem JRuby-Interpreter Für das Erkennen von sprachübergreifenden Aufrufen kann der JRuby-Interpreter selbst modifiziert werden. Da der Interpreter eine freie Software (s. Abschnitt 3.1.1) ist, besteht ohne Weiteres die Möglichkeit, einen für Refaktorisierungsmöglichkeiten weiter entwickelten Interpreter bereitzustellen. Grundsätzlich muss dafür eine Speicherung der Java-Aufrufe in die Interpreter-Software integriert werden. Im JRuby-Interpreter bieten sich dafür diejenigen Stellen an, an denen die Verantwortlichkeit für die Sprachübergriffe implementiert wurde (s. Abschnitt 6.2.1). Vorteil ist, dass an diesen Stellen ebenso weitere, später für das Rückschreiben eines veränderten, refaktorisierten Quellcodes notwendige, Informationen zur Verfügung stehen, beispielsweise die Zeilennummer des Methoden-Aufrufs in Ruby, denn der Interpreter nutzt die Zeilennummer selbst für ein Schreiben eines Methoden-Rückgabewerts nach Ruby. Mit anderen Worten könnte die gesamte Infrastruktur des Interpreters gleich auch für den Refaktorisierungsprozess genutzt werden. Ein Refaktorisierungsprozess mit Test-Suite in Verbindung mit Refacola und einem modifizierten Interpreter hat folgende Reihenfolge: 51 1. Eine Testsuite wird implementiert. 2. Die Testsuite wird vor jedem Refaktorisierungsprozess gestartet; die potentiellen JavaAufrufe finden statt. 3. Der modifizierte JRuby-Interpreter zeichnet die Methodenaufrufe von Ruby nach Java auf. 4. Die Aufzeichnungen werden genutzt, um Fakten bzw. Constraints für Refacola zu generieren. 5.2.2 Test-Suite mit Instrumentierung Als zweite Idee für die Suche nach sprachübergreifenden Aufrufen wird das Paket java. lang.instrument15 (s. Abschnitt 6.2.3) herangezogen. Diese API wird genutzt, um in einer laufenden Java Virtual Machine Klassen zu verändern. Für die Refaktorisierung reicht jedoch ein Nachverfolgen der sprachübergreifenden Methodenaufrufe über das Instrumentieren der JVM-Instanz aus. Ein großer Vorteil gegenüber dem im vorigen Abschnitt vorgeschlagenen Ansatz, den JRuby-Interpreter zu modifizieren, ist, dass der Einsatz des Pakets java.lang.instrument die Möglichkeit bietet, die Interaktion zwischen mehreren unterschiedlichen Programmiersprachen über die Java Virtual Machine zu loggen. Für Projekte, die neben Java und Ruby (über JRuby) noch Python (über Jython), Groovy oder Clojure nutzen, könnte der Ansatz praktikabler sein, da nicht alle Interpreter modifiziert werden müssten, sondern einheitlich die Aufrufe über die gleiche JVM-Instanz geloggt werden. Nachteil dieses Ansatzes ist, dass an dieser Stelle weitere Informationen wie Zeilennummern nicht mehr vorhanden sind. Ein Refaktorisierungsprozess mit Test-Suite in Verbindung mit Refacola und JavaInstrument ersetzt in der vorigen Aufzählung den Punkt 3: 3. Die JVM-Instanz wird instrumentiert und Methodenaufrufe von Ruby (oder anderen Programmiersprachen, die auf der JVM laufen) nach Java aufgezeichnet. 5.3 Ausblick auf eine sinnvollen Verbindung der unterschiedlichen Ansätze Im Jahre 2006 sagte Charles Nutter, einer der Entwickler von JRuby, bezüglich Refaktorisierung, dass erst eine Kombination aus statischer und dynamischer Analyse ein zufriedenstellendes Ergebnis für eine Refaktorisierung von Ruby liefern wird: „Even then, Ruby’s ability to rewrite itself through a wide variety of eval calls makes refactoring with 100% confidence nearly impossible. I believe that some combination of static analysis and online refactoring will be necessary to sufficiently solve the refactoring question.“ [ENEB06] 15 http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html 52 Eine Verbindung der skizzierten statischen Codeanalyse mit dem Ansatz, über TestSuites die notwendigen Informationen bereit zu stellen, könnte ein Schritt in diese von Nutter nur angedeutete Richtung sein.16 Denn beide Ansätze für sich genommen haben Vor- und Nachteile. Der im vorigen skizzierte Vorteil der statischen Codeanalyse ist, dass in der hier genutzten Entwicklungsumgebung Eclipse bereits notwendige statische Informationen bereitgestellt werden, da sie für beispielsweise die Quelltextdarstellung (Syntax Highlighting) ebenfalls benötigt werden. Der ebenso dargelegte Nachteil der statischen Codeanalyse, dass im Zusammenhang mit den dynamischen Eigenschaften der Sprache Ruby die Nachverfolgung komplex bis nicht mehr entscheidbar wird (s. die Abschnitte 5.1.2 und 5.1.3), macht jedoch ein alleiniges Nutzen der statischen Codeanalyse für eine Refaktorisierung unzureichend. Im Gegensatz dazu ist es der Vorteil des Ansatzes des Sammelns von notwendigen Informationen über eine Test-Suite, dass bei korrekt erstellten Tests alle potentiellen sprachübergreifenden Methodenaufrufe (und alle Ruby-Methodenaufrufe) gefunden werden. Für eine Verbindung beider Ansätze spricht, dass der Fall eintreten kann, dass Programmierende Test-Suites vernachlässigen oder sie unvollständig führen. Dann würde der Vorteil des Ansatzes, über eine Test-Suite Informationen für eine Refaktorisierung zu sammeln, negiert, denn es käme zu falschen Ergebnissen. Eine vorher mit einbezogene statische Codeanalyse könnte hier die Fehler zumindest abmildern. Mit anderen Worten bietet eine Verbindung der statischen Analyse mit dem Ansatz des Nutzens von Test-Suites die Möglichkeit, in programmiersprachenübergreifenden Projekten mit JRuby und Java nahezu die gleichen Möglichkeiten für Refaktorisierung zu erhalten, wie in Refaktorisierungsprozessen über die Sprache Java ohne Sprachübergriff. Erweiternd kann dieser Ansatz m. E. für alle für die Java Virtual Machine implementierten Sprachen gelten. Zumindest dann, wenn eine Test-Suite mit Instrumentierung genutzt wird (s. Abschnitt 5.2.2), können über mehrere beteiligte Sprachen die Aufrufe über die Java Virtual Machine geloggt werden. Ob das ebenso auf der Microsoft .net Plattform möglich ist, wird an dieser Stelle nicht weiter untersucht. Jedoch existiert hier ebenso eine Möglichkeit zur Instrumentierung (vgl. [Pie01]). 16 Eine weitere für Ruby vorhandene Möglichkeit, Quellcode nachzuverfolgen, die an dieser Stelle nicht mehr diskutiert wird: http://wiki.ruby-portal.de/Referenz:Kernel#set_trace_func, Abruf 05.07.2011. 53 6 Beschreibung der Software 6.1 Das Plugin AST4CongenJRubyView Das Eclipse-Plugin AST4CongenJRubyView zeigt den abstrakten Syntaxbaum des Ruby-Quelltextes als Baumansicht. Über Doppelklick auf einzelne Knoten des Baumes werden Informationen über das jeweilige Element angezeigt(s. Abbildung 5.2). AST4CongenJRubyView arbeitet nur mit Ruby-Dateien, bei Aufruf einer Java-Datei wird eine Fehlermeldung angezeigt. Gefundene sprachübergreifende Aufrufe werden in der Baumansicht durch Pfeile gekennzeichnet. Das Plugin wurde über File>New>Project>Plug-in Project angelegt. Im EclipseWizard wurde als Ansicht der Tree viewer ausgewählt, der eine Baumansicht zur Verfügung stellt. Die Inhalte der jeweiligen Knoten des Baums werden durch das Überschreiben der gewünschten Funktionen der Klasse AstVisitor gesammelt; dafür wurde die Klasse AstInformationFinder von AstVisitor abgeleitet (s. Abbildung 6.1). Zusätzlich wurde eine sprachenunabhängige Möglichkeit des DLTK für Typinferenz herangezogen, die für Ruby implementiert ist: Im Paket org.eclipse.dltk.ruby.typeinference befindet sich dafür die Klasse RubyTypeInferencer. Da die DLTK-Unterstützung für Ruby noch im Aufbau ist, galt es, die Möglichkeiten auszuloten, die diese Klasse bietet. Die Klasse AstInformationFinder integriert beide Ansätze. Abbildung 6.1: AST4CongenJRubyView - Aufbau der Plugin-Software Java-Methodenaufrufe aus Ruby werden prototypisch über Reflexion gefunden (s. Ab- 54 schnitt 5.1). Die Klasse JavaPerClassLoadReflector übernimmt die Aufgabe, nach den entsprechenden Methodennamen zu suchen. Insgesamt kann das Plugin herangezogen werden, um sich einen Überblick über das aktuelle Ruby-Skript einschließlich existierender Java-Methodenaufrufe zu verschaffen. Die ursprüngliche Idee, damit genug Informationen für einen Refaktorisierungsprozess zu erhalten, wurde im Abschnitt 5.1.3 bereits theoretisch negiert. 6.2 XRefact XRefact wurde in Anlehnung an den Refactoring Browser aus Smalltalk (s. Abschnitt 1.2.1) angedacht. XRefact ist in das Framework Refacola integriert und dient zum Sammeln der Informationen, die für einen Refaktorisierungsprozess in Refacola benötigt werden. XRefact vereint einen modifizierten JRuby-Interpreter, modifizierte Teile des Frameworks Refacola, Teile des Plugins AST4CongenJRubyView und ein Programm, das mit Hilfe des Java-Pakets java.lang.instrument (s. Abschnitt 5.2.2) und der Software des Projekts Javassist1 entwickelt wurde. Für XRefact wurde • JRuby modifiziert, so dass dort Methodenaufrufe von Ruby nach Java aufgezeichnet werden, • Refacola modifiziert, so dass die Aufzeichnungen benutzt werden können, um Fakten bzw. Constraints zu generieren, • eine Software mit Hilfe des Pakets java.lang.instrument und Javassist erstellt, um über die Java Virtual Machine selbst Methodenaufrufe zu finden. Im Outline-Fenster kann eine Java-Methode ausgewählt werden, die umbenannt werden soll (s. Abbildung 6.2). 1 http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/ 55 Abbildung 6.2: Umbenennen einer Java-Methode im Outline Ein Refaktorisierungs-Wizard bietet die Möglichkeit, einen neuen Methodennamen einzugeben. Als nächstes wird im Wizard angezeigt, ob das Umbenennen der JavaMethode erlaubt oder, wenn ein Aufruf dieser Methode aus Ruby gefunden wurde, verboten wird (s. Abbildung 5.6 und 5.5). Ist das Umbenennen erlaubt, wird die Methode im Java-Quelltext umbenannt, bei Verbot werden die einzelnen Aufrufe mit Zeilennummern angezeigt und der Wizard beendet. Wird versucht, eine Ruby-Methode im Ruby-Quelltext umzubenennen, wird eine Meldung generiert, dass dies in diesem Prototyp noch nicht erlaubt ist. Als Speichermedium wurde eine XML-Datei gewählt. Diese Datei wird durch den modifizierten JRuby-Interpreter geschrieben, wenn ein Test ausgeführt wird (s. Abbildung 6.3). 56 Abbildung 6.3: Test-Suite Nur die sprachübergreifenden Methodenaufrufe werden gespeichert. Wird eine Methode mehrmals aufgerufen, können die Aufrufe über die Zeilennummern identifiziert werden (s. Abbildung 6.4). Abbildung 6.4: XML-Datei 6.2.1 Modifizierter JRuby-Interpreter Der JRuby-Interpreter wurde an den Stellen modifiziert, an denen die Java-Methoden gesucht und aufgerufen werden.2 Dort wurde ebenso das Speichern von Informationen 2 Klasse InstanceMethodInvoker in org.jruby.java.invokers. 57 in die später benötigte XML-Datei eingefügt (s. Abbildung 6.4). 6.2.2 Modifiziertes Refacola-Framework Zum einen wurde Refacola durch den Refaktorisierungs-Wizard erweitert. In einem Ordner JRuby befindet sich dafür u. a. das Ruleset, in dem definiert wird, dass eine Refaktorisierung (vorerst) verboten wird, wenn eine Java-Methode umbenannt werden soll, die aus Ruby aufgerufen wird (s. Listing 4.2). Zum anderen wurde das Auslesen der XML-Datei eingefügt. Mit Hilfe der Informationen aus dieser Datei werden die Fakten generiert, die schließlich ggf. zum Verbot der Refaktorisierung führen. Dafür wurde die im Framework existierende Klasse PropertyFactGenerator um das Auffinden und Vergleichen des Ruby-Methodenaufrufs mit der Java-Methodendefinition erweitert. In der Refacola Language Definition für Java wurde die passende Query hinzugefügt (s. Listing 4.1). Auf der Konsolenausgabe kann der Fakt bindsFromRuby beobachtet werden, der generiert wird, wenn der Verbotsfall eintritt. Vorerst nur zur Beobachtung (mit Ausblick auf eine mögliche Verbindung der Ansätze, wie in Abschnitt 5.3 beschrieben) wurde darüber hinaus der AST-Aufbau des Plugins AST4CongenJRubyView über eine Refacola-Extension mit eingebunden in die Konsolenausgabe. 6.2.3 Instrumentation Abbildung 6.5: Interpreter-Argument Ein Java-Agent wird als Argument an JRuby übergeben. Die Software instrumentation.jar besitzt eine premain-Methode, die vor der main-Methode ausgeführt wird und in derselben Java Virtual Machine arbeitet wie der JRuby-Interpreter-Prozess. Im Manifest der Jar-Datei muss die Premain-Class notiert werden. Nun können Methodenaufrufe in der Java Virtual Machine nachverfolgt werden und mit Hilfe der Software Javassist, die in die instrumentation.jar integriert wurde, die entsprechenden Namen, Parameter, Parametertypen und Rückgabewerte gefunden werden (s. Listing 6.1). 58 E n t e r i n g a . J a v a C l a s s . m1( i n t ) −− m1 ( I )V 4711 E x i t i n g a . J a v a C l a s s . m1( i n t ) Listing 6.1: Konsolenausgabe des Agenten instrumentation.jar 59 A Auflistung, Installations- und Bedienungsanleitung der beigelegten Software A.1 Beigelegte Software • Software XRefact: XRefact als Eclipse-Projekte (mit Quellcode, gepackt als Zip-Datei). Modifizierter JRuby-Interpreter (Version 1.6.5). • Software zum Paket java.lang.instrument: instrumentation.jar (mit Quellcode, gepackt als Zip-Datei). Hinweise zum Erstellen und Ausführen der jar-Datei (instrumentation.txt). • Test-Projekte: Eclipse-Projekt: javaproject (mit Quellcode, gepackt als Zip-Datei). Eclipse-Projekt: rubyproject (mit Quellcode, gepackt als Zip-Datei). • Plugin AST4CongenJRuby (mit Quellcode, gepackt als jar-Plugin-Datei). A.2 Installation und Bedienung A.2.1 XRefact • Notwendige Voraussetzungen: Eclipse mit Eclipse Modeling Framework einschließlich Xtext und Xpand SDK; DLTK einschließlich Ruby-Unterstützung. • XRefact basiert auf Refacola. Einige Pakete von Refacola wurden für den vorliegenden Prototypen erweitert. Daher sollte XRefact mit den veränderten Refacola-Sourcen und dem eigens für JRuby implementierten Paket importiert werden. • Refacola, Stand Oktober 2011. Im Detail wurden folgende Veränderungen vorgenommen: Hinzufügt: Projekt de.feu.refacola.jruby. Hinzufügt: In Projekt de.feu.ps.refacola.lang.java.jdt den Ordner de.feu.ps.refacola.lang.jruby.facts. Verändert: refacola/lang/java/jdt/factgeneration/PropertyFactGenerator.java (Veränderungen gekennzeichnet). Verändert: refacola/Java.language.refacola (Veränderungen gekennzeichnet). 60 • mwe2-Workflow mit run ausführen: GenerateJRubyApi.mwe2 in Projekt de.feu.ps.refacola.jruby,Ordner refacola. GenerateJavaApiJava.mwe2 in Projekt de.feu.ps.refacola.lang.java, Ordner refacola. • Das de.feu.ps.refacola.jruby-Paket muss als Eclipse-Applikation gestartet werden. • Die mitgelieferten Java- und Rubyprojekte müssen in die Eclipse Runtime Application importiert werden. Hinweis: Die Pfade sind für diesen Prototypen fest notiert, siehe dazu Hilfsdatei xrefact.txt. Daher sollten die Projekte nicht umbenannt werden, andernfalls müssen die in der xrefact.txt festgehaltenen Pfade in den jeweiligen Dateien mit umbenannt werden. • Der modifizierte JRuby-Interpreter ist einsatzbereit. Er kann jedoch auch über die beiliegende ant-Datei1 übersetzt werden. • Der modifizierte JRuby-Interpreter muss als Ruby-Interpreter in der Eclipse eingetragen sein. • Die Applikation zur Instrumentierung liegt im JRuby-Ordner. • Soll zusätzlich instrumentiert werden, muss der Ruby-Interpreter mit dem Aufruf -J-javaagent:/pfad/instrumentation.jar gestartet werden (s. Abbildung 6.5). • Zum Umbenennen einer Java-Methode: Aufruf Outline-View, rechte Maustaste auf umzubenennende Methode, Rename Java...(Refacola) starten. A.2.2 Plugin AST4CongenJRubyView • Notwendige Voraussetzungen: Eclipse mit DLTK einschließlich Ruby-Unterstützung. • Die jar-Datei AST4CongenJRubyView.jar als Plugin installieren. • Zu finden ist das Plugin unter Window>Show View>Other... • Bei geöffnetem Rubyskript den AST-TreeView öffnen. Informationen über einzelne Knoten können über Doppelklick abgerufen werden. 1 http://ant.apache.org 61 B Abkürzungen API AST DLTK EIAO GPL irb JVM POLS RJB TIMTOWTDI Application Programming Interface Abstract Syntax Tree Dynamic Languages Toolkit Everything is an Object GNU General Public License Interactive Ruby Java Virtual Machine Principle of least surprise Ruby Java Bridge There is more than one way to do it 62 C Liste bekannter Refaktorisierungswerkzeuge • IntelliJ IDEA http://www.jetbrains.com/idea/features/refactoring.html • RubyMine http://www.jetbrains.com/idea/features/refactoring.html • Eclipse http://www.eclipse.org/articles/Article-LTK/ltk.html • NetBeans RefactoringNG http://kenai.com/projects/refactoringng • Visual Studio Refactor! Pro http://www.devexpress.com/Products/Visual_Studio_Add-in/Refactoring • ReSharper http://www.jetbrains.com/resharper • Smalltalk Refactoring Browser http://st-www.cs.illinois.edu/users/brant/Refactory/RefactoringBrowser.html • Emacs XRefactory http://www.xref.sk/about.html • Ruby Refactoring Browser http://www.kmc.gr.jp/proj/rrb/index-en.html 63 Abbildungsverzeichnis 2.1 Projekte im Package Explorer . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.1 Abbildung von Datentypen über JRuby . . . . . . . . . . . . . . . . . . . 30 5.1 5.2 5.3 5.4 5.5 5.6 Umbenennen der Java-Methode im Framework Refacola . . . . . . . . . Ruby AST Darstellung des AST4CongenJRubyView-Plugins mit geöffnetem Informationsfeld . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Informationsfeld des AST4CongenJRubyView-Plugins . . . . . . . . . . Ruby AST mit visualisierten Zugriffen nach Java . . . . . . . . . . . . . Refacola – Methodenumbenennung verboten . . . . . . . . . . . . . . . . Refacola – Methodenumbenennung erlaubt . . . . . . . . . . . . . . . . . . . . . 44 46 47 50 51 6.1 6.2 6.3 6.4 6.5 AST4CongenJRubyView - Aufbau der Plugin-Software Umbenennen einer Java-Methode im Outline . . . . . Test-Suite . . . . . . . . . . . . . . . . . . . . . . . . . XML-Datei . . . . . . . . . . . . . . . . . . . . . . . . Interpreter-Argument . . . . . . . . . . . . . . . . . . . . . . . 54 56 57 57 58 64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Listingverzeichnis 2.1 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.20 3.21 3.22 3.23 4.1 4.2 4.3 4.4 4.5 4.6 5.1 5.2 5.3 5.4 5.5 6.1 Bekanntmachen des Javaprojektes . . . . . . . . . . . . . . . . . . . Zahlen sind Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichenketten sind Objekte . . . . . . . . . . . . . . . . . . . . . . . Ruby klassenbasiert . . . . . . . . . . . . . . . . . . . . . . . . . . . Ruby prototypbasiert . . . . . . . . . . . . . . . . . . . . . . . . . . . Mixins mit Klassen und Objekten . . . . . . . . . . . . . . . . . . . . Ruby prozedural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ruby funktional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . instance_eval und class_eval . . . . . . . . . . . . . . . . . . . . Unterschied protected und private . . . . . . . . . . . . . . . . . . Sichtbarkeiten ändern . . . . . . . . . . . . . . . . . . . . . . . . . . Aufruf einer Ruby-Methode aus Java . . . . . . . . . . . . . . . . . . Aufruf einer Java-Methode aus Ruby . . . . . . . . . . . . . . . . . . Private Java-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . Sichtbarkeitstest einer Methode . . . . . . . . . . . . . . . . . . . . . Privates Java-Feld . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf das private Java-Feld . . . . . . . . . . . . . . . . . . . . Überladene Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . Problematik bei überladenen Methoden . . . . . . . . . . . . . . . . Zahl wird als Integer erkannt . . . . . . . . . . . . . . . . . . . . . Zugriff auf Membervariablen in Ruby . . . . . . . . . . . . . . . . . . Äquivalente Zugriffe auf eine Java-Membervariable von Ruby aus . . to_s und toString . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausgabe einer String-Repräsentation eines Java-Objektes in Ruby . . Auszug aus der Refacola Language Definition: java.language.refacola Beispiel-Regel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methodenumbenennung verbieten: einstellige Query . . . . . . . . . Auszug aus der Refacola Language Definition: java.language.refacola Methodenumbenennung verbieten: java.ruleset.refacola . . . . . . . . Definition von Refaktorisierungen: java.refactoring.refacola . . . . . . Java-Klasse mit Methode . . . . . . . . . . . . . . . . . . . . . . . . Bindung an unterschiedliche Objekte mit Ruby und Java . . . . . . . Bindung an unterschiedliche Objekte in Ruby . . . . . . . . . . . . . Bindung an unterschiedliche Objekte in Ruby und Java . . . . . . . Zeichenkette als Klassenname . . . . . . . . . . . . . . . . . . . . . . Konsolenausgabe des Agenten instrumentation.jar . . . . . . . . . 65 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 22 22 23 23 23 24 24 25 26 26 27 28 29 29 29 29 30 30 30 32 32 32 33 35 36 37 38 38 38 40 41 43 45 48 59 Literaturverzeichnis [ASU99] Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullmann: Compilerbau Teil 1. 2. Auflage, Oldenburg Wissenschaftsverlag, München, Wien, Oldenburg, 1999. [Bec00] Kent Beck: Extreme Programming. Addison-Wesley Verlag, München, 2000. [Dij68] Edsger W. Dijkstra: Go To Statement Considered Harmful. In: Communications of the ACM, Vol. 11, No. 3, März, 1968, S. 147-148. [DW02] Thomas Dudziak, Jan Wloka: Tool-Supported Discovery and Refactoring of Structural Weaknesses in Code. Diploma Thesis of the Faculty of Computer Science, Technical University of Berlin, 2002. [ES92] Stefan Eicker, Thomas Schnieder: Reengineering. Arbeitsbericht Nr. 13, Institut für Wirtschaftsinformatik, Wilhelms-Universität Münster, 1992. [ENEB06] Pat Eyler, Charles Nutter, Thomas Enebo, Ola Bini: A New JRuby Interview and More. Linux Journal, Pat Eyler’s blog, 29. September, 2006. Von: http://www.linuxjournal.com/node/1000103, Abruf am 10.03.2012. [Fow05] Martin Fowler. Mit Beiträgen von: Kent Beck, John Brant, William Opdyke, Don Roberts: Refactoring. Wie Sie das Design vorhandener Software verbessern. Addison-Wesley Verlag, München, 2005. [Gri91] William G. Griswold: Program Restructuring as an Aid to Software Maintenance. Ph D. Thesis, University of Washington, Department of Computer Science and Engineering, 1991. [JO93] Ralph E. Johnson, William F. Opdyke: Refactoring and Aggregation. In: Proceedings of the JSSST International Symposium on Object Technologies for Advanced Software, 1993, S. 264-278. [Mar09] Robert C. Martin: Clean Code. Refactoring, Patterns, Testen und Techniken für sauberen Code. 1. Auflage, mitp-Verlag, Heidelberg, München, Landsberg, Frechen, Hamburg, 2009. [MO08] Hussein Morsy, Tanja Otto: Ruby on Rails 2. Das Entwickler-Handbuch. 1. Auflage, Galileo Press, Bonn, 2008. [NBDDGW05] Oscar Nierstrasz, Alexandre Bergel, Marcus Denker, Stephane Ducasse, Markus Gälli, Roel Wuyts: On the Revival of Dynamic Languages. In: Thomas Gschwind, Uwe Aßmann, Oscar Nierstrasz (Hrsg.): Software Composition. 4th International Workshop, SC 2005 Edinburgh, UK, 9. April, 2005, S. 1-13. 66 [New08] Ted Neward: Einführung in F#. Funktionale Programmierverfahren im .NET Framework. In: MSDN Magazine, Microsoft Deutschland, Unterschleißheim, Mai, 2008. [NSEBD11] Charles O. Nutter, Nick Sieger, Thomas Enebo, Ola Bini, Ian Dees: Using JRuby. Bringing Ruby to Java. The Pragmatic Programmers LLC, The Pragmatic Bookshelf, Raleigh, North Carolina; Dallas, Texas, 2011. [Opd92] William F. Opdyke: Refactoring Object-Oriented Frameworks. University of Illinois at Urbana-Champaign, 1992. [Paw07] Monica Pawlan: JRuby and the Java Platform. Oracle, Sun Developer Network (SDN), 2007. Von: http://java.sun.com/developer/technicalArticles/scripting/jruby/, Abruf am 10.06.2011. [Pie01] Matt Pietrek: The .NET Profiling API and the DNProfiler Tool. In: MSDN Magazine, Microsoft, San Francisco, Dezember, 2001. [RBJ97] Don Roberts, John Brant, Ralph E. Johnson: A Refactoring Tool for Smalltalk. In: Theory and Practice of Object Systems, Vol. 3, Nr. 4, 1997, S. 253-263. [RL04] Stefan Roock, Martin Lippert: Refactorings in großen Softwareprojekten. 1. Auflage, dpunkt.verlag, Heidelberg, 2004. [Rob99] Donald Bradley Roberts: Practical Analysis for Refactoring. Ph D., University of Illinois at Urbana-Champaign, 1999. [SDSTT10] Max Schäfer, Julian Dolby, Manu Sridharan, Emina Torlak, Frank Tip: Correct Refactoring of Concurrent Java Code. In: ECOOP’10 Proceedings of the 24th European conference on Object-oriented programming, Springer-Verlag Berlin, Heidelberg, 2010, S. 225-249. [SKP11] Friedrich Steimann, Christian Kollee, Jens von Pilgrim: A Refactoring Constraint Language and its Application to Eiffel. In: ECOOP 2011, Lehrgebiet Programmiersysteme, FernUniversität in Hagen, 2011. S. 255-280. [Sli05] Stefan Slinger: Code Smell Detection in Eclipse. Thesis Report, Delft University of Technology, März, 2005. [SM01] Bruce Stewart, Yukihiro Matsumoto: An Interview with the Creator of Ruby. Interview, linuxdevcenter, O’REILLY, 29. November, 2001 Von: http://linuxdevcenter.com/pub/a/linux/2001/11/29/ruby.html, Abruf am 10.03.2012. [SRK07] Daniel Speicher, Tobias Rho, Günter Kniesel: JTransformer – Eine logikbasierte Infrastruktur zur Codeanalyse. WSR’07 - 9. Workshop Software-Reengineering der GIFachgruppe Software-Reengineering, Bad Honnef, 2.-4. Mai, 2007. 67 [ST10] Friedrich Steimann, Andreas Thies: From behaviour preservation to behaviour modification: Constraint-based mutant generation. In: ICSE ’10 Proceedings of the 32nd ACM/IEEE International Conference of Software Engineering – Volume 1, 2010, S. 425-434. [Ste10] Friedrich Steimann: Korrekte Refaktorisierungen: Der Bau von Refaktorisierungswerkzeugen als eigenständige Disziplin. In: OBJEKTspektrum, SIGS DATACOM GmbH, Troisdorf, Nr. 4, 2010, S. 24-29. [TFKEBS09] Frank Tip, Robert M. Fuhrer, Adam Kiezun, Michael D. Ernst, Ittai Balaban, Bjorn De Sutter: Refactoring using Type Constraints. IBM T.J. Watson Research Center technical report RC24804, Hawthorne, NY, 2009. [TKB03] Frank Tip, Adam Kiezun, Dirk Bäumer: Refactoring for generalization using type constraints. In: Proceedings of the 18th annual ACM SIGPLAN conference on Object-oriented programing, systems, languages, and applications (OOPSLA ’03), New York, 2003, S. 13-26. [TR10] Andreas Thies, Christian Roth: Recommending Rename Refactorings. In: 2nd International Workshop on Recommendation Systems for Software Engineering (RSSE), 2010, S. 1-5. 68