Suche
Neuer Ansatz für Web-Entwicklung

Google Dart

Eberhard Wolff

Die Client-Teile der Web-Anwendungen werden immer komplexer. Für ihre Implementierung gab es mit JavaScript zwar bisher eine Lösung, die aber nicht für so komplexe Anwendungen gedacht war. Nun hat Google dafür eine neue Programmiersprache vorgestellt, nämlich Dart.

Eine Warnung vorab: Dart ist im Moment eine Technologie-Preview. Vieles fehlt noch und vieles kann und wird sich ändern. Das ist auch eine Chance: Dart ist jetzt herausgekommen, um das Feedback der Community in Dart einzubauen. Feedback können Sie an die Entwickler von Dart weitergeben, zum Beispiel durch die Dart User Discussion Group oder das Einstellen von Fehlern in den Issue Tracker.

Hinter Dart steht ein Team bei Google. Zu diesem Team gehört Lars Bak. Er hatte zuvor wesentlichen Anteil an der HotSpot Java VM und arbeitet nun an der V8 JavaScript VM, die in Google Chrome genutzt wird und das Rennen um höhere JavaScript-Performance eröffnet hat. Ein weiteres wichtiges Mitglied des Teams ist Gilad Bracha. Er hatte zuvor unter anderem an der Java Language Specification und an der Java Virtual Machine Specification gearbeitet.

Warum Dart?

Für das Dart-Projekt gibt es verschiedene Gründe: Immer mehr Anwendungen lassen Logik im Browser ausführen. Dieses Modell wird in Zukunft durch HTML5 sogar noch wichtiger werden. Allerdings hieß es bei der Einführung von Dart auf der Goto Konferenz, dass Dart keine Konkurrenz zu JavaScript sein soll, sondern es um den fragmentierten mobilen Markt geht. Eine mögliche Interpretation dieser Aussage ist, das durch Dart HTML5-Oberflächen noch wichtiger für mobile Endgeräte werden sollen, so dass alle Plattformen mit einer einheitlichen Code-Basis bedient werden können. Eine andere Interpretation ist, dass Dart langfristig auf der Android-Plattform Java verdrängen soll. Vor allem wegen der gerichtlichen Auseinandersetzung zwischen Google und Oracle wegen Java ist es sicher gut für Google, dazu noch eine Alternative zu haben.

Interessant ist Dart auch, weil die Sprache explizit darauf ausgelegt ist, für Entwickler möglichst leicht erlernbar und nutzbar zu sein. Dazu sind einige sehr interessante Entscheidungen getroffen worden.

Als Ablaufumgebung kann Dart JavaScript nutzen. Dazu wird der Dart-Code in JavaScript übersetzt. Diesen Ansatz hatte Google mit GWT (Google Web Toolkit) schon für Java implementiert. So kann Dart auf jedem Browser laufen. Zur Zeit werden Chrome, Safari und Firefox unterstützt. Die Kompilierung in JavaScript-Code ist im Moment noch nicht besonders effizient – weder in Bezug auf die Größe des generierten Codes noch in Bezug auf die Laufzeiteffizienz. Laut Lars Bak soll die Performance aber schon vergleichbar sind mit der JavaScript-Performance der ersten Versionen der V8-JavaScript-VM. In diesem Bereich wird sicher noch einiges optimiert.

Einfache Beispiele

Wer Dart ausprobieren will, sollte sich die Dart-Web-Site anschauen. Dort gibt es einige Beispiele, die direkt im Browser laufen können. Dabei wird der Dart-Code mit Hilfe eines Server in JavaScript kompiliert, der auf der Google App Engine läuft, und dann ausgeführt. Schon ein Blick auf das Hello World (Listing 1) zeigt, dass Dart sich in die Tradition der C-Sprachen einreiht, zu denen ja auch Java und JavaScript gehören. Entwickler, die Sprachen aus dieser Familie beherrschen, finden sich recht schnell zurecht.

main() {
print('Hello, Dart!');
}

Das etwas komplexere Beispiel in Listing 2 zeigt in der main-Methode eine abkürzende Schreibweise mit => für einzeilige Funktionsdefinitionen. Außerdem zeigt es Funktionen und typisierte Variablen sowie Templates für die Ausgabe.

int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
main() => print('fib(20) = ${fib(20)}');

Objekt-Orientierung mit Dart

