Meine erste selbstgebaute Sprache
02.03.2009 Permalink Während der Feiertage des Jahreswechsels 08/09 wollte ich Xtext ausprobieren, denn aufgrund der Arbeiten unserer Kollegen in Kiel und Stuttgart sowie meiner Lektüre der Veröffentlichungen von Martin Fowler und Martin Ward hielt ich es für dringend erforderlich, mir selbst ein praktisches Gefühl für LOP, also den Bau von domänenspezifischen Sprachen zu verschaffen.Wie aufwändig ist sowas? Welche "besonderen Fähigkeiten" werden verlangt? Und wie gut funktionieren die Werkzeuge dazu, allen voran natürlich oAW und Xtext?
Meine persönlichen Voraussetzungen halte ich für durchschnittlich. Sehr gute OO Programmierkenntnisse, aber kaum theoretisches Wissen zum Compilerbau. Praktischer Generatorbau auf Basis von oAW hatte ich auch erst im Rahmen eines itemis internen Powerworkshops besser kennengelernt, Xtext war mir jedoch völlig neu. Dafür kannte ich die von mir gewählte Domäne ganz gut.
Domäne: User Interface
Wer sich schonmal mit der Architektur von User Interfaces beschäftigt hat, wird
wahrscheinlich schon über Fowlers
PresentationModel
gestolpert sein.
Die Idee ist, kurzgesagt, Daten und Verhalten soweit wie möglich aus den View-Klassen
herauszuziehen und in einer eigenen Modell-Klasse zu vereinen. Diese ist
unabhängig von UI-Technologie und daher leichter zu warten und besser zu testen,
als wenn alles zusammen an einem Ort codiert ist. Bildlich kann man sich das
so vorstellen:
Durchaus Popularität findet dieses Rich Client Muster lustigerweise im Web Frontend Bereich, nämlich bei JavaServer Faces. Hier heißt das PresentationModel dann BackingBean, aber die Rollenverteilung bleibt wie bei Fowler beschrieben.
Die Anwendung dieses Musters führt dazu, dass man je fachlichem "Formular" zwei Klassen schreiben muss: die eine enthält Daten und Logik, die andere die Widgets und das Layout. Zusätzlich braucht's noch ein Stückchen konfigurierenden Code, der die Instanzen der beiden zusammenführt. Die folgenden Kästen zeigen das Model und die View.
Presentation Model:
public class EmployeeDetailsModel extends SingleViewPresentationModel { public EmployeeDetailsModel (Employee employee) { super(); this.employee = employee; initialize(); } public void initialize () { registerField(new DefaultField(TITLE, String.class, ManagedField.GET,ManagedField.SET)); registerField(new DefaultField(EMPLOYEE_FIRSTNAME, String.class, ManagedField.GET,ManagedField.SET)); registerField(new DefaultField(EMPLOYEE_LASTNAME, String.class, ManagedField.GET,ManagedField.SET)); registerField(new DefaultField(EMPLOYEE_POSITION, String.class, ManagedField.GET,ManagedField.SET)); registerField(new DefaultField(OKBUTTONTEXT, String.class, ManagedField.GET,ManagedField.SET)); } protected Employee employee; public Employee getEmployee () { return employee; } public void setEmployee (Employee employee) { this.employee = employee; } protected String title; public String getTitle () { return title; } public void setTitle (String title) { this.title = title; } protected String okButtonText; public String getOkButtonText () { return okButtonText; } public void setOkButtonText (String okButtonText) { this.okButtonText = okButtonText; }
Java Swing View:
public class EmployeeDetailsView extends JDialog implements ManagedView { public EmployeeDetailsView () { super(); initialize(); } public void initialize () { JPanel contentPane = new JPanel(); double sizes[][]={ {10,80,layout.TableLayout.FILL,10}, {10,22,22,22,layout.TableLayout.FILL,32,10} }; contentPane.setLayout(new layout.TableLayout(sizes)); contentPane.add(getFirstnameLabel(), "1,1"); contentPane.add(getEmployeeFirstnameTextfield(), "2,1"); contentPane.add(getLastnameLabel(), "1,2"); contentPane.add(getEmployeeLastnameTextfield(), "2,2"); contentPane.add(getPositionLabel(), "1,3"); contentPane.add(getEmployeePositionTextfield(), "2,3"); contentPane.add(getPanel3Panel(), "1,5,2,5"); setContentPane(contentPane); setTitle(""); setMinimumSize(new Dimension(300,150)); setModal(true); pack(); } private ViewSupport viewSupport; public void setup(ViewSupport vs) { viewSupport = vs; viewSupport.register(EmployeeDetailsModel.EMPLOYEE_FIRSTNAME, new TextfieldSynchronizer(getEmployeeFirstnameTextfield())); viewSupport.register(EmployeeDetailsModel.EMPLOYEE_LASTNAME, new TextfieldSynchronizer(getEmployeeLastnameTextfield())); viewSupport.register(EmployeeDetailsModel.EMPLOYEE_POSITION, new TextfieldSynchronizer(getEmployeePositionTextfield())); viewSupport.register(EmployeeDetailsModel.OKBUTTONTEXT, new ButtonTextSynchronizer(getOkButton())); viewSupport.register("ok", new SwingManagedAction(getOkButton(),"")); viewSupport.register("cancel", new SwingManagedAction(getCancelButton(),"Cancel")); viewSupport.register(EmployeeDetailsModel.TITLE, new WindowTitleSynchronizer(this)); viewSupport.registerComponents(this); } public Object getComponent () { return this; } private JLabel firstnameLabel; public JLabel getFirstnameLabel () { if (firstnameLabel == null) { firstnameLabel = new JLabel(); firstnameLabel.setText("First name"); } return firstnameLabel; } private JTextField employeeFirstnameTextfield; public JTextField getEmployeeFirstnameTextfield () { if (employeeFirstnameTextfield == null) { employeeFirstnameTextfield = new JTextField(); employeeFirstnameTextfield.setName("employeeFirstname"); } return employeeFirstnameTextfield; } . . . }
Gedanklich bleiben die Klassen natürlich eine Einheit, und es wäre praktisch, ein Formular fachlich kompakt spezifieren zu können, so dass die architekturgemäße saubere Trennung in zwei Artefakte von einem Generator übernommen wird. Meine User Interface Language sollte z.B. folgende Ausdrücke erlauben, die ich für UIs immer noch als ausreichend intuitiv erachte, also z.B. für obiges Beispiel dann so:
form EmployeeDetails ( binding(employee:de.itemis.hoa.cleanmvc.samples.bo.Employee) dialog(title:String size[300 150]) layout(table columns[gap label fill gap] rows[gap input input input fill button gap]) elements( c("1,1 ") label "First name" c("2,1 ") textfield binding(employee.firstname:String) c("1,2 ") label "Last name" c("2,2 ") textfield binding(employee.lastname:String) c("1,3 ") label "Position" c("2,3 ") textfield binding(employee.position:String) c("1,5,2,5") panel flow right ( button binding(okButtonText:String) >ok button "Cancel" >cancel ) ) )Vom Wunsch zur Wirklichkeit
1. Hello World
Ich habe erstmal ohne lange zu überlegen den New Xtext Project Wizard verwendet. Und siehe da: das Eclipse Projekt Grundgerüst hatte ich nach gefühlten drei Mausklicks zusammen. Ich habe in der Xtext Grammatikdefinition zwei, drei Ausdrücke hingeschrieben, über Rechtsklick Generate Xtext Artifacts ausgelöst, der Grammatik entsprechend ein "Programm" formuliert und im Generator in einem Template zur Verifikation durch das Programm definierte Elemente ausgegeben. Nach Starten des oAW Workflow hatte ich meine "Hello World" Testausgabe. Das alles hat nicht länger als 30 Minuten gedauert.
2. Struktur
Obwohl meine Ideen von der UI-Language zu Beginn viel rudimentärer waren als im
oben dargestellten Beispiel, war ich mir ziemlich sicher, dass ich mit dem
einfachen oAW Setup hier nicht auskomme. Konkret fehlte mir eine
Modell-zu-Modell (M2M) Transformation, die mir aus der sehr kompakten
Form-Beschreibung den fachlichen Extrakt jeweils für die View und das
PresentationModel in eigene Modellelemente überführt. Daher habe ich ein neues
Ecore basiertes Metamodell namens MVC begonnen und eine eigenständige
Extensions-Datei uil2mvc.ext angelegt, mit der aus dem textbasierten UIL Modell
ein MVC Modell wird, das die Codegenerierung stark vereinfacht. Für den
korrekten oAW Workflow musste ich dann etwas basteln, da ich mich zu wenig damit
auskannte. Aber insgesamt hatte ich ca. zwei Stunden später tragfähige
Strukturen aus Workflow, Modelltransformation, Generator unterstützenden
Extensions und Xpand Templates geschaffen, die ich "nur noch" mit Leben zu
füllen hatte.
3. Vorbild
Ich hatte die oben angedeutete Architektur aus View und PresentationModel schon
etwa drei Jahre als Prototyp in der Schublade liegen, trotzdem musste ich diesen
noch stärker strukturieren, um mir klar zu machen, wie im Detail der generierte
Code aussehen soll. Also habe ich refaktorisiert bis das Ideal erreicht war,
denn ich wollte auf keinen Fall Javacode erzeugen, der nicht nach sorgfältig
handgeschriebenem aussieht.
4. Viele Iterationen
Der Rest war iterative Fleißarbeit: Wunschausdruck formulieren, UIL Grammatik
anpassen, MVC Metamodell erweitern, Transformation schreiben und Xpand Templates
erweitern. Nach und nach kamen die generierten Artefakte dem Vorbild nahe.
Fazit
Was als Experiment und Werkzeugtest gestartet war, zeigte schon nach zwei Tagen
nützliche Ergebnisse. Und es macht Spaß, da die gute Eclipse Integration von
Xtext und oAW flüssiges Arbeiten fördert. Der Einstieg war deutlich leichter als
ich erwartet hatte, allerdings kannte ich die grundsätzliche Verwendung der oAW
Bausteine bereits aus einem Workshop.
Magische Fähigkeiten musste ich nicht entwickeln, die Xtext Grammatik ist schnell gelernt, und sowohl Extend als auch Xpand erweisen sich bei intensiver Arbeit wie eine gut bestückte und sich selbst erweiternde Werkzeugkiste. Es gilt hier noch viel stärker als bei sonstiger Programmierung, dass man redundanzfrei codieren muss, denn nur dann ergibt sich eine elegante Sammlung von Extensions für die Transformation und Generierung.
Es hat sich als unumgänglich erwiesen, eine glasklare Architektur in einem sauberen Prototypen zu haben, sonst wird man zum Kapitän ohne Kompass. Zudem muss man bewusste Entscheidungen treffen, wo man Komplexität unterbringen will:
- in der domänenspezifischen Sprache
- im Framework, das den generischen Teil der Architektur in der Zielsprache enthält
- in Extensions zur M2M Transformation
- in Extensions zur Generatorunterstützung
- in den Xpand Templates
- im Generat
Abschliessend kann ich sagen, dass da ein fantastischer Werkzeugkasten verfügbar ist, um Projekten in Sachen Produktivität und Durchsetzung von Architekturen ohne Monate dauernde "Forschungsarbeit" zu einem echten Quantensprung zu verhelfen.