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:

Idee PresentationModel

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:

Die DSL ist natürlich der völlig falsche Ort, genau die sollte so intuitiv wie möglich sein, sie ist schließlich das neue Frontend, das wir Entwicklern anbieten wollen. Nach meiner jungen Erfahrung scheiden für mich außerdem die Xpand Templates und auch das Generat aus, beide sollten einfach sein. Stattdessen ziehe ich das Framework in der Zielsprache sowie die Extensions vor.

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.