Listing 3 zeigt ein Beispiel für Objekte-Orientierung. Es wird eine Klasse Person mit den beiden Instanz-Variablen name und firstname definiert. Der Konstruktor Person(this.name) ist eine kompakte Schreibweise dafür, dass der erste Parameter des Konstruktors der Instanz-Variable name zugewiesen werden soll. Methoden wie auch Konstruktoren können nicht überladen werden. Es ist also nicht möglich, zwei Methoden mit demselben Namen und unterschiedlichen Parametern zu definieren. Daher gibt es einen zweiten Konstruktor mit einem anderen Namen.

class Person {
String name;
String firstname;
Person(this.name);
Person.withFirstname(this.firstname,this.name);
}
main() {
Person p = new Person.withFirstname('Gilad','Bracha');
print('Hi ${p.firstname} ${p.name}');
}

Neben Konstruktoren können Objekt auch mit Hilfe von Factories erzeugt werden. Verschiedene Factory-Ansätze gehörten schon zu den ersten dokumentierten Design Patterns. Aber in Dart wird dieser Ansatz direkt in der Sprache unterstützt. Ebenfalls gibt es eine Unterstützung für Interfaces. Listing 4 zeigt ein Beispiel: Es wird ein Interface Person mit der Factory PersonFactory definiert. Abhängig von dem Konstruktor-Parameter wird eine konkrete Klasse instanziiert. In der Benutzung unterscheidet sich eine Factory nicht von einem Konstruktor-Aufruf. Es scheint also so, als ob ein Interface instanziiert wird. Der Factory-Ansatz wird auch für Klassen unterstützt. Mit Factories ist die Instanziierung von Objekte praktisch beliebige änderbar –Singletons, Caches und andere Trick können für den Nutzer der Klassen transparent implementiert werden.

Bei Listing 4 fällt auch auf, dass Interfaces anscheinend Instanz-Variablen beinhalten dürfen. In Wirklichkeit sind Instanz-Variablen aber eine Kombination aus Methoden für Getter und Setter. Daher wird ihre Definition auch in einem Interface unterstützt. Sie können auch überschrieben werden, um weitere Logik wie beispielsweise Validierungen zu implementieren.

interface Person factory PersonFactory {
Person(name);
final name;
}

class PersonFactory {
factory Person(name) {
if (name == 'Bracha') {
return new DartDeveloper(name);
}
return new RealPerson(name);
}
}

class DartDeveloper implements Person {
DartDeveloper(this.name);
String name;
}

class RealPerson implements Person {
RealPerson(this.name);
String name;
}

main() {
print(new Person('Wolff') is DartDeveloper);
print(new Person('Bracha') is DartDeveloper);
}

[ header = Seite 2: Darts Typsystem ]

Darts Typsystem

Interessant ist Listing 5: Hier werden zwei Klassen definiert, die in einer Vererbungsbeziehung stehen. Es wird dann eine Instanz der Subklasse Customer gebaut, aber als Person deklariert. Auf dieser Instanz wird die Methode buy() aufgerufen, die nur in der Subklasse Customer definiert ist. Eine Sprache wie Java würde ein solches Konstrukt nicht übersetzen, weil in der Klasse Personkeine Methode buy() definiert ist, obwohl das konkrete Objekt diese Methode enthält. Dart übersetzt das Programm und führt es auch aus. Es gibt allerdings eine Warnung wegen des Typfehlers. Dart hat nämlich einen sehr interessanten Ansatz bezüglich Typen: Typfehlern sind keine Compiler-Fehler. Das statische Typ-System überprüft den Code zwar, aber steht einer Kompilierung und Ausführung nicht im Wege. Die grundlegende Design-Entscheidung bei Dart ist, dass Typen nur Annotationen für Variablen sind. Sie ändern nie die Semantik des Programms.

An dieser Stelle lohnt es sich, ein paar Worte zu Typ-Systemen zu verlieren. Ein Typ definiert eine Menge von Werten und Operationen. Zum Beispiel könnten die ganzen Zahlen in bestimmten Grenzen und Operationen wie Addition, Subtraktion und Multiplikation ein Typ sein. Oder alle Instanzen einer bestimmten Klasse mit ihren Methoden als Operationen. Durch ein Typ-System wird überprüft, ob das Programm Typfehler hat – also eine Ganzzahl mit einem String multipliziert oder eine Methode aufruft, die in der Klasse des Objekts nicht definiert ist. Es gibt dynamische Typ-Systeme, die solche Fehler zur Laufzeit finden. Sie machen Entwicklern das Leben recht leicht, weil sie einfach zu handhaben sind und einige interessante Tricks erlauben. So können Sprachen wie Groovy oder Ruby zur Laufzeit Methodenaufrufe auf nicht existierende Methoden abfangen und ad-Hoc implementieren. Dadurch ist es möglich, bei einem Aufruf vonfindCustomerByName eine Anfrage an die Datenbank zu stellen und die passenden Daten zurückzugeben, ohne diese spezielle Methode implementiert zu haben. Der Code fängt einfach alle Aufrufe auf Methoden, die mit find beginnen, ab und führt die richtige Logik aus.

Statische Typ-Systeme nehmen die Typprüfung bei der Kompilierung vor. Die dazu notwendigen Typen in allen Fällen richtig zu deklarieren, kann schwierig werden. Der Vorteil ist, dass Typfehler schon bei der Kompilierung gefunden werden, was von vielen als sicherer empfunden wird. Aber Typfehler sind meistens offensichtliche und grundlegende Probleme. Unit-Tests würden solche Fehler ebenfalls finden. Statische Typen haben noch andere Vorteile: So kann eine Entwicklungsumgebung mehr Unterstützung bieten, wenn sie die Typen der Variablen kennt und dadurch beispielsweise die aufrufbaren Methoden zur Auswahl anbieten kann. Ebenfalls ist statische Typisierung für Dokumentation nützlich: Die Schnittstelle macht klar, ob eine Methode eine Person oder einen Customerzurückgibt.

Diese Argumentation lässt das Dart-Typsystem sehr interessant erscheinen: Es gibt einem zwar Hinweise, ob der Code korrekt ist, und der Code kann durch Typen anderen Entwicklern oder der Entwicklungsumgebung zusätzliche Informationen an die Hand geben. Gleichzeitig muss der Entwickler sich nicht damit beschäftigen, wie er in komplexen Fällen das Programm statisch typsicher macht, wenn er das nicht möchte, und er kann auch die Flexibilität dynamischer Typsysteme nutzen.

class Person {}
class Customer extends Person {
buy() {print("bought");}
}
main() {
Person p = new Customer();
p.buy();
}

Dart unterstützt auch Generics. Dabei ist eine List<Customer> ein Subtyp von List<Person>. Das erscheint intuitive zunächst richtig und logisch. Listing 6 zeigt ein Problem mit diesem Ansatz: Der Code fügt in die List<Person> eine Person ein. Gleichzeitig nutzt er sie als List<Customer>. Wenn aus dieser List<Customer> ein Customer ausgelesen werden soll, gibt es ein Problem, weil eben nur eine Person enthalten ist.

Übrigens tritt in Listing 6 der Fehler erst beim Aufruf der Methode buy() auf. Das Auslesen aus der Liste wird trotz des falschen Typen noch ausgeführt. Der Dart-Compiler kann optional aber auch zur Laufzeit die Typen überprüfen. Dann wird der Fehler schon beim Auslesen aus der Liste erzeugt. Natürlich wird durch die Typüberprüfung die Ausführung der Anwendung verlangsamt, so dass sie wohl in Produktion kaum genutzt wird.

Dart hat bei Generics also eine logische Inkonsistenz. Die Alternative wäre ein ausgefeiltes Typ-System ohne solche Inkonsistenzen. Java hat sich daran versucht. Das Ergebnis ist in Angelika Langers Generics FAQ sehr gut beschrieben – aber das Ergebnis ist so komplex, dass dieses Dokument 297 Seiten stark ist und kaum ein Java-Entwickler Generics wirklich verstanden hat. Dart hat sich für eine zwar inkonsistente Lösung entschieden, die aber für jeden Entwickler verständlich ist. Das ist eine Schlussfolgerung aus dem Ziel, die Sprache für möglichst viele Entwickler attraktiv zu machen. Die Inkonsistenz ist bei Dart eher akzeptabel, da das Typsystem sowieso nur Warnungen ausgibt. Selbst im besten Fall kann aber ein Typsystem nie die vollständige Korrektheit des Programms zeigen, da es nur auf Typen basiert. Es ist sogar theoretisch bewiesen, dass ein vollständig automatisierter Korrektheitsbeweis über ein Programm unmöglich ist.

main() {
List<Customer> listOfCustomer = new List<Customer>();
List<Person> listOfPerson = listOfCustomer;
listOfPerson.add(new Person());
Customer c = listOfCustomer[0];
c.buy();
}

[ header = Seite 3: Nebenläufigkeit mit Isolates ]

Nebenläufigkeit mit Isolates

Ein weiteres wichtiges Thema ist Nebenläufigkeit. Klassische objekt-orientierte Systeme synchronisieren den Zugriff auf Objekte durch mehrere Threads. Die Objekte haben einen Zustand, der von den verschiedenen Threads geschrieben und gelesen wird. Die Threads koordinieren sich dann so, dass ein Thread nicht den Zustand liest, während ein anderer ihn schreibt.

Dieser Ansatz führt zu einigen Problemen:

  • Es ist in der Praxis schwer, mit diesen Mitteln korrekte Programme zu implementieren. Deadlocks oder Inkonsistenzen in den Daten, weil die Zugriffe nicht richtig koordiniert worden sind, sind keine Seltenheit. Die notwendigen Konzepte und Bibliotheken sind vielfach nicht bekannt oder werden nicht genutzt.
  • Selbst bei einer korrekten Implementierung kann es zu Problemen kommen, wenn mehrere Threads dauernd dasselbe Objekt ändern müssen. Die notwendige Koordinierung verlangsamt die Ausführung. Außerdem können Threads durch den parallelen Zugriff dauernd blockiert sein, wenn sie auf den exklusiven Zugriff auf ein bestimmtes Objekt ständig warten müssen. Dann nimmt die Gesamtperformance ab. Solche Probleme treten häufig erst in Produktion auf, wo die Last entsprechend hoch ist und durch Mehrkernprozessoren auch wesentlich mehr parallel abgearbeitet wird.

Dart löst dieses Problem, da der Code immer nur in einem Thread ausgeführt wird. Das hört sich zunächst reichlich naiv an, da Parallelität in Anwendungen durch die wachsende Anzahl von Prozessor-Kernen ständig wichtiger wird. Aber Dart hat mit Isolates ein anderes Parallelitäts-Konzept. Isolates sind inspiriert durch das Aktoren-Modell, wie es in der Sprache Erlang oder in Scala mit Akka implementiert ist. Das bedeutet folgendes:

  • Innerhalb eines Isolates ist immer nur ein Thread aktiv.
  • Das Isolate kann Nachrichten über einen sogenannten Port annehmen.
  • Die Nachrichten werden in einer Warteschlange abgelegt und dann einzeln durch den Thread abgearbeitet.

Dadurch wird der Zustand in einem Isolate eingekapselt (siehe Abb. 1) und so der parallele Zugriff auf Zustand verhindert. Gleichzeitig kann die Anwendung weiterhin parallel arbeiten, da mehrere Isolates parallel aktiv sein können und so verschiedene Aufgaben parallel abgearbeitet werden können.

Abb. 1: Ein Isolate in Dart

class Printer extends Isolate {
main() {
port.receive((String message, replyTo) {
if (message == null) port.close();
else print(message);
});
}
}

class Echo extends Isolate {
main() {
port.receive((String message, replyTo) {
if (message == null) port.close();
else replyTo.send(message);
});
}
}

main() {
var printerPort;
new Printer().spawn().then((port) {
printerPort=port;
});
new Echo().spawn().then((port) {
for (var message in ['some', 'message', null]) {
port.send(message, printerPort);
}
});
}

Listing 7 zeigt ein Beispiel: Das Isolate Printer nimmt am Port Nachrichten an und gibt sie direkt auf dem Bildschirm aus. Das Isolate Echo schickt die Nachrichten direkt an den Reply-To-Port. Er ist in der Nachricht als der Port festgelegt, zu dem Antworten auf die Nachricht geschickt werden sollen. Das Hauptprogramm erzeugt ein Printer-Isolate und übergibt ihm eine Funktion, die den Port des Printer-Isolates in einer Variablen speichert. Dann wird ein Echo Isolate aufgebaut, dem Nachrichten geschickt werden. In diesen Nachrichten wird als Reply-To-Port der Port des Printer-Isolates übergeben. Diese Nachrichten treffen also erst auf das Echo-Isolate, das sie direkt an den Reply-To-Port und damit den Printer weiterschickt. Dort werden sie dann ausgegeben.

Isolates sind ein recht interessantes Konzept. In der Dart VM wird dieses Konzept direkt unterstützt. Bei JavaScript ist zum Beispiel eine Umsetzung mit WebWorkern möglich, die klassischen Threads ähneln. Isolates teilen sich keinerlei Zustand und haben völlig getrennte Speicherbereiche. So müssen zwar die Nachrichten zwischen den Isolates kopiert werden, was aufwändig sein kann, aber die Dart VM kann jedes Isolate auch einzeln einer Garbage Collection unterziehen. Die anderen Isolates können während dessen ungestört weiterlaufen. So wird ein Stillstand der gesamten VM vermieden. Isolates sollen außerdem unterschiedliche Versionen von Libraries nutzen können. Ebenso könnte das Nachladen von Code in einem Isolate in Zukunft erlaubt werden.

Für Sicherheitsmechanismen wäre es möglich, die Ports mit entsprechenden Regeln zu versehen, um nur berechtigte Zugriffe zu erlauben. Ebenso wäre es denkbar, die Ports im Netzwerk zur Verfügung zu stellen und von anderen Rechnern aus Nachrichten zu schicken, so dass verteilte System transparent implementiert werden können. Werden Isolates so wie in Erlang genutzt, können sie aber nicht direkt auf Betriebssystem-Threads abgebildet werden, weil mehr Isolates benötigt werden als ein Betriebssystem an Threads unterstützt. Auch sollte das Umschalten zwischen verschiedenen Isolates billiger sein als zwischen Betriebssystem-Threads. Diese Problematiken sind interessante Herausforderungen für die Dart-VM-Implementierung.

Schließlich werden in Erlang Isolates durch Monitore ergänzt, die bei einem Problem in einem Isolate informiert werden. Der Monitor kann das überwachte Isolate dann neu starten. Dadurch implementiert Erlang hohe Verfügbarkeit einer Software-Lösung: Statt bei jedem Fehler auf die Fehlersituation zu reagieren, werden Teile des Programms neu gestartet. Dabei muss der Zustand des Isolates natürlich entsprechend gemanagt werden. Dieser Ansatz hat sich in der Praxis als sehr einfach für Entwickler und sehr zuverlässig bewährt. Er wäre eine interessante Erweiterung für Darts Isolates. Isolates haben aber keinen hohen Abstraktionsgrad. Es wäre daher nicht überraschend, wenn abstraktere und einfacher zu nutzende Konzepte später in Dart eingebaut werden.

Ebenfalls gibt es mit dem Dart Editor auch schon eine Entwicklungsumgebung für Dart, die auf Eclipse beruht. Und auch eine komplexe Beispielanwendung steht schon bereit, nämlich ein Newsreader, der ohne Änderungen auf Chrome und auf dem iPad funktioniert. Dafür ist eine GUI-Bibliothek entstanden, die sicher auch in Zukunft in Dart integriert wird.

Fazit

Dart ist im Moment ein Technology Preview und bei weitem noch nicht fertig. Features wie Reflection oder direkte Unterstützung im Web-Browser Chrome könnten sicher in Zukunft implementiert werden, sind aber noch nicht verfügbar. Wer sich für Dart interessiert, sollte sich die Website anschauen. Sie bietet auch die Möglichkeit für eigne Experimente. Hier ist der Source Code für Dart, den Dart Editor und einige Beispiele, den man aber selbst kompilieren muss. Diese Seite bietet ein für Mac OS X kompiliertes Dart.

Dart ist der bis jetzt erfolgversprechendste Versuch für eine neue Web-Programmiersprache. Für die zunehmende Menge an komplexen Client-Programmen ist Dart sicher sehr gut geeignet. Das Sprach-Design mit C-ähnliche Syntax und dem optionalen und einfachen Typ-System zielt klar darauf ab, die Sprache möglichst populär zu machen. Also löst Dart ein reales und wichtiges Problem und ist einfach nutzbar – zwei wesentliche Voraussetzungen für den Erfolg einer Sprache. Google hat die Ressourcen, um Dart weiterzuentwickeln, und mit den vielen eigenen Web-Anwendungen auch ein klares Interesse an dieser Sprache. Außerdem könnte Dart auch für Googles Android interessant werden, wenn Java aus rechtlichen Gründen dort keine Option mehr ist, und für Server-Anwendungen natürlich auch.

Geschrieben von
Eberhard Wolff
Eberhard Wolff
Eberhard Wolff ist Fellow bei innoQ und arbeitet seit mehr als fünfzehn Jahren als Architekt und Berater, oft an der Schnittstelle zwischen Business und Technologie. Er ist Autor zahlreicher Artikel und Bücher, u.a. zu Continuous Delivery und Microservices und trägt regelmäßig als Sprecher auf internationalen Konferenz vor. Sein technologischer Schwerpunkt sind moderne Architektur- und Entwicklungsansätze wie Cloud, Continuous Delivery, DevOps, Microservices und NoSQL.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.