File loading please wait...
Citation preview
Philip Ackermann
Professionell entwickeln mit JavaScript Design, Patterns, Praxistipps 2., aktualisierte und erweiterte Auflage 2018
Impressum Dieses E-Book ist ein Verlagsprodukt, an dem viele mitgewirkt haben, insbesondere: Lektorat Almut Poll
Fachgutachten Sebastian Springer, München
Korrektorat Petra Biedermann, Reken
Covergestaltung Barbara Thoben, Köln
Coverbild Fotolia: 60695408 © Tiberius Gracchus; 123RF Stockfoto: 10876961 © bowie15
Herstellung E-Book August Werner
Satz E-Book Typographie & Computer, Krefeld
Bibliografische Information der Deutschen Nationalbibliothek:
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. ISBN 978-3-8362-5689-6 2., aktualisierte und erweiterte Auflage 2018
© Rheinwerk Verlag GmbH, Bonn 2018
Liebe Leserin, lieber Leser, ehemals unterschätzt, genießt JavaScript nun schon seit Jahren den Ruf einer vielseitig einsetzbaren Programmiersprache; ob komplexe Businessapplikationen, anspruchsvolle Web-GUIs oder moderne IoT-Anwendungen: JavaScript hat seinen festen Platz in der professionellen Softwareentwicklung. Damit gehen hohe Ansprüche an Ihre Arbeit einher: Modularität und Wiederverwendbarkeit, Testabdeckung, ein professioneller Build-Prozess, Security und vieles mehr verlangen ProfiArbeitsweisen und wollen durch passende Werkzeuge unterstützt werden. Für Umsteiger kommt hinzu: JavaScript enthält einige Besonderheiten, mit denen sie sich auseinandersetzen müssen und auf die sie auch unterschiedliche Antworten finden. Deshalb haben wir ein Buch im Programm, das sich voll und ganz mit der professionellen Entwicklung mit JavaScript befasst. Philip Ackermann führt Sie gekonnt und umsichtig durch Sprachfeatures, Entwurfsmuster und Best Practices. Sowohl die funktionale als auch die objektorientierte Programmierung mit Java- Script werden ausführlich behandelt, ebenso alle wichtigen Aspekte des Entwicklungsprozesses: Dokumentation, Paketverwaltung, Verbesserung der Codequalität und der Einsatz von Continuous Integration Tools. Sie können das Buch von Anfang bis Ende durchlesen, genauso gut können Sie aber auch nach Themen stöbern, die Sie interessieren. Das Buch ist so aufgebaut, dass Sie sofort finden, was Sie suchen.
Wenn Sie Fragen, Kritik oder Verbesserungsvorschläge haben, schicken Sie mir gerne eine E-Mail. Ihr Feedback ist jederzeit willkommen. Viel Erfolg mit JavaScript wünscht Almut Poll
Lektorat Rheinwerk Computing [email protected]
www.rheinwerk-verlag.de Rheinwerk Verlag • Rheinwerkallee 4 • 53227 Bonn
Inhaltsverzeichnis Liebe Leser! Inhaltsverzeichnis
Geleitwort Vorwort Materialien zum Buch
1 Einführung 1.1 Einleitung 1.2 Entstehung und Historie 1.3 Einsatzgebiete von JavaScript 1.3.1 Clientseitige JavaScript-Webanwendungen 1.3.2 Serverseitige JavaScript-Anwendungen 1.3.3 Desktop-JavaScript-Anwendungen 1.3.4 Mobile JavaScript-Anwendungen 1.3.5 Embedded-Anwendungen 1.3.6 Popularität von JavaScript
1.4 Laufzeitumgebungen 1.4.1 V8 1.4.2 SpiderMonkey/TraceMonkey/JägerMonkey/OdinMonkey 1.4.3 JavaScriptCore 1.4.4 Chakra
1.4.5 Rhino 1.4.6 Nashorn 1.4.7 Dyn.js 1.4.8 Auswahl der richtigen Laufzeitumgebung 1.4.9 Interpreter und Just-in-time-Compiler
1.5 Entwicklungsumgebungen 1.5.1 IntelliJ WebStorm 1.5.2 Visual Studio Code 1.5.3 Aptana Studio 3 1.5.4 Sublime Text 2 1.5.5 NetBeans 1.5.6 JSFiddle, JSBin und Codepen 1.5.7 Fazit
1.6 Debugging-Tools 1.6.1 Das »console«-Objekt 1.6.2 Browser 1.6.3 Node.js Inspector 1.6.4 IDEs und Editoren
1.7 Einführung in die Sprache 1.7.1 Statische Typisierung vs. dynamische Typisierung 1.7.2 Datentypen und Werte 1.7.3 Variablen und Konstanten 1.7.4 Funktionen 1.7.5 Operatoren 1.7.6 Kontrollstrukturen und Schleifen 1.7.7 Fehlerbehandlung 1.7.8 Sonstiges Wissenswertes
1.8 Zusammenfassung und Ausblick
2 Funktionen und funktionale Aspekte 2.1 Die Besonderheiten von Funktionen in JavaScript 2.1.1 Funktionen als First-Class-Objekte 2.1.2 Funktionen haben einen Kontext 2.1.3 Funktionen definieren einen Sichtbarkeitsbereich 2.1.4 Alternativen zum Überladen von Methoden 2.1.5 Funktionen als Konstruktorfunktionen
2.2 Standardmethoden jeder Funktion 2.2.1 Objekte binden mit der Methode »bind()« 2.2.2 Funktionen aufrufen über die Methode »call()« 2.2.3 Funktionen aufrufen über die Methode »apply()«
2.3 Einführung in die funktionale Programmierung 2.3.1 Eigenschaften funktionaler Programmierung 2.3.2 Unterschied zur objektorientierten Programmierung 2.3.3 Unterschied zur imperativen Programmierung 2.3.4 Funktionale Programmiersprachen und JavaScript
2.4 Von der imperativen Programmierung zur funktionalen Programmierung 2.4.1 Iterieren mit der Methode »forEach()« 2.4.2 Werte abbilden mit der Methode »map()« 2.4.3 Werte filtern mit der Methode »filter()« 2.4.4 Einen Ergebniswert ermitteln mit der Methode »reduce()« 2.4.5 Kombination der verschiedenen Methoden
2.5 Funktionale Techniken und Entwurfsmuster 2.5.1 Komposition 2.5.2 Rekursion 2.5.3 Closures 2.5.4 Partielle Auswertung
2.5.5 Currying 2.5.6 Das IIFE-Entwurfsmuster 2.5.7 Das Callback-Entwurfsmuster 2.5.8 Self-defining Functions
2.6 Funktionale reaktive Programmierung 2.6.1 Einführung 2.6.2 ReactiveX und RxJS 2.6.3 Praxisbeispiel: Drag & Drop 2.6.4 Praxisbeispiel: Echtzeitdaten über Web-Sockets
2.7 Zusammenfassung und Ausblick
3 Objektorientierte Programmierung mit JavaScript 3.1 Objekte 3.1.1 Arten von Objekten 3.1.2 Objekte erstellen
3.2 Prototypen 3.3 Vererbung 3.3.1 Prototypische Vererbung 3.3.2 Pseudoklassische Vererbung 3.3.3 Vererbung mit Klassensyntax 3.3.4 Kopierende Vererbung 3.3.5 Mehrfachvererbung mit Mixins
3.4 Datenkapselung 3.4.1 Öffentliche Eigenschaften 3.4.2 Private Eigenschaften 3.4.3 Privilegierte öffentliche Methoden
3.4.4 Nichtprivilegierte öffentliche Methoden 3.4.5 Private Methoden
3.5 Emulieren von statischen Eigenschaften und statischen Methoden 3.6 Emulieren von Interfaces 3.6.1 Interfaces emulieren mit Attribute Checking 3.6.2 Interfaces emulieren mit Duck-Typing
3.7 Emulieren von Namespaces 3.8 Emulieren von Modulen 3.8.1 Das klassische Module-Entwurfsmuster 3.8.2 Das Revealing-Module-Entwurfsmuster 3.8.3 Importieren von Modulen 3.8.4 Module Augmentation 3.8.5 Asynchronous Module Definition (AMD) und CommonJS 3.8.6 Universal Module Definition (UMD)
3.9 Modulsyntax 3.9.1 Module exportieren 3.9.2 Module importieren
3.10 Zusammenfassung und Ausblick
4 ECMAScript 2015 und neuere Versionen 4.1 Einführung 4.2 Block-Scope und Konstanten 4.2.1 Block-Scope 4.2.2 Konstanten
4.3 Striktere Trennung zwischen Funktionen und Methoden
4.3.1 Arrow-Funktionen 4.3.2 Definition von Methoden
4.4 Flexiblerer Umgang mit Funktionsparametern 4.4.1 Beliebige Anzahl an Funktionsparametern 4.4.2 Abbilden von Arrays auf Funktionsparameter 4.4.3 Standardwerte für Funktionsparameter 4.4.4 Benannte Parameter
4.5 Mehrfachzuweisungen über Destructuring 4.5.1 Array-Destructuring 4.5.2 Objekt-Destructuring
4.6 Iteratoren und Generatoren 4.6.1 Iteratoren 4.6.2 Generatorfunktionen und Generatoren
4.7 Promises 4.8 Proxies 4.8.1 Proxies seit ES2015 4.8.2 Emulieren von Proxies in ES5 4.8.3 Anwendungsbeispiel: Proxy als Profiler 4.8.4 Anwendungsbeispiel: Proxy zur Validierung
4.9 Collections 4.9.1 Maps 4.9.2 Weak Maps 4.9.3 Sets 4.9.4 Weak Sets
4.10 Neue Methoden der Standardobjekte 4.10.1 Neue Methoden in »Object« 4.10.2 Neue Methoden in »String« 4.10.3 Neue Methoden in »Array«
4.10.4 Neue Methoden in »RegExp«, »Number« und »Math«
4.11 Sonstiges neue Features 4.11.1 Template-Strings 4.11.2 Symbole 4.11.3 »for-of«-Schleife
4.12 Zusammenfassung und Ausblick
5 Der Entwicklungsprozess 5.1 Einleitung 5.2 Node.js und NPM 5.2.1 NPM installieren 5.2.2 Node.js-Anwendungen installieren
5.3 Styleguides und Code Conventions 5.3.1 Einrückungen 5.3.2 Semikolons 5.3.3 Anführungszeichen bei Strings 5.3.4 Variablendeklaration 5.3.5 Namenskonventionen 5.3.6 Klammern
5.4 Codequalität 5.4.1 JSLint 5.4.2 JSHint 5.4.3 ESLint 5.4.4 JSBeautifier 5.4.5 Google Closure Linter 5.4.6 Fazit
5.5 Dokumentation
5.5.1 JSDoc 3 5.5.2 YUIDoc 5.5.3 ESDoc 5.5.4 Unterstützte Tags 5.5.5 Fazit
5.6 Konkatenation, Minification und Obfuscation 5.6.1 YUI Compressor 5.6.2 Google Closure Compiler 5.6.3 UglifyJS2 5.6.4 Fazit
5.7 Package Management und Module Bundling 5.7.1 Package Management mit NPM 5.7.2 Module Bundling mit Webpack 5.7.3 Fazit
5.8 Building 5.8.1 Grunt 5.8.2 Gulp JS 5.8.3 NPM Scripts nutzen 5.8.4 Fazit
5.9 Scaffolding 5.9.1 Yeoman 5.9.2 Starterkits
5.10 Zusammenfassung und Ausblick
6 JavaScript-Anwendungen testen 6.1 Testgetriebene Entwicklung 6.1.1 Grundlagen und Begriffsdefinition 6.1.2 Testgetriebene Entwicklung in JavaScript
6.1.3 QUnit 6.1.4 mocha 6.1.5 Jest 6.1.6 Weitere Frameworks 6.1.7 Integration in Build-Tools
6.2 Test-Doubles 6.2.1 Sinon.JS 6.2.2 Spies 6.2.3 Stubs 6.2.4 Mock-Objekte
6.3 Testabdeckung 6.3.1 Einführung 6.3.2 Blanket.js 6.3.3 Istanbul
6.4 DOM-Tests 6.5 Funktionstests 6.5.1 PhantomJS 6.5.2 CasperJS
6.6 Zusammenfassung und Ausblick
7 Fortgeschrittene Konzepte der objektorientierten Programmierung 7.1 SOLID 7.1.1 Single-Responsibility-Prinzip (SRP) 7.1.2 Open-Closed-Prinzip (OCP) 7.1.3 Liskovsches Substitutionsprinzip (LSP) 7.1.4 Interface-Segregation-Prinzip (ISP)
7.1.5 Dependency-Inversion-Prinzip (DIP)
7.2 Fluent APIs 7.2.1 Einführung 7.2.2 Synchrone Fluent APIs 7.2.3 Asynchrone Fluent APIs mit Callbacks 7.2.4 Asynchrone Fluent APIs mit Promises 7.2.5 Zugriff auf Original-API 7.2.6 Generische Fluent API Factory
7.3 Aspektorientierte Programmierung in JavaScript 7.3.1 Einführung 7.3.2 Begrifflichkeiten 7.3.3 AOP durch Methodenneudefinition 7.3.4 Die JavaScript-Bibliothek meld 7.3.5 AOP über Decorators 7.3.6 Die Bibliothek aspect.js
7.4 Zusammenfassung und Ausblick
8 Die Entwurfsmuster der Gang of Four 8.1 Einführung 8.2 Erzeugungsmuster 8.2.1 Objekte an einer zentralen Stelle erzeugen (Abstract Factory/Factory Method) 8.2.2 Nur ein Objekt von einem Typ erstellen (Singleton) 8.2.3 Erstellen von komplexen Objekten (Builder) 8.2.4 Ähnliche Objekte erstellen (Prototype)
8.3 Strukturmuster 8.3.1 Die Schnittstelle anpassen (Adapter) 8.3.2 Abstraktion und Implementierung entkoppeln (Bridge)
8.3.3 Objekte in Baumstrukturen anordnen (Composite) 8.3.4 Eigenschaften unter Objekten teilen (Flyweight) 8.3.5 Objekte mit zusätzlichen Funktionalitäten ausstatten (Decorator) 8.3.6 Einheitliche Schnittstelle für mehrere Schnittstellen (Facade) 8.3.7 Den Zugriff auf Objekte abfangen (Proxy)
8.4 Verhaltensmuster 8.4.1 Über Datenstrukturen iterieren (Iterator) 8.4.2 Den Zugriff auf Objekte beobachten (Observer) 8.4.3 Eine Vorlage für einen Algorithmus definieren (Template Method) 8.4.4 Funktionen als Parameter übergeben (Command) 8.4.5 Algorithmen als Funktionen beschreiben (Strategy) 8.4.6 Das Zusammenspiel mehrerer Objekte koordinieren (Mediator) 8.4.7 Den Zustand eines Objekts speichern (Memento) 8.4.8 Operationen auf Objekten von Objekten entkoppeln (Visitor) 8.4.9 Das Verhalten eines Objekts abhängig vom Zustand ändern (State) 8.4.10 Eine Repräsentation für die Grammatik einer Sprache definieren (Interpreter) 8.4.11 Anfragen nach Zuständigkeit bearbeiten (Chain of Responsibility)
8.5 Zusammenfassung und Ausblick
9 Architekturmuster und Konzepte moderner JavaScript-Webframeworks
9.1 Model View Controller 9.2 Model View Presenter 9.3 MVC und MVP in Webanwendungen 9.3.1 Klassische Webanwendungen 9.3.2 Moderne Webanwendungen
9.4 Model View ViewModel 9.4.1 MVVM am Beispiel von Knockout.js 9.4.2 Kombination von MVC und MVVM am Beispiel von AngularJS
9.5 Komponentenbasierte Architektur 9.5.1 Komponentenbasierte Architektur am Beispiel von Angular 9.5.2 Komponentenbasierte Architektur am Beispiel von React 9.5.3 Komponentenbasierte Architektur am Beispiel von Vue.js
9.6 Routing 9.7 Zusammenfassung und Ausblick
10 Messaging 10.1 Einführung 10.2 AMQP 10.2.1 Producer und Consumer 10.2.2 Exchanges
10.3 AMQP unter JavaScript 10.3.1 Installation eines Message-Brokers für AMQP 10.3.2 AMQP-Clients für JavaScript 10.3.3 Senden und Empfangen von Nachrichten 10.3.4 Verwenden von Exchanges
10.3.5 STOMP
10.4 MQTT 10.4.1 Publish-Subscribe und Topics 10.4.2 Wildcards 10.4.3 Quality of Service 10.4.4 Last Will and Testament 10.4.5 Retained Messages 10.4.6 Persistent Sessions
10.5 MQTT unter JavaScript 10.5.1 Installation eines Message-Brokers für MQTT 10.5.2 MQTT-Clients für JavaScript
10.6 Zusammenfassung und Ausblick
11 Continuous Integration 11.1 Vorbereitungen 11.1.1 Installation von Docker 11.1.2 Installation des Git-Servers Gogs 11.1.3 Anlegen eines Git-Repositorys 11.1.4 Hinzufügen eines SSH-Schlüssels 11.1.5 Anlegen des Beispielprojekts
11.2 Jenkins 11.2.1 Installation 11.2.2 Installieren von Plugins 11.2.3 Anlegen von Jobs 11.2.4 Jenkins mit Node.js steuern
11.3 Alternativen: Travis CI und CircleCI 11.4 Zusammenfassung und Ausblick
Stichwortverzeichnis Rechtliche Hinweise Über den Autor
Geleitwort JavaScript ist schon lange nicht mehr die viel belächelte Scriptsprache, die es vor vielen Jahren war. JavaScript ist mittlerweile da, wo Java initial hinwollte: auf nahezu jedem Endgerät. JavaScript ist nicht nur auf der Client- und Serverseite angekommen, sondern auch in den verschiedensten Branchen zu finden. Von den Medien und in Internetkonzernen, wo JavaScript naturgemäß allgegenwärtig ist, über Banken und Versicherungen bis hin zu großen Industrieunternehmen. Überall dort wird JavaScript in geschäftskritischen Prozessen eingesetzt. Das führt allerdings zu einem Problem: Während man in Sprachen wie Java oder C# meist eine solide Ausbildung der Entwickler in der jeweiligen Sprache und Architektur findet, ist dies in JavaScript nicht immer der Fall. Das liegt jedoch nicht daran, dass die Entwickler sich nicht mit dem Thema beschäftigen wollen, sondern daran, dass sich JavaScript gerade in den letzten Jahren erheblich weiterentwickelt hat und entsprechende Ressourcen zur Weiterbildung nicht einfach zu finden sind. Bei der Festlegung neuer Features hat sich das Standardisierungskonsortium von zahlreichen anderen Programmiersprachen und Frameworks inspirieren lassen, und so finden Sie als Umsteiger auf JavaScript immer wieder Muster, die Ihnen bekannt vorkommen. Aber natürlich auch Paradigmen, mit denen Sie bisher noch nicht in Berührung gekommen sind. In diesem Buch trägt Philip Ackermann seinen Teil dazu bei, Ihnen als Leser professionelle Entwicklung in JavaScript näherzubringen. Sowohl Einsteiger als auch Umsteiger erfahren viel zu den Hintergründen der Sprache JavaScript. Die gute Nachricht für alle, die mit JavaScript arbeiten wollen oder müssen, ist, dass sich Frontends wie auch Backends gut
strukturieren lassen, was zu wartbaren und langlebigen Applikationen führt. Die Arbeit an einer solchen Applikation macht erheblich mehr Spaß als mit einem gewachsenen und fehleranfälligen System. Damit wird es für einen Entwickler auch nicht mehr zur Strafarbeit, sich mit JavaScript zu beschäftigen. Doch wie gelangen Sie zu einer solchen Applikation, bei der es tatsächlich Spaß macht, Erweiterungen und Verbesserungen zu entwickeln? Der Schlüssel hierfür ist die Kenntnis der zugrundeliegenden Sprache und der möglichen Architekturansätze. Das Verständnis von modernem JavaScript führt allerdings zum Teil über die Entwicklungsgeschichte von JavaScript. Dieses Buch stellt genau diese Hintergründe vor, die Sie als Entwickler kennen müssen, um solide Schnittstellen zu entwickeln und die Vorteile der modernen Sprachfeatures von JavaScript nutzen zu können. In diesem Buch tauchen Sie sowohl in die klassische objektorientierte Entwicklung mit JavaScript in Form von Klassen, aber auch in die funktionale Programmierung ein. Sie erfahren, wie Sie mit der allgegenwärtigen Asynchronität umgehen können. Außerdem zeigt Ihnen Philip, wie Ihnen JavaScript die Arbeit an einer Applikation mit diversen Sprachfeatures erleichtert. Statt jedoch die eine richtige Lösung präsentiert zu bekommen, lernen Sie, das richtige Werkzeug für Ihr Problem im Dschungel der JavaScript-Welt zu finden und es richtig einzusetzen. Der Weg zu einem professionellen JavaScript-Entwickler führt allerdings nicht über das reine Lesen eines Buchs zum Ziel. Deshalb können Sie sich darauf freuen, im Zuge dieses Buchs zahlreiche Beispiele nachzuvollziehen, die beschriebenen Inhalte selbst auszuprobieren und so das Gelernte durch Anwendung weiter zu festigen. Auf Basis dieser Beispiele können Sie dann Ihre eigenen Experimente sowie kleine und große Applikationen aufbauen und Erfahrungen sammeln.
Ich wünsche Ihnen viel Spaß beim Durcharbeiten dieses Buchs und dass Sie viel für Ihre tägliche Arbeit mit JavaScript mitnehmen können. Sebastian Springer
Vorwort Die Sprache JavaScript musste sich lange Zeit mit vielen Vorurteilen herumschlagen. »Eine einfache Skript-Sprache«, »nicht für professionelle Anwendungen verwendbar«, »keine objektorientierte Programmierung möglich« und ähnlich lauteten die Aussagen vieler Entwickler. Doch dies hat sich geändert. In den letzten Jahren hat die Popularität der Sprache beachtlich zugenommen, und ihre Anwendungsgebiete sind um einiges vielfältiger geworden: Nicht nur auf Clientseite im Browser kommt die Sprache zum Einsatz, sondern auch auf Serverseite, in Desktop-Anwendungen, in mobilen Anwendungen oder gar im Bereich Embedded Systems. Aktuelle Stellenausschreibungen und Beliebtheitsumfragen spiegeln diesen Trend wider: kaum eine Statistik, in der sich nicht auch JavaScript auf den ersten Rängen positioniert. Als professioneller Softwareentwickler und Webentwickler kommt man also heutzutage in der Regel gar nicht mehr um JavaScript herum. Folglich ist das Erlernen der Sprache ist gut investierte Zeit. Doch trotz des Scheins, eine simple Sprache zu sein, hat JavaScript eine Vielzahl von Besonderheiten, die oft zu Missverständnissen führen und es Einsteigern in die Sprache nicht gerade leicht machen. Für wen ist dieses Buch?
Der Schwerpunkt des Buches liegt darauf, Ihnen zu zeigen, wie Sie mit JavaScript professionelle Softwareentwicklung betreiben. Das Buch richtet sich in erster Linie an Entwickler, die bereits
Programmiererfahrung in mindestens einer anderen Programmiersprache haben, sich schnell in JavaScript einarbeiten und sich nicht erst durch Einsteigerbücher, unzählige Blogeinträge und Tutorials durcharbeiten möchten. Ich selbst bin sowohl Webentwickler als auch Softwareentwickler mit Java/JEE-Hintergrund und habe die Sprache JavaScript in den letzten Jahren sozusagen aus einem anderen Blickwinkel wiederentdeckt. Ihnen möchte ich den Einstieg in JavaScript mit diesem Buch so effektiv wie möglich gestalten. Mein Ziel ist es, dass Sie nach dem Lesen des Buches einen guten Überblick über die professionelle Entwicklung mit JavaScript haben, dass Sie die Kernkonzepte der Sprache verstanden haben, Entwurfsmuster erkennen und anwenden können und wissen, welche Möglichkeiten Ihnen für einen professionellen Softwareentwicklungsprozess mit JavaScript zur Verfügung stehen. Sollten Sie bereits mein anderes JavaScript-Buch, »JavaScript – Das umfassende Handbuch«, gelesen haben, können Sie das erste Kapitel getrost überspringen. Auch in Kapitel 2 und Kapitel 3 gibt es bei den grundlegenden Themen ein paar wenige Überschneidungen, wobei der Fokus im vorliegenden Buch auf fortgeschrittenen funktionalen und objektorientierten Aspekten liegt (beispielsweise funktionale Entwurfsmuster, funktionale reaktive Programmierung, aspektorientierte Programmierung und viele mehr), die ich im JavaScript-Handbuch nicht bespreche. Wie ist das Buch aufgebaut?
Das Buch beginnt mit einer kurzen Einführung in die Sprache JavaScript, zeigt anschließend die funktionalen, objektorientierten sowie prototypischen Aspekte und geht auf die Features ein, die mit und seit dem relativ großen Feature-Update von ES2015 Einzug in
die Sprache gehalten haben. Die zweite Hälfte des Buches beschäftigt sich dann mit Themen wie dem Entwicklungsprozess von JavaScript-Anwendungen, Continuous Integration, dem Thema Testen sowie den Entwurfsmustern der Gang of Four, Architekturmustern und Konzepten moderner JavaScriptWebframeworks sowie dem Thema Messaging. Mir persönlich geht es beim Lesen von Programmierbüchern häufig so, dass lange Codebeispiele das Verständnis des Kerns der Sache oft unnötig verkomplizieren. Aus diesem Grund habe ich die Codebeispiele in diesem Buch bewusst kurz gehalten. Im Allgemeinen ist es meiner Meinung nach nämlich so, dass sich – ein entsprechend didaktischer Aufbau vorausgesetzt – die meisten Aspekte recht einfach, übersichtlich und anhand kurzer Codebeispiele ebenso gut verdeutlichen lassen. Die Codebeispiele können Sie von www.rheinwerk-verlag.de/4457 herunterladen. Alternativ dazu steht der Quelltext auch in einem GitHubRepository unter https://github.com/cleancoderocker/javascriptprofibuch zur Verfügung. Außerdem habe ich weitestgehend auf den Einsatz von Fremdbibliotheken verzichtet. Viele Bücher über JavaScript verwenden ein eigenes, vom Autor zugrundegelegtes Framework oder zeigen Beispiele anhand von Bibliotheken wie jQuery. Mein Ziel dagegen war es, den Code frei von solchen Abhängigkeiten zu halten und Ihnen dadurch nicht den Blick auf das Wesentliche zu verschleiern. Wie sollte ich das Buch durchlesen?
Des besseren Verständnisses wegen empfiehlt es sich, das Buch dem Aufbau entsprechend von vorn nach hinten durchzuarbeiten.
Soweit dies möglich ist, sind die einzelnen Kapitel so aufgebaut, dass so wenig wie möglich auf spätere Kapitel vorgegriffen wird. Trotzdem lässt es sich nicht immer ganz vermeiden, auf bestimmte Aspekte vorzugreifen, um ein Thema zu erklären. In diesen Fällen habe ich zumindest einen Querverweis auf das entsprechende Thema eingefügt. Das Buch verfolgt somit didaktisch einen gewissen roten Faden, lässt sich aber, je nach Kenntnisstand des Lesers, ebenso gut als Nachschlagewerk nutzen. Danksagung
Am allermeisten möchte ich meiner Frau und meinen Kindern danken, für ihre Geduld und Unterstützung während der Zeit, die ich an diesem Buch gearbeitet habe. Außerdem möchte ich mich bei meiner Lektorin Almut Poll für ihre konstruktiven Vorschläge und ihre Unterstützung sowie bei Anne Scheibe, Annette Lennartz und dem gesamten beteiligten Team im Rheinwerk Verlag bedanken. Auch Sebastian Springer und Christoph Höller gilt mein Dank, für das wertvolle Fachgutachten und die vielen nützlichen Hinweise. Mein Dank gilt auch dem Heise-Verlag, mit dessen freundlicher Genehmigung ich Auszüge aus Artikeln, die ich für die iX geschrieben habe, für das Buch wiederverwenden durfte (dies betrifft Teile aus Kapitel 3, die Abschnitte über PhantomJS und CasperJS in Kapitel 6 sowie die Beispiele zu den Command- und Strategy-Entwurfsmustern in Kapitel 8). Besonders bedanken möchte ich mich hierfür bei Julia Schmidt. Auch bei Max Bold von der Neuen Mediengesellschaft Ulm mbH bedanke ich mich, mit dessen freundlicher Genehmigung ich
Auszüge aus Artikeln verwenden durfte, die ich für das Fachmagazin web & mobile Developer verfasst habe. Zu guter Letzt danke ich Ihnen, nicht nur für den Kauf dieses Buches, sondern für die Zeit, die Sie mit dem Lesen und Durcharbeiten verbringen. Ich hoffe, Sie haben viel Vergnügen dabei. Außerdem würde ich mich sehr über Ihr Feedback freuen und stehe Ihnen unter mailto:[email protected] auch gerne für Fragen und Anregungen zur Verfügung. Unter http://javascriptprofibuch.de finden Sie zudem weitere Informationen und Updates zum Buch. Philip Ackermann
Materialien zum Buch Auf der Webseite zu diesem Buch stehen folgende Materialien für Sie zum Download bereit: alle Beispielprogramme Gehen Sie auf www.rheinwerk-verlag.de/4457. Klicken Sie im Abschnitt Materialien zum Buch auf den Link Zu den Materialien >. Es öffnet sich ein Fenster, in dem Sie die herunterladbaren Dateien samt einer Kurzbeschreibung des Dateiinhalts sehen. Klicken Sie auf den Button Herunterladen, um den Download zu starten. Je nach Größe der Datei (und Ihrer Internetverbindung) kann es einige Zeit dauern, bis der Download abgeschlossen ist.
1 Einführung JavaScript ist mittlerweile eine der populärsten Programmiersprachen. Egal, ob Web, Mobile oder Desktop, Client- oder Serverseite oder gar Embedded-Anwendungen – es gibt keinen Bereich, in dem nicht auch JavaScript mitmischt. Dabei galt die Sprache doch lange Zeit als eher simple Skriptsprache, mit der nicht wirklich ernsthafte Softwareentwicklung möglich ist. Bevor wir uns ab Kapitel 2, »Funktionen und funktionale Aspekte«, den fortgeschrittenen Themen zuwenden, gebe ich Ihnen in diesem Kapitel zunächst einen kurzen Überblick über die Sprache JavaScript. Ich beginne mit der Entstehung und einer kurzen Übersicht über die Einsatzbereiche der Sprache, erkläre den Zusammenhang zwischen ECMAScript und JavaScript und in welchen wesentlichen Aspekten sich JavaScript von Sprachen wie Java und C# abgrenzt. Außerdem stelle ich Ihnen die bekanntesten Laufzeitumgebungen, Entwicklungsumgebungen und DebuggingTools vor. Den Abschluss des Kapitels bildet eine Einführung in die wichtigsten Sprachmittel. Da ich vermute, dass Sie bereits über ein gewisses Maß an Programmiererfahrung verfügen, ist diese Einführung relativ kurz gehalten und auf das Wesentliche reduziert. Trotzdem sollten Sie anschließend über genug Wissen verfügen, um auf die folgenden vertiefenden Themen vorbereitet zu sein.
1.1 Einleitung
Lange Zeit hatte JavaScript den Ruf, eine simple Skriptsprache zu sein, die nicht zu mehr diente, als Webseiten mit dynamischen Effekten »aufzuhübschen« und hier und da ein bisschen mehr Interaktivität in eine Webseite zu bringen. Ernsthaft programmieren – so dachten viele – könne man mit dieser Sprache nicht. In den vergangenen Jahren jedoch hat sich diese Einstellung gewandelt. Mittlerweile übernimmt JavaScript zum einen auf Clientseite deutlich komplexere Aufgaben, zum anderen kommt JavaScript immer häufiger auch serverseitig zum Einsatz. Zudem ist JavaScript viel dynamischer als andere Sprachen, beispielsweise bei der Typisierung. Dies hat Vorteile, aber auch Nachteile. Dinge wie fehlende Typsicherheit führen einerseits dazu, dass Sie als Entwickler viele Freiheiten haben, andererseits aber auch dazu, dass Sie während der Entwicklung viel stärker darauf achten müssen, »sauber« zu programmieren. Bei Java oder anderen compilerbasierten Sprachen bekommt man beispielsweise schon durch die Spracharchitektur und den Compiler sehr viel mehr Hilfestellungen, was in JavaScript nicht der Fall ist. Doch all diese Besonderheiten, wechselnden Anforderungen und neuen Einsatzgebiete verlangen von den Entwicklern auch (sowohl von Webentwicklern als auch von solchen, die bisher nur am Rande mit JavaScript zu tun hatten), sich eingehender mit JavaScript zu beschäftigen. Allerdings sollte man hierbei den Aufwand nicht unterschätzen. Die Details von JavaScript sind nicht immer einfach zu verstehen, und die Sprache richtig einzusetzen lernt man auch nicht von heute auf morgen. Dennoch scheinen viele Entwickler der Meinung zu sein, allein die Tatsache, dass sie bereits eine Sprache wie C++, C# oder Java professionell beherrschen, mache sie
automatisch zu professionellen JavaScript-Entwicklern. Dem ist nicht so! Ich muss allerdings zugeben, dass ich selbst zu Anfang so dachte. Als ich mit der Webentwicklung anfing, war JavaScript für mich zunächst nichts anderes als eine einfache Skriptsprache. In späteren Projekten, in denen neuere Techniken, wie beispielsweise Ajax (Asynchronous JavaScript and XML), verwendet wurden, gewann JavaScript zwar etwas mehr an Bedeutung, entwickelte aber nie den Stellenwert, den es eigentlich verdient hätte. Erst als ich vor etwa acht Jahren an einem Projekt beteiligt war, in dem wir im Rahmen eines größeren Refactorings die Entscheidung trafen, ein Produkt nahezu vollständig (das heißt sowohl auf Clientseite als auch auf Serverseite) von Java/JEE zu JavaScript/Node.js zu migrieren, war ich mehr oder weniger gezwungen, mich intensiver mit der Sprache zu beschäftigen, und lernte dabei die Tücken, aber auch die Stärken von JavaScript erst richtig kennen und einzusetzen.
1.2 Entstehung und Historie JavaScript wurde 1995 innerhalb kürzester Zeit (nämlich in etwa 12 Tagen) von Brendan Eich für den Netscape Navigator entwickelt, damals noch unter den Namen Mocha bzw. LiveScript. Den jetzigen Namen JavaScript trägt die Sprache erst seit dem Jahr 1996. Zu verdanken ist diese finale Namensänderung einer Kooperation zwischen Netscape und Sun, der Firma, die hinter der Programmiersprache Java steckt(e), wobei man sich bei der Namenswahl augenscheinlich die damalige Popularität von Java zunutze machen wollte. Trotz des verwandten Namens hat JavaScript jedoch nicht viel mit Java zu tun. Vielmehr gelten die beiden Programmiersprachen Scheme und Self als Vorbilder. Von Ersterer finden sich in JavaScript beispielsweise funktionale Konzepte wie Closures, von Letzterer Konzepte wie die (objektbasierte) prototypische Objektorientierung. Gerade durch diese beiden Konzepte unterscheidet sich JavaScript wesentlich von Java, bei dem zum einen klassenbasierte Objektorientierung zum Einsatz kommt und zum anderen bis vor Version 8 keine funktionalen Features zu finden waren. Im Detail werde ich in Kapitel 2, »Funktionen und funktionale Aspekte«, auf die funktionalen und in Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, auf die prototypischen Aspekte von JavaScript eingehen. Merke JavaScript und Java haben bis auf einen ähnlichen Namen und eine teilweise ähnliche Syntax eher wenige Gemeinsamkeiten. Als Vorbilder von JavaScript gelten die beiden Sprachen Self und Scheme. Wesentliche Konzepte von JavaScript sind prototypische Objektorientierung und funktionale Programmierung.
Kurze Zeit nachdem JavaScript erschienen war, implementierte Microsoft eine mehr oder weniger kompatible Sprache für den Internet Explorer 3.0, mit leicht abgewandeltem Namen: JScript. Um diese beiden ähnlichen
Sprachen unter einen Hut zu bringen, wurde JavaScript daraufhin von Netscape bei der ECMA, der European Computer Manufacturers Association, eingereicht, mit dem Ziel, einen einheitlichen Standard für die Sprache zu schaffen. Dieser Standard läuft seitdem unter dem Namen ECMAScript, der 2015 in Version 6 (kurz ES6 bzw. ES2015, siehe Hinweiskasten unten) verabschiedet wurde (https://www.ecmainternational.org/publications/standards/Ecma-262.htm). Kurz nach dieser Version einigte man sich darauf, statt alle paar Jahre fortan jährlich eine neue Version herauszubringen (auch wenn diese nur wenige Änderungen und Neuerungen enthalten sollte). Folglich tragen die Versionen seit 2015 auch die Bezeichnungen ES2015, ES2016, ES2017 usw. Hinweis Aus diesem Grund findet man im Internet und der Literatur verschiedene Bezeichnungen für die ECMAScript-Version, die 2015 in Version 6 erschienen war: ECMAScript 6, ES6 oder ES2015. In diesem Buch verwende ich den Begriff ES2015.
JavaScript ist demnach »nur« eine Implementierung dieses Standards (weitere sind beispielsweise QtScript, das aus Flash bekannte ActionScript sowie das in vielen Adobe-Produkten verwendete ExtendScript). Version
Erscheinungsjahr Wesentliche Änderungen
ECMAScript 1997 1
erste Version
ECMAScript 1998 2
redaktionelle Überarbeitungen
ECMAScript 1999 3
reguläre Ausdrücke Fehlerbehandlung über try-catch
Version
Erscheinungsjahr Wesentliche Änderungen
ECMAScript – 4
Version wurde nie veröffentlicht.
ECMAScript 2009 5
strikter Modus JSON-Support
ECMAScript 2011 5.1
redaktionelle Überarbeitungen.
ECMAScript 2015 6 (ES2015)
Klassen Module Generatoren let und const
ECMAScript 2016 7 (ES2016)
neue Methode includes() für Arrays Exponentialoperator
ECMAScript 2017 8 (ES2017)
async/await
ECMAScript 2018 9 (ES2018)
asynchrone Iteration Rest/Spread Properties Promise.prototype.finally() verschiedene Neuerungen bezüglich regulärer Ausdrücke
Tabelle 1.1 Übersicht über die ECMAScript-Versionen
1.3 Einsatzgebiete von JavaScript Im Gegensatz zu früher, als JavaScript hauptsächlich im Browser zum Einsatz kam, sind die Einsatzgebiete heute vielfältiger: Neben clientseitigen Webanwendungen kommt JavaScript nun auch auf Serverseite, in Desktop-Anwendungen oder in mobilen Anwendungen zum Einsatz. 1.3.1 Clientseitige JavaScript-Webanwendungen
Lange Zeit wurde JavaScript vor allem dazu genutzt, das User Interface einer Webseite durch dynamische Effekte »aufzuhübschen«. DHTML (Dynamisches HTML), das eine Manipulation des DOMs (Document Object Model) eines HTMLDokuments bezeichnet, ist sicherlich dem ein oder anderen Entwickler noch ein Begriff. Ein wichtiger Meilenstein für JavaScript und Grundlage für komplexere Webanwendungen, wie wir sie heutzutage kennen, war die Einführung des XMLHttpRequest-Objekts. Über dieses Objekt war es erstmals möglich, asynchrone Anfragen an den Server zu schicken (Ajax) und dabei sowohl Daten zu speichern als auch Daten zu laden. Dies war zugleich der Startschuss für Webanwendungen, die sich von der Nutzung her mehr nach Desktop-Anwendungen »anfühlen«, auch als Rich Internet Applications (RIAs) bezeichnet. Oft ist eine solche Anwendung als sogenannte Single-Page Application (SPA) aufgebaut, das heißt, die Logik ist nicht wie bei klassischen Webanwendungen über mehrere Webseiten verteilt, sondern spielt sich innerhalb einer einzelnen Webseite ab. Inhalte
werden dann je nach Nutzeraktion dynamisch generiert oder nachgeladen, die Webseite entsprechend dynamisch aktualisiert. Zum Austausch von Inhalten zwischen Client und Server kommt dabei oft das JSON-Austauschformat (JavaScript Object Notation) zum Einsatz, das mittlerweile sogar XML als Standardaustauschformat zwischen Anwendungen ernsthafte Konkurrenz macht. Vorteil: JSON kann direkt von JavaScript verarbeitet und genutzt werden. Idealerweise werden die Daten dann auch noch im gleichen Format in einer entsprechenden dokumentbasierten Datenbank wie MongoDB (http://www.mongodb.org) gespeichert. 1.3.2 Serverseitige JavaScript-Anwendungen
Vorreiter und bekanntestes Beispiel für serverseitige JavaScriptAnwendungen ist Node.js, eine auf V8 (siehe Abschnitt 1.4.1) basierende Plattform, die es ermöglicht, serverseitige Aufgaben mit JavaScript umzusetzen. Node.js stellt beispielsweise Packages bereit, über die sich ein kompletter Webserver umsetzen lässt. Über zusätzliche Module lassen sich zudem relativ einfach beispielsweise REST-basierte (Representational State Transfer) Webservices implementieren, Datenbankzugriffe vereinfachen oder mehrsprachige Anwendungen entwickeln. Node.js ist aber nicht nur als Webserver geeignet: Prinzipiell lassen sich mit Node.js alle Arten von kommandozeilenbasierten Anwendungen erstellen. Node.js kann als eine Art Unix-Shell angesehen werden, die statt Shell-Skripten eben JavaScript interpretiert und ausführt. Über den Node Package Manager (NPM) beispielsweise, den ich Ihnen in Abschnitt 5.6.1, »YUI Compressor«, vorstellen werde, lassen sich Programmmodule bequem (auch für
Nicht-JavaScript-Anwendungen) über die Kommandozeile installieren. Node.js gilt aufgrund seiner Architektur als höchst skalierbar, äußerst performant sowie echtzeitfähig. Es ist daher mittlerweile keine Seltenheit mehr, dass in einem Projekt alle Komponenten einer Client-Server-Anwendung in JavaScript programmiert werden. Vorteil davon: Ein Entwickler mit JavaScript-Kenntnissen kann innerhalb eines solchen Projekts sowohl clientseitige als auch serverseitige Komponenten entwickeln. 1.3.3 Desktop-JavaScript-Anwendungen
In Kombination mit HTML5 und CSS3 kommt JavaScript mittlerweile nicht mehr nur im Browser zum Einsatz, sondern auch in Desktop-Anwendungen. So ist es beispielsweise unter Windows 8 möglich, native Anwendungen komplett in den genannten Technologien zu erstellen. Das Framework AppJS (http://appjs.com) verspricht sogar die betriebssystemunabhängige Entwicklung von Desktop-Anwendungen für Linux, Windows und Mac. Ebenfalls einen Blick Wert sind die beiden Frameworks Electron (https://electronjs.org/) und NW.js (https://nwjs.io/). 1.3.4 Mobile JavaScript-Anwendungen
Mobile Anwendungen werden häufig nicht nativ programmiert (also beispielsweise mittels Java im Fall von Android-Anwendungen bzw. Objective-C im Fall von iOS-Anwendungen), sondern ebenfalls basierend auf HTML5, CSS3 und JavaScript. Frameworks wie Apache Cordova (http://cordova.apache.org) oder das darauf basierende PhoneGap (http://phonegap.com) stellen Dienste des mobilen Endgeräts über (JavaScript-)Web-APIs zur Verfügung und
ermöglichen es zudem, die mobile Webanwendung in die AppStores der jeweiligen Betriebssystemhersteller hochzuladen. Mit Hilfe von NativeScript (https://www.nativescript.org/) lassen sich mobile iOS- und Android-Anwendungen unter Verwendung von Angular (https://angular.io/) implementieren, React Native (http://facebook.github.io/react-native/) ermöglicht das Gleiche unter Verwendung von React (https://reactjs.org/). 1.3.5 Embedded-Anwendungen
Auch im Bereich der Embedded-Anwendungen hält JavaScript Einzug. Beispiele hierfür sind die beiden Mikrocontroller Tessel (https://tessel.io) und Espruino (http://www.espruino.com), auf denen nativ bereits JavaScript zur Verfügung steht. Des Weiteren existieren bereits verschiedene JavaScript-Bibliotheken zum Thema IoT (Internet of Things) wie beispielsweise die Bibliothek johnny-five (https://github.com/rwaldron/johnny-five), über die man unter anderem einen Arduino (http://www.arduino.cc) steuern kann. Prinzipiell tut sich in diesem Bereich derzeit recht viel, da man auch erkannt hat, dass JavaScript eine attraktive Sprache ist. 1.3.6 Popularität von JavaScript
Mittlerweile ist JavaScript eine der am weitesten verbreiteten Programmiersprachen. Einige interessante Statistiken dazu bietet die Seite http://jxcore.com/business-case-for-javascript-and-node-jsjxcore/. Dieser zufolge liegt beispielsweise die Anzahl der zur Verfügung stehenden Module für die Plattform Node.js (dazu gleich mehr) derzeit auf Platz 3. Bei den Job-Angeboten liegen JavaScriptsowie Node.js-Kenntnisse momentan auf Platz 1. Auch bezüglich der neuen Projekte auf Github (http://redmonk.com/sogrady/2017/06/08/language-rankings-6-17/)
ist JavaScript mittlerweile auf dem ersten Platz. Im TIOBE-Index (http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html) rangiert JavaScript im Moment (Dezember 2017) auf Platz 6 und hat damit im Vergleich zum Vorjahr um zwei Plätze aufgeholt. Populärer sind laut TIOBE nur die Großen: Java, C, C++, Python und C#. Auch auf dem Freelancer-Portal GULP (https://www.gulp.de/projektmarktindex) befindet sich JavaScript derzeit (Dezember 2017) unter den Top Drei: auf Platz 3 hinter Java und SQL.
1.4 Laufzeitumgebungen Um JavaScript-Programme ausführen zu können, benötigen Sie zunächst eine Laufzeitumgebung. Davon existieren für JavaScript gleich mehrere: Zum einen verwendet jeder der bekannteren Browserhersteller eine eigene, zum anderen gibt es weitere Laufzeitumgebungen, die es ermöglichen, JavaScript außerhalb des Browsers (das heißt »headless«) auszuführen. 1.4.1 V8
Die von Google entwickelte, in C++ geschriebene V8-Engine kommt sowohl in Google Chrome als auch in Node.js zum Einsatz. V8 ist betriebssystemunabhängig und läuft auf Windows XP und neuer, auf macOS (10.5 und neuer) und verschiedenen Linux-Systemen. Laut verschiedenen Benchmarks ist V8 schneller als andere Laufzeitumgebungen (https://developers.google.com/v8), wobei die hohe Geschwindigkeit vor allem durch folgende drei Designprinzipien erreicht wird: durch schnellen Zugriff auf Objekteigenschaften, dynamische Generierung von Maschinencode (Just-in-time-Kompilierung oder kurz JIT, dazu später mehr) und effiziente Garbage Collection (Details siehe unter https://developers.google.com/v8/design). Für .NET-Entwickler besonders interessant: V8 kann über JavaScript .NET (http://javascriptdotnet.codeplex.com) in entsprechende Anwendungen integriert werden. Auf diese Weise kann JavaScript direkt aus .NET heraus aufgerufen werden. 1.4.2 SpiderMonkey/TraceMonkey/JägerMonkey/OdinMonkey
SpiderMonkey ist eine in C implementierte JavaScriptLaufzeitumgebung, die Brendan Eich ursprünglich für den Netscape Navigator entwickelte, die inzwischen aber von der Mozilla Foundation weiterentwickelt wird. Verwendet wird sie hauptsächlich in verschiedenen Mozilla-Produkten wie Firefox und Thunderbird. Im Laufe der Jahre wurde SpiderMonkey immer weiterentwickelt und durch Module ergänzt, unter anderem bzw. vor allem mit dem Ziel, die Performance zu verbessern. Diese Erweiterungen und Updates sind unter den folgenden Namen bekannt: TraceMonkey, das die Laufzeitumgebung unter anderem um Justin-time-Kompilierung erweiterte JägerMonkey, das weitere Optimierungen der Just-in-timeKompilierung brachte OdinMonkey, das die Laufzeitumgebung für die JavaScriptErweiterung asm.js (http://asmjs.org) anpasste, mit deren Hilfe es möglich ist, JavaScript-Programme zu erstellen, die bezüglich der Ausführungsgeschwindigkeit mit Java- oder C#-Programmen vergleichbar sind 1.4.3 JavaScriptCore
Das von Apple entwickelte JavaScriptCore kommt in verschiedenen macOS-Anwendungen zum Einsatz, beispielsweise im SafariBrowser. Über eine C-API und eine darauf basierende Objective-CAPI ist es möglich, aus entsprechendem Code JavaScript aufzurufen und auf diese Weise JavaScript in native macOS-Anwendungen zu integrieren.
1.4.4 Chakra
Chakra ist eine von Microsoft entwickelte JavaScriptLaufzeitumgebung, die vom Microsoft Edge Browser verwendet wird und als Fork der gleichnamigen JScript-Laufzeitumgebung entstand, die beim Microsoft Internet Explorer zum Einsatz kommt. Hinsichtlich der Performance wurde diese Version von Chakra (unter anderem dank JIT-Kompilierung) stark verbessert. Anfang 2016 wurde die Engine unter einer Open-Source-Lizenz veröffentlicht (https://github.com/Microsoft/ChakraCore). 1.4.5 Rhino
Rhino ist eine komplett in Java geschriebene JavaScriptLaufzeitumgebung, mit deren Hilfe JavaScript innerhalb der JVM (Java Virtual Machine) ausgeführt werden kann. Rhino wurde in den letzten Jahren von Sun bzw. Oracle etwas stiefmütterlich behandelt und ist mittlerweile in die Jahre gekommen, wird aber trotzdem noch in einigen Projekten verwendet. Ein prominentes Beispiel ist Vert.x (http://vertx.io), eine Plattform für die Entwicklung moderner Webanwendungen, die sich stark an Node.js orientiert und es ebenfalls ermöglicht, neben Sprachen wie Java, Groovy und Ruby (bzw. JRuby) auch JavaScript direkt auszuführen. 1.4.6 Nashorn
Nashorn ist ebenfalls eine in Java geschriebene JavaScript-Engine, seit Java 8 im JDK (Java Development Kit) enthalten. Sie gilt als Nachfolger von Rhino. JavaScript-Code wird von Nashorn in JavaBytecode kompiliert und auf der JVM ausgeführt. Insbesondere was die Ausführungsgeschwindigkeit angeht, ist Nashorn dabei dank
der mit Java 7 eingeführten Invoke-Dynamic-Anweisung deutlich schneller als Rhino. Prinzipiell lässt sich Nashorn auf zwei verschiedene Weisen nutzen: Entweder Sie starten es ähnlich wie Node.js von der Kommandozeile aus (über das im JDK enthaltene Tool jjs), oder Sie binden es über entsprechende Klassen direkt in ein Java-Programm ein. Dabei ist es unter anderem möglich, sowohl aus Java heraus JavaScript aufzurufen als auch umgekehrt aus JavaScript Java-Code aufzurufen. 1.4.7 Dyn.js
Bei Dyn.js handelt es sich um eine weitere JavaScriptLaufzeitumgebung für die JVM, die vor allem »aus der Not« heraus entstand: Die Tatsache, dass Rhino lange Zeit nicht wirklich weiterentwickelt wurde und keine neuen Java-Features wie InvokeDynamic verwendete, führte in der Community zu der Reaktion, kurzerhand eine eigene Laufzeitumgebung zu entwickeln, die genau diese neuen Features nutzt und einen Geschwindigkeitsvorteil gegenüber Rhino verspricht. 1.4.8 Auswahl der richtigen Laufzeitumgebung
Welche Laufzeitumgebung Sie auswählen, hängt vom jeweiligen Projekt ab. Handelt es sich um eine Webanwendung, bei der JavaScript ausschließlich auf der Clientseite eingesetzt wird, kommen als Ziel-Laufzeitentwicklungen die der jeweiligen Browserhersteller in Frage. Möchten Sie dabei verschiedene Browser unterstützen, bietet die Website unter http://pointedears.de/scripts/test/es-matrix/ einen guten Überblick über den Support einzelner JavaScript-Versionen und Features. Bezogen auf die relativ neuen Features seit ES2015 ist dagegen die
Website unter http://kangax.github.io/compat-table/es6/ ein guter Anlaufpunkt, denn hier unterstützen bei weitem (noch) nicht alle Browser die gleichen neuen Features (wobei sich dank sogenannter Polyfill-Bibliotheken auch viele Features für ältere Browser aktivieren lassen). Bezüglich Web-APIs bietet zusätzlich die Website https://caniuse.com/ einen guten Überblick, ob und ab welcher Version eine bestimmte API in welchem Browser unterstützt wird. Wenn Sie mit dem Gedanken spielen, JavaScript auf der Serverseite einzusetzen, ist Node.js (das wiederum die V8-Laufzeitumgebung verwendet) oder alternativ Vert.x (Rhino) die richtige Wahl, wobei Ersteres sicherlich populärer ist, Letzteres aber auch die Integration anderer auf der JVM laufender Sprachen ermöglicht. Wollen Sie dagegen JavaScript nur als Teilkomponente einer (bestehenden) Java- oder JEE-Anwendung integrieren, lohnt sich ein Blick auf Nashorn oder Dyn.js. Für die Integration in eine .NET-Anwendung bietet sich JavaScript .NET an, das ebenfalls V8 als Laufzeitumgebung verwendet. 1.4.9 Interpreter und Just-in-time-Compiler
Im Gegensatz zu Sprachen wie C++, bei denen der Quelltext vor Ausführung eines Programms in Maschinencode kompiliert wird, handelt es sich bei JavaScript um eine Sprache, bei der der Quelltext direkt zur Laufzeit von einem Interpreter ausgewertet wird. Der Vorteil von kompilierten Programmen ist, dass sie in der Regel schneller sind als Programme, die erst noch zur Laufzeit ausgewertet werden müssen. Ein Vorteil von interpretierten Programmen ist dagegen, dass sie sich (sofern eine entsprechende Laufzeitumgebung installiert ist) auf jedem Betriebssystem ohne Kompilierungsschritt ausführen lassen und somit betriebssystemunabhängig sind. Kompilierte Programme dagegen
sind lediglich auf dem Zielsystem lauffähig, für das sie kompiliert wurden. Um ein solches Programm für mehrere Betriebssysteme lauffähig zu machen, muss zunächst für jedes eine entsprechende Version kompiliert werden. JavaScript ist also eine interpretierte Sprache und somit erst einmal prinzipiell »langsamer« als kompilierte Sprachen. Um diesem Verlust entgegenzuwirken und die Ausführungsgeschwindigkeit dennoch zu steigern, arbeiten viele Laufzeitumgebungen daher mit der sogenannten Just-in-time-Kompilierung (JIT-Kompilierung). Das Prinzip ist dabei, ein Programm oder (häufig ausgeführte) Teile eines Programms zur Laufzeit in Maschinencode zu übersetzen. Solcher Maschinencode kann dann gegenüber reinem Quelltext, der erst von einem Interpreter ausgewertet werden muss, viel schneller ausgeführt werden.
1.5 Entwicklungsumgebungen Wenn Sie wirklich professionell und effektiv in einer Sprache entwickeln möchten, kommen Sie früher oder später nicht darum herum, leistungsstarke Editoren oder Entwicklungsumgebungen (Integrated Development Environments, IDEs) zu verwenden. Im Gegensatz zu C, C++ oder Java, in denen vermehrt IDEs zum Einsatz kommen, sind unter JavaScript- und Webentwicklern häufig auch Editoren sehr beliebt, die vor allem schlank und oft schneller in der Handhabung sind als die teilweise mit Menüs und Funktionalitäten überladenen IDEs. 1.5.1 IntelliJ WebStorm
Das auf IntelliJ IDEA (https://www.jetbrains.com/idea/) basierende WebStorm (http://www.jetbrains.com/webstorm, siehe Abbildung 1.1) war lange Zeit mein absoluter Favorit für die Entwicklung von JavaScript-Anwendungen. Das Tool ist zwar nicht kostenlos, sondern kostet im Abo im ersten Jahr 129 € (danach wird es etwas günstiger), aber dafür bietet es doch schon einiges. Neben den selbstverständlichen Standardfeatures wie SyntaxHighlighting und einer Codevervollständigung, die auch für eingebundene Module und Bibliotheken funktionieren, bietet Webstorm Support für die Entwicklung von Node.js-Anwendungen, automatische Code-Refactorings und viele weitere nette Features, die bei der Entwicklung sehr praktisch sind, wie etwa die Navigation per (Ctrl)- bzw. (Strg)- plus Maustaste von Funktionsaufruf zu Funktionsdefinition.
WebStorm unterstützt die Erstellung von Projekten unter anderem basierend auf HTML5 Boilerplate, Bootstrap, Node.js und Angular.
Abbildung 1.1 Screenshot der WebStorm-IDE
1.5.2 Visual Studio Code
Wenn man an WebStorm über etwas meckern kann, dann ist es die Performance, die insbesondere bei vielen geöffneten Projekten nicht mehr ganz so überzeugt. Hier sind die schlankeren Editoren deutlich schneller. Besonders gut gefällt mit diesbezüglich Visual Studio Code (https://code.visualstudio.com/) von Microsoft und gehört mittlerweile (neben WebStorm) zu den Mitteln meiner Wahl. Es ist in den meisten Fällen wie gesagt deutlich schneller als WebStorm, bietet ebenfalls Debugging-Funktionalität, CodeHighlighting, Codevervollständigung und vieles, vieles mehr.
Abbildung 1.2 Screenshot von Visual Studio Code
1.5.3 Aptana Studio 3
Aptana Studio 3 (http://www.aptana.com) basiert auf der Eclipse-IDE (https://eclipse.org), erweitert diese aber um eine Reihe von Funktionalitäten, die man im Rahmen der Webentwicklung benötigt. Dazu zählen Support für die Entwicklung mit HTML, CSS, JavaScript, PHP, Ruby on Rails, Syntax-Highlighting, Codevervollständigung, Kommandozeilenzugriff über ein integriertes Terminal, Integration mit Git, Debugging durch Integration mit FireBug und vieles mehr (siehe Abbildung 1.3).
Abbildung 1.3 Screenshot der Aptana-IDE
Aptana macht seinen Job dabei schon wesentlich besser als die ebenfalls für Eclipse verfügbaren JSDT (JavaScript Development Tools), von denen ich Ihnen persönlich zum jetzigen Zeitpunkt eigentlich nur abraten kann. Zu unvollständig und unzuverlässig sind versprochene Features wie Codevervollständigung und Fehlerbehebung, so dass die Entwicklung mit JSDT weder wirklich Spaß macht noch sonderlich effektiv ist. 1.5.4 Sublime Text 2
Der Editor Sublime Text 2 (http://www.sublimetext.com) ist unter Webentwicklern äußerst beliebt (siehe Abbildung 1.4). Im Gegensatz
zu so mancher IDE ist Sublime Text 2 sehr schnell und verfügt dennoch über eine Fülle an Features wie Syntax-Highlighting für diverse Sprachen, Codevervollständigung etc. Der Editor bietet sogar das ein oder andere Feature, das selbst schwergewichtige IDEs nicht mitbringen, beispielsweise den flexiblen Einsatz mehrerer Cursors, um so auf einfache Weise direkt mehrere Codestellen zu editieren (Multi-Edit). Des Weiteren stehen jede Menge Plugins zur Verfügung, über die sich der Editor nach eigenen Wünschen anpassen lässt, etwa das Plugin Emmet, das aus CSS-Selektoren das entsprechende HTML erzeugt, mehrere Plugins für die Integration von Git-Repositories, für die Integration von CSS-Präprozessoren wie SASS und vieles mehr.
Abbildung 1.4 Screenshot des Editors Sublime Text 2
Derzeit wird an einer Version 3 des Editors gearbeitet, die als BetaVersion auf der Hersteller-Site zur Verfügung steht (http://www.sublimetext.com/3). 1.5.5 NetBeans
NetBeans ist eine Open-Source-Entwicklungsumgebung und wurde von Oracle vor allem für die Entwicklung von Java-Anwendungen konzipiert, lässt sich aber auch für andere Sprachen wie C, C++ oder mittlerweile auch für JavaScript verwenden und mit diversen Plugins weiter aufrüsten (siehe Abbildung 1.5). In den letzten Versions-Releases (beginnend mit Version 7.3) sind immer mehr Funktionalitäten für die Entwicklung von Rich Web Applications und Mobile Applications hinzugekommen mit Fokus auf den Webtechnologien wie HTML5, CSS3 und eben auch JavaScript. Der integrierte JavaScript-Editor unterstützt dabei SyntaxHighlighting, Codevervollständigung etc. Zudem hilft NetBeans bei der Erstellung von Projekten, basierend auf populären Frameworks wie Bootstrap, Boilerplate und AngularJS. Dank eines eingebetteten, auf WebKit basierenden Browsers ist es außerdem möglich, Anwendungen direkt aus der IDE heraus zu debuggen. Alternativ lässt sich auch Chrome als externer Browser einbinden. Auch das Ausführen von Unit-Tests für JavaScript-Code (siehe auch Kapitel 5, »Der Entwicklungsprozess«) ist über Testrunner wie JSTestDriver oder Karma direkt aus der IDE heraus möglich. Als Versionsverwaltungssystem können außerdem Git, Subversion, Mercurial sowie CVS eingebunden werden.
Abbildung 1.5 Screenshot der NetBeans-IDE
1.5.6 JSFiddle, JSBin und Codepen
Eine interessante Option, die Sie in Erwägung ziehen können, wenn Sie schnell ein JavaScript-Feature implementieren, testen und eventuell mit anderen Entwicklern teilen wollen, sind Onlinedienste wie JSFiddle (http://jsfiddle.net, Abbildung 1.6), JSBin (http://jsbin.com) oder Codepen (http://codepen.io).
Abbildung 1.6 Screenshot des Online-Editors JSFiddle
Bei diesen Onlinetools stehen dem Entwickler im Browserfenster meist vier Bereiche zur Verfügung: jeweils einer für den HTML-, CSSund JavaScript-Code sowie einer für das gerenderte Ergebnis. Besonders praktisch: Die Onlinedienste ermöglichen über Dropdown-Menüs das Einbinden bekannter JavaScript-Bibliotheken – ideal also, um auf die Schnelle eine bestimmte Konstellation verschiedener Bibliotheken zu testen. 1.5.7 Fazit
Welche IDE oder welchen Editor Sie für die JavaScript-Entwicklung verwenden, ist letztendlich Geschmackssache. Als Java-Entwickler bin ich natürlich ein bisschen Eclipse-geprägt und habe lange Zeit auch hiermit JavaScript entwickelt. Allerdings muss man einfach sagen, dass Eclipse bezüglich der JavaScript-Entwicklung schlicht noch nicht ausgereift ist. Persönlich finde ich wie gesagt die WebStorm-IDE (bzw. Intellij IDEA mit entsprechenden Plugins) sowie den Editor Visual Studio Code am komfortabelsten.
NetBeans ist als Alternative ebenfalls interessant, insbesondere wenn Sie JavaScript als Bestandteil eines Java-/JEE-Projekts verwenden. Für das schnelle Testen von Funktionalitäten oder das kollaborative Entwickeln und Teilen von Quelltextbeispielen eignen sich dagegen die Onlinedienste JSFiddle, JSBin oder Codepen. Der schnelle Editor Sublime Text 2 erfordert etwas Einarbeitungszeit, wenn Sie ihn optimal nutzen möchten, belohnt dann aber mit vielen Features. Neben den genannten Tools gibt es noch weitere, beispielsweise Coda (https://panic.com/coda), TextMate (http://macromates.com) oder Espresso (http://macrabbit.com/espresso).
1.6 Debugging-Tools So wie nahezu jeder Browserhersteller eine eigene JavaScriptLaufzeitumgebung verwendet, gibt es auch für jeden Browser ein eigenes Debugging-Tool. Sollten Sie dagegen eine serverseitige JavaScript-Anwendung debuggen wollen, bietet sich unter Node.js für ältere Versionen das Modul node-inspector an, für Node.jsVersionen seit 6.3 der integrierte Node.js Inspector. Alternativ dazu bieten natürlich wie im vorigen Abschnitt erwähnt viele der Entwicklungsumgebungen und Editoren Debugging-Tools an. Bevor wir uns jedoch diesen Debugging-Tools zuwenden, möchte ich Ihnen kurz noch einen Weg vorstellen, über den Sie simple Ausgaben auf die Konsole schreiben können – in den meisten Fällen zwar nicht die beste Art zu debuggen, aber trotzdem oft sehr hilfreich. Die Rede ist von dem Objekt console, das sowohl in den Laufzeitumgebungen der verschiedenen Browser als auch unter Node.js zur Verfügung steht (unter Google Chrome öffnen Sie die Entwicklerkonsole beispielsweise über Anzeigen • Entwickler • Entwicklertools bzw. Anzeigen • Entwickler • JavaScript-Konsole). 1.6.1 Das »console«-Objekt
Beim console-Objekt handelt es sich um ein Objekt, das erstmals durch das Firefox-Plugin Firebug eingeführt wurde und mit dem es möglich ist, auf die Standardausgabe zu schreiben. Mittlerweile steht das console-Objekt (obwohl immer noch nicht im Standard enthalten) in nahezu jeder JavaScript-Laufzeitumgebung zur Verfügung.
Die einzelnen Methoden, die das Objekt zur Verfügung stellt, unterscheiden sich jedoch von Laufzeitumgebung zu Laufzeitumgebung. Um dem entgegenzuwirken, gibt es daher bereits Bestrebungen, die API zu standardisieren. Immer unterstützt wird aber die Methode log(), mit der Sie eine einfache Konsolenausgabe erzeugen: console.log('Hallo Welt');
Tabelle 1.2 gibt eine Übersicht über die Methoden, die von jeder Laufzeitumgebung unterstützt werden, die das console-Objekt bereitstellt. Methode Beschreibung clear() Leert die Konsole. debug() Erwartet ein oder mehrere Objekte und gibt diese auf der Konsole aus. error() Erwartet wie debug() ein oder mehrere Objekte und gibt diese als Fehler auf der Konsole aus. In Browsern wird innerhalb der Konsole oft ein Fehler-Icon neben der ausgegebenen Meldung dargestellt sowie der Fehler-Stack ausgegeben. info()
Hiermit werden die übergebenen Objekte als InfoMeldung auf die Konsole ausgegeben. Chrome beispielsweise gibt zusätzlich ein Info-Icon mit aus.
log()
Die wohl am häufigsten verwendete Methode von console. Loggt die übergebenen Objekte auf die Konsole.
Methode Beschreibung trace() Gibt den Stack-Trace, also den Methodenaufruf-Stack, auf die Konsole aus. warn()
Gibt die übergebenen Objekte als Warnung auf die Konsole aus. Auch hier wird in den meisten Browsern ein entsprechendes Icon neben der Meldung ausgegeben.
Tabelle 1.2 Standardmethoden des »console«-Objekts
Ein besonderes nettes Feature der log()-Methode ist die Möglichkeit, innerhalb des übergebenen Strings mit Platzhaltern zu arbeiten. Enthält ein String solche Platzhalter, werden die nachfolgenden Parameter bei der Ausgabe als Werte für die Platzhalter eingesetzt. Beispielsweise erzeugt der Aufruf console.log('%s: %d', 'Ergebnis', 2.4);
die Ausgabe Ergebnis: 2.4. Auf diese Weise lassen sich ebenfalls komplette Objekte in die Meldung einbauen, nämlich einfach, indem Sie den Platzhalter %j verwenden. Folgendes Programm erzeugt beispielsweise die Ausgabe {"name":"Max","nachname":"Mustermann"}: const person = {
firstName: 'Max',
lastName: 'Mustermann'
}
console.log('%j', person);
Allerdings funktioniert die Ausgabe von Objekten nur unter Node.js. Eine Methode, die eine ähnliche Ausgabe erzeugt, ist die Methode dir().
Hinzu kommen weitere Methoden für die formatierte Ausgabe, wie beispielsweise dirxml(), die XML- oder HTML-(Unter-)Bäume auf die Konsole ausgibt, group(), groupCollapsed() und groupEnd(), die Konsolenausgaben gruppieren, und table(), die Daten in Tabellenform ausgibt. Aber wie gesagt: Nicht alle Laufzeitumgebungen bieten alle Methoden an. Eine Übersicht, welche Methoden zur Verfügung stehen, finden Sie auf den Dokumentationsseiten der jeweiligen Laufzeitumgebung: für Chrome (https://developer.chrome. com/devtools/docs/console-api), Firefox (https://developer.mozilla.org/en-US/docs/Web/API/console), Node.js (http://nodejs.org/api/). Einen guten Überblick gibt außerdem Axel Rauschmayer in seinem Blog unter http://www.2ality.com/2013/10/console-api.html. 1.6.2 Browser
In jedem der bekannten Browser gibt es mittlerweile ein DebuggingTool, wobei sich die einzelnen Tools voneinander nur wenig unterscheiden. In jedem der Tools ist es beispielsweise möglich, verschiedene Arten von Breakpoints zu setzen (in dem einen Tool mehr, in dem anderen weniger), den Methoden-Stack zu begutachten, die aktuelle Variablenbelegung einzusehen, Variablen zu verändern, schrittweise im Programm weiterzugehen etc. Firefox Developer Tools
Welches Tool Sie verwenden, hängt also eher davon ab, für welchen Browser Sie entwickeln. Eines der wohl bekannteren DebuggingTools ist der Debugger der Firefox Developer Tools (bis 2017 unter dem Namen Firebug entwickelt). Damit können verschiedene
Aspekte bei der Webentwicklung überprüft werden, so auch die Ausführung von JavaScript-Code (siehe Abbildung 1.7). Der Debugger (https://getfirebug.com), erlaubt Ihnen einzelne Breakpoints zu setzen und ausgehend davon schrittweise im JavaScript-Code zu navigieren. Einem Breakpoint können Sie dabei eine boolesche Bedingung zuweisen (Conditional Breakpoints), die angibt, in welchen Fällen an dem jeweiligen Breakpoint angehalten werden soll. Alternativ können Sie pauschal den Debugger immer dann pausieren lassen, wenn ein Fehler auftritt. Zu den weiteren Features zählen Zugriff auf den Stack-Trace, Watch Expressions (über die es möglich ist, bestimmte Ausdrücke zu beobachten) sowie diverse Profiling-Optionen. Ebenfalls praktisch: Über die eingebaute Konsole lässt sich JavaScript direkt im Kontext der jeweiligen Anwendung ausführen.
Abbildung 1.7 Firefox Firebug
Chrome Developer Tools
Die Chrome Developer Tools (CDT) (https://developers.google.com/chrome-developertools/docs/javascript-debugging) zählen unter JavaScriptEntwicklern wohl zu den beliebtesten Debugging-Tools (siehe Abbildung 1.8). Ähnlich wie in Firebug lassen sich auch unter CDT Breakpoints setzen. Neben Breakpoints, die Exceptions abfangen, gibt es dort aber zusätzlich die Möglichkeit, DOM-Breakpoints oder
XHR-Breakpoints zu setzen, wodurch das jeweilige Programm bei DOM-Manipulationen oder bei Aufrufen über das XHR-Objekt zum Halten kommt. Mittlerweile ist es sogar möglich, den Quelltext eines Projekts mit CDT zu verknüpfen, so dass er sich direkt aus dem Tool heraus ändern lässt.
Abbildung 1.8 Chrome Developer Tools
Opera Dragonfly
Dragonfly ist der JavaScript-Debugger, der im Opera-Browser zum Einsatz kommt (http://www.opera.com/dragonfly/documentation/debugger). Zu den verschiedenen Breakpoint-Arten zählen normale LineBreakpoints, die beim Erreichen einer Codezeile ausgelöst werden, Event-Breakpoints, die bei bestimmten Ereignissen ausgelöst werden, sowie Conditional Breakpoints, die ausgelöst werden, falls eine definierte boolesche Bedingung erfüllt ist. Safari Web Inspector
Der Apple-Browser Safari enthält den sogenannten Web Inspector (https://developer. apple.com/safari/tools). Im Wesentlichen bietet auch dieses Tool ähnliche Features wie die oben genannten.
Microsoft Edge Developer Tools
Auch Microsoft Edge stellt mit den Microsoft Edge Developer Tools (https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide) ein entsprechendes Entwicklerwerkzeug zur Verfügung. Features sind auch hier unter anderem das Debugging von Anwendungen, Netzwerkanalyse, Messen der Performance, Speicheranalyse und einiges mehr. 1.6.3 Node.js Inspector
Seit Version 6.3 bietet Node.js standardmäßig ein auf den Chrome Developer Tools basierendes Tool für das Debuggen an. Um es zu aktivieren, hängen Sie beim Aufruf der entsprechenden Node.jsAnwendung einfach den Parameter --inspect an, was intern dafür sorgt, dass die Anwendung im Debug-Modus gestartet wird. Geben Sie nun im Chrome-Browser den Befehl »chrome://inspect« in die Adressleiste ein, haben Sie Zugriff auf die Node.js-Anwendung und können (wie für Webanwendungen bereits üblich) die Chrome Developer Tools für Node.js-Anwendungen verwenden. Für weitere Details empfehle ich Ihnen die offizielle Dokumentation des Node.js Inspectors unter https://nodejs.org/en/docs/guides/debugging-getting-started/ und den Artikel »Debugging Node.js with Chrome DevTools« von Paul Irish unter https:// medium.com/@paul_irish/debugging-node-jsnightlies-with-chrome-devtools-7c4a1b95ae27. 1.6.4 IDEs und Editoren
Die verschiedenen IDEs und Editoren, die ich Ihnen in diesem Kapitel vorgestellt habe, bieten zum großen Teil ebenfalls Debugging-Funktionalität an. In WebStorm und Visual Studio Code
lassen sich relativ einfach entsprechende Konfigurationen anlegen, so dass es ohne weiteres möglich ist, mehrere Anwendungen parallel zu »debuggen«.
1.7 Einführung in die Sprache In diesem Abschnitt gebe ich Ihnen eine kurze Einführung in die Sprachmittel von JavaScript. Da ich annehme, dass Sie bereits Erfahrung in mindestens einer anderen Programmiersprache haben, gehe ich hierbei nur auf das Wichtigste ein. Allgemeine Themen wie Kontrollstrukturen, Schleifen etc. behandle ich dementsprechend kurz. Das Ziel dieser Einführung ist es ohnehin, Ihnen die wichtigsten Sprachgrundlagen zu vermitteln und aufzuzeigen, worin sich JavaScript von anderen Sprachen unterscheidet. 1.7.1 Statische Typisierung vs. dynamische Typisierung
Ein erster wichtiger Aspekt, in dem sich JavaScript von vielen anderen Sprachen unterscheidet, ist die Typisierung. Sprachen wie Java und C# verwenden eine statische Typisierung, das heißt, Variablen, Parameter und Objekteigenschaften haben jeweils einen festen Typ, der zur Compile-Zeit bereits bekannt ist. Die Typinformation kann vom Compiler dazu verwendet werden, Typüberprüfungen durchzuführen und somit typbedingte Fehler, die sonst erst zur Laufzeit auftreten würden, bereits zur CompileZeit zu erkennen. Auch statisch typisierte Sprachen haben dynamische Typen, etwa wenn in Java (zur Compile-Zeit) als Typ eines Methodenparameters ein Interface verwendet wird und zur Laufzeit beim Methodenaufruf eine (konkrete) Klasse übergeben wird. Das Interface ist in dem Fall der statische (zur Compile-Zeit bekannte) Typ, die Klasse der dynamische (zur Laufzeit ermittelte) Typ.
JavaScript hingegen ist vollständig dynamisch typisiert, das heißt, alle Typen werden dynamisch zur Laufzeit ermittelt. In JavaScript ist es demnach erst gar nicht möglich, für eine Variable einen Typ anzugeben. All das wiederum bedeutet im Umkehrschluss jedoch nicht, dass es in JavaScript überhaupt keine Typen gibt. Im Vergleich zu anderen Sprachen hält sich die Anzahl verschiedener Typen nur in Grenzen und lässt sich (fast) an einer Hand abzählen. Betrachten wir im Folgenden die verschiedenen Datentypen im Detail. 1.7.2 Datentypen und Werte
Insgesamt gibt es in JavaScript sechs verschiedene Typen. Dies sind zum einen die primitiven Datentypen String, Number und Boolean, zum anderen die speziellen Typen null und undefined sowie der Typ Object. Letzterer umfasst neben eigenen erstellten Objekten auch Arrays, reguläre Ausdrücke sowie Funktionen (zu letzteren mehr in Abschnitt 1.7.4, »Funktionen«). Ermitteln können Sie den Typ einer Variablen übrigens mit dem typeof-Operator. Mögliche Rückgabewerte hierbei sind string, number, boolean, object, function und undefined. Sie sehen: null ist zwar ein eigener Typ, existiert aber nicht als Rückgabewert: typeof null liefert den Typ object. Zahlen
Im Gegensatz zu Sprachen wie C und Java unterscheidet JavaScript bei Zahlen nicht zwischen Ganzzahlen und Fließkommazahlen. Alle Zahlen werden als 64-Bit-Fließkommazahlen dargestellt. Dabei können Sie die Dezimalschreibweise (ohne Präfix), die Hexadezimalschreibweise (mit Präfix 0x) und die Oktalschreibweise
(mit Präfix 0) verwenden. Eine Binärschreibweise, wie es sie mittlerweile beispielsweise in Java gibt, bietet JavaScript nicht an. const aNumber = 5;
const aFloat = 5.4;
console.log(typeof aNumber); console.log(typeof aFloat);
// number
// number
Listing 1.1 JavaScript unterscheidet nicht zwischen Ganzzahlen und Fließkommazahlen.
Hinweis Bezeichner können Sie in JavaScript auf verschiedene Weisen definieren: Es stehen die Schlüsselwörter var, let und const zur Verfügung. Worin genau der Unterschied liegt und welches der Schlüsselwörter Sie in welchen Fällen verwenden sollten, erkläre ich Ihnen später in Abschnitt 1.7.3, »Variablen und Konstanten«, und außerdem in Kapitel 4, »ECMAScript 2015 und neuere Versionen«. Nur so viel vorab: Mit var und let erstellen Sie Variablen, mit const – Sie können es sich denken – Konstanten.
Liegt ein Wert außerhalb des Wertebereichs, wird Infinity als Wert verwendet. Neben diesem Wert gibt es den Wert NaN (not a number), ein Stellvertreter für alles, was keinem Zahlenwert entspricht. Für das Rechnen mit Zahlen stehen wie gewohnt die Basisoperatoren für Addition, Subtraktion, Multiplikation und Division zur Verfügung. Komplexere Operationen stellt das MathObjekt bereit. Hinweis Obwohl JavaScript bezüglich des Typs keine Unterscheidung zwischen Ganzzahlen und Fließkommazahlen macht, gibt es zwei
verschiedene globale Funktionen, die aus einem String eine Zahl parsen: parseInt() für die Umwandlung in eine Ganzzahl (d.h. ohne Berücksichtigung eventueller Nachkommastellen), parseFloat() für die Umwandlung in eine Fließkommazahl (d.h. mit Berücksichtigung eventueller Nachkommastellen).
Zeichenketten
Zeichenketten bzw. Strings sind in JavaScript 16-Bit-Zeichen nach der UCS-2-Kodierung, nicht etwa wie in Java nach UTF-16, und können in JavaScript ebenfalls anders als in Java sowohl durch einfache als auch durch doppelte Anführungszeichen definiert werden (es gibt keinen primitiven Datentyp char für einzelne Zeichen). Praktisch ist daran, dass Sie die jeweils anderen Anführungszeichen innerhalb der Zeichenkette dann ohne EscapeSequenz verwenden können. const const const const
artist = 'DJ Shadow';
title = "Endtroducing";
message = "Der Titel der LP lautet \"Endtroducing\""; // mit Escape-Sequenz
message2 = 'Der Titel der LP lautet "Endtroducing"'; // ohne Escape-Sequenz
Listing 1.2 Strings können Sie in JavaScript sowohl mit einfachen als auch mit doppelten Anführungszeichen definieren.
Ob Sie einfache oder doppelte Anführungszeichen verwenden, ist meist Geschmackssache. Selbst die verschiedenen JavaScriptGuidelines (auf die ich in Kapitel 5, »Der Entwicklungsprozess«, eingehen werde) vertreten diesbezüglich unterschiedliche Ansichten. Allerdings verwenden gefühlt die meisten JavaScriptEntwickler eher die Schreibweise mit einfachen Anführungszeichen. Auf einzelne Zeichen eines Strings greifen Sie entweder über die Methode charAt() oder (seit ECMAScript 5) wie bei einem Array
über [] zu. Wie auch in Java sind Strings in JavaScript unveränderlich, das heißt, Methoden, die Sie auf einem String aufrufen, verändern diesen nicht, sondern geben einen neuen String zurück. Außerdem erwähnenswert: Strings können Sie mit den Operatoren < und > vergleichen. Booleans
Zu Booleans an sich gibt es in JavaScript herzlich wenig zu sagen, bis auf eine »Kleinigkeit«, die Sie beim Testen boolescher Bedingungen im Hinterkopf haben sollten: Neben den booleschen Werten false und true interpretiert JavaScript dort nämlich auch nicht boolesche Werte: entweder als »truthy« oder als »falsy«. Die Werte null, undefined, leere Strings, 0 und NaN zählen dabei zu den Werten, die als »falsy« interpretiert werden. Alle anderen Werte werden als »truthy« interpretiert. Dabei gelten folgende Regeln: false, 0 und leere Strings sind gleich: console.log(false == 0); // true
console.log(false == ''); // true
console.log(0 == ''); // true
null und undefined sind nur untereinander gleich: console.log(null == false); console.log(null == true); console.log(null == null); console.log(undefined == undefined); console.log(undefined == null);
// // // // //
false
false
true
true
true
NaN ist zu nichts gleich, sogar zu sich selbst nicht: console.log(NaN == false); // false
console.log(NaN == null); // false
console.log(NaN == NaN); // false
Innerhalb von booleschen Bedingungen evaluieren »truthy«Werte zu true und »falsy«-Werte zu false: console.log(false == null); if(null) {
console.log('null'); } else if(!null) {
console.log('!null'); }
console.log(true == {}); if({}) {
console.log('{}'); } else if(!{}) {
console.log('!{}'); }
// false
// Wird nicht ausgeführt
// Wird ausgeführt
// false
// Wird ausgeführt
// Wird nicht ausgeführt
»undefined« und »null«
In JavaScript gibt es zwei verschiedene Werte, die ausdrücken, dass eine Variable nicht belegt ist: undefined und null. Der Unterschied: Bei undefined handelt es sich um eine globale Variable. Variablen, die nicht initialisiert wurden, nicht existente Objekteigenschaften sowie nicht vorhandene Funktionsparameter haben den Wert undefined, zeigen also auf die globale Variable. Auch Funktionen, die keinen Rückgabewert haben, liefern beim Aufruf den Wert undefined. Bei null dagegen handelt es sich um ein Schlüsselwort. In der Praxis werden beide Werte zwar oft für das Gleiche verwendet (nämlich auszusagen, dass eine Variable mit keinem Wert belegt ist), allerdings sollten Sie den Wert undefined nicht explizit einer Variablen zuweisen, sondern stattdessen null verwenden. Objekte
Objekte in JavaScript sind nichts anderes als Container für SchlüsselWert-Paare. Über einen Schlüssel kann auf den dahinterliegenden Wert zugegriffen werden. Ein Wert kann ein Literal, eine Funktion
oder ein anderes Objekt sein. Der Schlüssel bezeichnet je nachdem also eine Eigenschaft oder eine Methode des Objekts. Objekte können Sie in JavaScript auf verschiedene Weise erstellen. Über Konstruktorfunktionen, über die sogenannte Objekt-LiteralSchreibweise und zu guter Letzt über die Funktion Object.create(). Für den Anfang verwenden wir die recht simple Objekt-Literal-Schreibweise und werden in Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, auf die anderen Varianten zurückkommen. Ein einfaches Objekt mit einer Funktion und zwei Eigenschaften würden Sie in Objekt-LiteralSchreibweise wie folgt anlegen: const person = {
firstName: 'Max',
lastName: 'Mustermann',
talk: function() {
console.log('Hallo');
}
}
Listing 1.3 Objekterzeugung über die Objekt-Literal-Schreibweise
Alles, was innerhalb der geschweiften Klammern steht, stellt den Inhalt des Objekts dar. In diesem Fall sind dies die zwei Eigenschaften firstName und lastName sowie die Methode talk(). Einzelne Eigenschaften und Methoden geben Sie durch Kommas separiert an. Der Wert einer Eigenschaft oder die Methode stehen jeweils hinter dem Doppelpunkt. Sie bemerken vielleicht schon eine Kuriosität: Methoden werden über das Schlüsselwort function eingeleitet. Dies unterscheidet sie syntaktisch erst einmal nicht von der Definition einer »normalen« Funktion. Erst in ES2015 können Objektmethoden auch ohne das Schlüsselwort über eine etwas andere Syntax definiert werden (dazu in Kapitel 3 dann mehr).
Der Zugriff auf Eigenschaften eines Objekts erfolgt entweder über die Punktschreibweise oder über die []-Notation. console.log(person.firstName); console.log(person[lastName]);
// Max
// Mustermann
Listing 1.4 Lesen von Objekteigenschaften
Hinweis Die Punktschreibweise funktioniert nur für Eigenschaften mit gültigem Variablennamen. Für das Arbeiten mit Eigenschaften, die keinen gültigen Namen haben – beispielsweise solche mit einem Bindestrich –, müssen Sie die []-Notation verwenden.
Da Objekte im Gegensatz zu primitiven Datentypen veränderbar sind, können Eigenschaften auf diese Weise aber nicht nur gelesen, sondern auch geschrieben, das heißt verändert werden: person.firstName = 'Moritz';
person['lastName'] = 'Tester';
console.log(person.firstName); // console.log(person.lastName); // person.talk(); // person[talk](); //
Moritz
Tester
Hallo
Hallo
Listing 1.5 Schreiben von Objekteigenschaften
Über delete löschen Sie einzelne Objekteigenschaften komplett: delete person.firstName;
console.log(person.firstName); // Ausgabe: undefined
delete person['lastName'];
console.log(person.lastName); // Ausgabe: undefined
Listing 1.6 Löschen von Objekteigenschaften
Sie werden in Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, noch eine Technik kennenlernen, die Ihnen dabei hilft, Daten innerhalb eines Objekts so zu kapseln, dass sie von außerhalb
des Objekts nicht (unerlaubt) verändert oder gelöscht werden können. Wrapper-Objekte für primitive Datentypen
Wie in vielen anderen Sprachen gibt es auch in JavaScript zu jedem primitiven Datentyp ein entsprechendes Wrapper-Objekt (beginnend mit einem Großbuchstaben). Diese Wrapper-Objekte stellen jeweils verschiedene, dem jeweiligen primitiven Datentyp entsprechende Methoden bereit. let let let let
aNumberObject = new Number(4);
anotherNumberObject = new Number(4.4);
aStringObject = new String('Hallo');
aBooleanObject = new Boolean(true);
Listing 1.7 Wrapper-Objekte für die Basistypen
Allerdings sollten Sie es in der Regel vermeiden, Wrapper-Objekte überhaupt zu verwenden, da JavaScript zwischen primitiven Datentypen und Wrapper-Objekten einen Unterschied macht. Der Typ eines jeden Wrapper-Objekts ist im Gegensatz zu dem primitiven Äquivalent nämlich immer object. let aNumber = 4;
let anotherNumber = 4.4;
let aString = 'Hallo';
let aBoolean = true;
console.log(typeof aNumber); console.log(typeof aNumberObject); console.log(typeof anotherNumber); console.log(typeof anotherNumberObject); console.log(typeof aString); console.log(typeof aStringObject); console.log(typeof aBoolean); console.log(typeof aBooleanObject);
// // // // // // // //
number
object
number
object
string
object
boolean
object
Listing 1.8 Die Typen von primitiven Datentypen und Wrapper-Objekten stimmen nicht überein.
Problematisch wird das Ganze, wenn Sie nun zwei Variablen miteinander vergleichen und die eine Variable einen primitiven Datentyp verwendet, die andere dagegen ein Wrapper-Objekt, beide aber letztendlich den gleichen Wert repräsentieren: Vergleichen wir beispielsweise die beiden Variablen aNumber und aNumberObject aus Listing 1.8 mit dem ==-Operator (einfache Gleichheit), erhalten wir zwar ein true, weil JavaScript die automatische Typkonvertierung durchführt (dazu später mehr). Ein Vergleich mit dem ===-Operator (strikte Gleichheit), der auch den Typ überprüft, ergibt aber konsequenterweise ein false. Ich werde auf die Operatoren noch einmal zurückkommen, Sie können sich aber schon einmal Folgendes merken: Für einen direkten Vergleich zwischen zwei Werten sollten Sie immer den ===-Operator verwenden, der auch den Typ überprüft. In den meisten Fällen will man nämlich nicht, dass JavaScript eine automatische Typkonvertierung vornimmt. Genau aus diesem Grund sollten Sie aber auch die Wrapper-Objekte vermeiden, denn sie können unter Verwendung dieses Operators nicht mit primitiven Datentypen verglichen werden. Außerdem ist zu beachten: Wie weiter oben im Kapitel gesehen, gelten Objekte als »truthy«, new Boolean(false) würde in einer booleschen Bedingung zu true evaluieren. Wozu dann überhaupt Wrapper-Objekte? Der Grund ist einfach: Wrapper-Objekte werden im Hintergrund erzeugt. Beispielsweise immer dann, wenn auf einem primitiven Datentyp eine Methode aufgerufen wird. Denn dies ist eigentlich nicht möglich: Nur Objekte können Methoden haben. Dank der automatischen Typkonvertierung wandelt JavaScript aber in solchen Fällen den primitiven Datentyp in ein temporäres Wrapper-Objekt um und ruft entsprechende Methoden auf diesem Objekt auf. Ist der Ergebniswert der Methode dann ermittelt, wird das Objekt wieder verworfen.
Arrays
Arrays sind in JavaScript ebenfalls Objekte und können sowohl über die Konstruktorfunktion new Array() als auch über eine LiteralKurzschreibweise deklariert werden. Sie können ein Array also wie folgt erstellen und mit Werten befüllen: const artists = new Array();
artists[0] = 'Kyuss';
artists[1] = 'Baby Woodrose';
artists[2] = 'Hermano';
artists[3] = 'Monster Magnet';
artists[4] = 'Queens of the Stone Age';
Listing 1.9 Erzeugung eines Arrays
Oder Sie verwenden die Kurzschreibweise: const artists= [
'Kyuss',
'Baby Woodrose',
'Hermano',
'Monster Magnet',
'Queens of the Stone Age'
];
Listing 1.10 Kurzschreibweise für die Erzeugung von Arrays
Wobei Sie bei Verwendung der Konstruktorfunktion eines im Hinterkopf haben sollten: Übergeben Sie der Funktion einen einzelnen Zahlenparameter, wird dieser Parameter als Länge des zu erstellenden Arrays angesehen. new Array(10);
erzeugt somit ein Array der Länge 10, wobei die Werte alle undefined sind. new Array(10, 11);
dagegen erzeugt ein Array der Länge 2 mit den Werten 10 und 11.
Neue Elemente fügen Sie entweder wie gezeigt direkt über Angabe des Index hinzu oder über die Array-Methode push(), die das neue Element an das bestehende Array hängt. Tabelle 1.3 gibt eine kurze Übersicht der wichtigsten Methoden von Arrays. Auf einige davon werde ich in Kapitel 2, »Funktionen und funktionale Aspekte«, näher eingehen, wenn wir uns den funktionalen Aspekten von JavaScript zuwenden. Methode
Beschreibung
concat()
Hängt Elemente oder Arrays an ein bestehendes Array an.
filter()
Filtert Elemente aus dem Array auf Basis eines in Form einer Funktion übergebenen Filterkriteriums.
forEach() Wendet eine übergebene Funktion auf jedes Element im Array an. join()
Wandelt ein Array in eine Zeichenkette um.
map()
Bildet die Elemente eines Arrays auf Basis einer übergebenen Umwandlungsfunktion auf neue Elemente ab.
pop()
Entfernt das letzte Element eines Arrays.
push()
Fügt ein neues Element am Ende des Arrays ein.
reduce()
Fasst die Elemente eines Arrays auf Basis einer übergebenen Funktion zu einem Wert zusammen.
reverse() Kehrt die Reihenfolge der Elemente im Array um. shift()
Entfernt das erste Element eines Arrays.
Methode
Beschreibung
slice()
Schneidet einzelne Elemente aus einem Array heraus.
splice()
Fügt neue Elemente an beliebiger Position im Array hinzu.
sort()
Sortiert das Array, optional auf Basis einer übergebenen Vergleichsfunktion.
unshift() Fügt ein Element oder mehrere Elemente an den Anfang eines Arrays hinzu. Tabelle 1.3 Die wichtigsten Methoden von Arrays
Reguläre Ausdrücke
Reguläre Ausdrücke können sowohl über eine Konstruktorfunktion erzeugt werden als auch über ein Literal. In letzterem Fall beginnen und enden sie jeweils mit einem Slash (/). In beiden Fällen sind reguläre Ausdrücke Instanzen von RegExp. const startingWithHTTP = /http/;
Listing 1.11 Erzeugen eines regulären Ausdrucks mit Literal-Schreibweise
Über die Methode test() können Sie prüfen, ob ein String auf einen regulären Ausdruck passt, über exec() können Sie zudem die einzelnen Teile des Strings ermitteln, die auf den regulären Ausdruck passen. Des Weiteren funktionieren reguläre Ausdrücke auch im Zusammenspiel mit Strings: Über die String-Methode replace() etwa lassen sich einzelne Teile eines Strings ersetzen, search() findet den Index einer Übereinstimmung eines Strings, split() teilt Strings optional anhand eines regulären Ausdrucks,
und match() findet alle Übereinstimmungen innerhalb eines Strings. Automatische Typkonvertierung
JavaScript verfügt, wie ich schon ansatzweise bei der Interpretierung von booleschen Ausdrücken erwähnt habe, über eine automatische Typkonvertierung. Dabei versucht JavaScript, falls notwendig, einen Typ in einen anderen Typ umzuwandeln. Wenn beispielsweise wie in Listing 1.12 versucht wird, eine Zeichenkette an einer Stelle zu verwenden, an der eigentlich eine Zahl erwartet wird, und sich die Zeichenkette in eine Zahl konvertieren lässt, führt JavaScript eine automatische Typkonvertierung durch. console.log(Math.sqrt('25'));
Listing 1.12 Der JavaScript-Interpreter versucht, Typen, falls notwendig, automatisch zu konvertieren.
Es ist wichtig, sich bei der JavaScript-Entwicklung dieser automatischen Typkonvertierung bewusst zu sein, denn beispielsweise auch beim Vergleich zweier Variablen über den Gleichheitsoperator == versucht der JavaScript-Interpreter, eine Typumwandlung vorzunehmen. So liefert der Vergleich 4711 == '4711' den booleschen Wert true. Erst die Verwendung des Identitätsoperators === liefert ein false, da dieser Operator neben dem Wert auch den Typ einer Variablen überprüft. 1.7.3 Variablen und Konstanten
In JavaScript wird zwischen Variablen und Konstanten unterschieden. Wie der Name jeweils sagt, können Erstere ihren Wert ändern, Letztere dagegen nicht. Sollte man zumindest meinen.
Variablen
Variablen werden in JavaScript über die Schlüsselwörter var und (seit ES2015) let deklariert. Im Gegensatz zu streng typisierten Programmiersprachen wie Java wird dabei, wie bereits erwähnt, kein Typ angegeben. JavaScript ermittelt den Datentyp dynamisch, und zwar immer dann, wenn der Variablen ein Wert zugewiesen wird. Das heißt, eine Variable kann während der Laufzeit eines Programms durchaus ihren Typ ändern. Wobei dies in der Regel nicht als tolles Feature verstanden und eher mit Vorsicht genossen werden sollte. // Vor ES2015:
var x = 5;
console.log(typeof x); x = 'Hallo';
console.log(typeof x); // ES2015 und neuer:
let y = 5;
console.log(typeof y); y = 'Hallo';
console.log(typeof y);
// "number"
// "string"
// "number"
// "string"
Listing 1.13 Variablen sind dynamisch typisiert, können den Typ also auch während der Laufzeit ändern.
Den genauen Unterschied zwischen var und let werde ich Ihnen in Kapitel 4, »ECMAScript 2015 und neuere Versionen«, erklären. So viel kann ich aber schon verraten: Mit let angelegte Variablen sind nur im aktuellen Codeblock sichtbar, mit var angelegte Variablen innerhalb der gesamten Funktion, innerhalb der sie definiert wurden, oder global, falls sie nicht innerhalb einer Funktion definiert wurden. Außerdem sollten Sie – sofern Sie eine ECMAScript-Version ES2015 oder neuer verwenden – Variablen nicht mehr mit var, sondern mindestens mit let deklarieren. Warum mindestens? Weil es mittlerweile eigentlich Best Practice ist, Variablen, die im Verlauf ihres Gültigkeitsbereichs nicht mehr
verändert werden (und damit eigentlich keine Variablen, sondern Konstanten sind), nicht mit let zu deklarieren, sondern mit const. In diesem Buch handhabe ich die Deklaration von Variablen demnach gemäß folgenden Konventionen: var wird nur in Ausnahmefällen verwendet, beispielsweise wenn
der entsprechende Code explizit für ES5 oder früher geschrieben ist.
let wird in den Fällen verwendet, in denen die Variable innerhalb
ihres Gültigkeitsbereichs mit einem neuen Wert belegt wird.
const wird in den Fällen verwendet, in denen die »Variable«
innerhalb ihres Gültigkeitsbereichs nicht mit einem neuen Wert belegt wird, sondern unverändert bleibt. Globale Variablen
Generell lassen sich Variablen auch ohne die Angabe eines der Schlüsselwörter var, let oder const anlegen. Lassen Sie es weg, ist die Variable automatisch global, unabhängig davon, in welchem Kontext sie angelegt wird. Globale Variablen werden dann implizit als Eigenschaften des globalen Objekts definiert, das sich je nach Laufzeitumgebung unterscheiden kann, z.B. im Browser das Objekt window (https://developer.mozilla.org/enUS/docs/Web/API/Window), in Node.js ein anderes spezielles Objekt (http://nodejs.org/api/globals.html). Variablen, die ohne Schlüsselwort angelegt werden, können Eigenschaften des globalen Objekts überschreiben und sollten unbedingt vermieden werden. Konstanten
Neben Variablen gibt es in JavaScript seit ES2015 die Möglichkeit, Konstanten zu definieren. Dies geschieht passenderweise über das Schlüsselwort const. Der Wert einer Konstanten kann nach der Initialisierung nicht mehr verändert werden. Ein entsprechender Versuch führt zu einem Fehler: const LOG_LEVEL_DEBUG = 'debug';
console.log(LOG_LEVEL_DEBUG); // Ausgabe: debug
LOG_LEVEL_DEBUG = 'info'; // TypeError
Listing 1.14 Konstanten kann kein neuer Wert zugewiesen werden.
Vor ES2015 gab es keine Konstanten, und Sie mussten diese bei Bedarf über bestimmte Techniken emulieren. In Kapitel 4, »ECMAScript 2015 und neuere Versionen«, in dem ich noch einmal auf die neue Konstantensyntax zurückkomme, werde ich Ihnen auch zeigen, welche Techniken bzw. Entwurfsmuster vor ES2015 zum Einsatz kamen, um Konstanten zu emulieren. Namenswahl
Variablennamen müssen mit einem Buchstaben, einem Unterstrich oder dem Dollarzeichen beginnen. Die darauffolgenden Zeichen dürfen Buchstaben, Ziffern und den Unterstrich enthalten. 1.7.4 Funktionen
Funktionen nehmen in JavaScript einen besonderen Stellenwert ein. Wir werden uns diesem Thema im nächsten Kapitel noch ausführlicher widmen, aber auf Folgendes sei schon einmal hingewiesen: Funktionen sind in JavaScript »first class«. Das bedeutet, Funktionen können als Parameter anderer Funktionen verwendet, Variablen zugewiesen oder als Rückgabewert einer Funktion genutzt werden.
Ähnlich wie reine Objekte können Sie auch Funktionen auf verschiedene Weise erstellen: über eine Funktionsanweisung (function statement) über einen Funktionsausdruck (function expression) über den Konstruktor des Function-Objekts als sogenannte Arrow-Funktion (siehe Kapitel 4, »ECMAScript 2015 und neuere Versionen«) Funktionen erzeugen über Funktionsanweisung
Eine einfache Funktion sehen Sie in Listing 1.15. Sie sehen: Weder Parameter noch Typ des Rückgabewertes werden dabei explizit angegeben. Die Funktion erlaubt prinzipiell also einerseits die Addition zweier Zahlen, könnte aber auch zwei Strings konkatenieren. Oder eine Zahl und einen String. Oder umgekehrt. Oder zwei boolesche Werte. function add(number1, number2) {
return number1 + number2;
};
Listing 1.15 Deklaration einer Funktion über eine Funktionsanweisung
Um explizit nur Zahlen als Parameter zu erlauben, ist dagegen eine manuelle Typüberprüfung notwendig: function add(number1, number2) {
if((typeof number1 !== 'number')
|| (typeof number2 !== 'number')) {
throw new TypeError('Parameter müssen Zahlen sein.');
}
return number1 + number2;
};
Listing 1.16 Überprüfung des Typs von Funktionsparametern
Funktionen erzeugen über Funktionsausdruck
Alternativ zur Funktionsanweisung können Sie sogenannte Funktionsausdrücke verwenden, um Funktionen zu definieren. Hierbei weisen Sie die Funktion direkt einer Variablen zu. Über diese Variable können Sie später die Funktion aufrufen. const add= function addFunction(number1, number2) {
return number1 + number2;
};
Listing 1.17 Deklaration einer Funktion über einen Funktionsausdruck
Den Namen der Funktion selbst (addFunction) können Sie auch weglassen: const add= function(number1, number2) {
return number1 + number2;
};
Listing 1.18 Deklaration einer anonymen Funktion und Zuweisung an Variable
Hinweis Der Name der ursprünglichen Funktion wird in der Eigenschaft name gespeichert. Im ersten Beispiel enthält add.name den Wert addFunction, im zweiten Beispiel ist der Wert leer (»«). Aufgerufen wird die Funktion in beiden Fällen über add().
Funktionen erzeugen über Konstruktorfunktion
Alternativ können Sie eine Funktion auch über den FunctionKonstruktor definieren. Als Parameter bekommt dieser Konstruktor den Funktionskörper (als String) sowie optional die Namen der Parameter, die die zu erstellende Funktion entgegennehmen soll. Der Funktionskörper steht dabei an letzter Stelle.
const add= new Function(
'number1', 'number2', 'return number1 + number2' );
// Erster Parameter
// Zweiter Parameter
// Funktionskörper
Listing 1.19 Deklaration einer Funktion über den »Function«-Konstruktor
In der Praxis sollten Sie jedoch vermeiden, Funktionen auf diese Weise zu erstellen, weil der Funktionskörper erst dann geparst wird, wenn der Function-Konstruktor innerhalb des Programms aufgerufen wird. Außerdem funktionieren solche Funktionen nicht im Zusammenspiel mit Closures, einem Feature, das Sie im nächsten Kapitel kennenlernen werden (siehe Abschnitt 2.5.3). Und überhaupt: den gesamten Funktionskörper als String angeben? Nein, danke. Arrow-Funktionen
Mit der Version ES2015 gibt es eine weitere Möglichkeit, Funktionen besonders platzsparend zu definieren. Die Syntax, die dazu eingeführt wird, läuft unter dem Namen Arrow Function und erinnert an Konstrukte aus anderen funktional orientierten Sprachen wie Groovy und Scala oder an Lambda-Ausdrücke, wie sie beispielsweise mit Java 8 eingeführt wurden: let add = (x) => { return x + x };
Listing 1.20 Deklaration einer Funktion als Arrow Function [ES2015]
Kleiner Vorgriff an dieser Stelle: Viel wichtiger als die kompaktere Schreibweise ist etwas anderes, das sogenannte this-Binding, aber dazu mehr in Kapitel 4, wenn ich Ihnen die neuen Features aus ES2015 vorstelle.
Funktionen aufrufen
Nachdem Sie nun eine Funktion erzeugt haben (auf welche Art und Weise auch immer), können Sie die entsprechende Funktion wie folgt aufrufen: const result = add(2, 2);
console.log(result); // Ausgabe: 4
let result2 = add('Hallo ', 'Welt');
console.log(result2); // Ausgabe, wegen String-Konkatenation: Hallo Welt
Listing 1.21 Aufruf einer Funktion
Dynamische Anzahl an Funktionsparametern
Jedes Mal, wenn eine Funktion aufgerufen wird, steht innerhalb der Funktion implizit ein Objekt mit dem Namen arguments zur Verfügung, das die Funktionsargumente bzw. Funktionsparameter enthält, die beim Funktionsaufruf übergeben wurden. Dieses Objekt ist zwar »arrayähnlich«, aber kein wirkliches Array. Konkret bedeutet das, dass dieses Objekt wie Arrays über eine Eigenschaft length verfügt und es auch möglich ist, über Indizes auf die einzelnen Elemente zuzugreifen. Array-Methoden wie concat(), slice() oder forEach() stehen jedoch nicht zur Verfügung. Besonders häufig wird das arguments-Objekt dann verwendet, wenn eine Funktion mit beliebig vielen Parametern oder einer variablen Anzahl an Parametern aufgerufen werden können soll, z.B. die in Listing 1.22 gezeigte Additionsfunktion, die beliebig viele Zahlen miteinander addiert: function addAll() {
let result = 0;
for(let i=0; i=
Liefert true, wenn der linke Operand größer als der rechte Operand oder gleich dem rechten Operanden ist.
kleiner als
>> Rechtsverschiebung ohne Beachtung des Vorzeichens
bitweise Rechtsverschiebung des linken Operanden um Anzahl der Stellen, die durch den rechten Operanden definiert wird ohne Beachtung des Vorzeichens
Tabelle 1.7 Bitweise Operatoren in JavaScript
Spezielle Operatoren
Neben den genannten Operatoren steht in JavaScript eine Reihe spezieller Operatoren zur Verfügung. Tabelle 1.8 gibt eine Übersicht. Operation
Operator
Beschreibung
konditionaler Operator
? :
tertiärer Operator, der abhängig von einer Bedingung (erster Operand) einen von zwei Werten zurückgibt (die durch den zweiten und dritten Operanden definiert werden)
Operation
Operator
Löschen von delete Objekten, Objekteigenschaften oder Elementen innerhalb eines Arrays
Beschreibung Erlaubt das Löschen von Elementen in einem Array, das Löschen von Objekten sowie das Löschen von Objekteigenschaften.
Existenz einer Eigenschaft in einem Objekt
Überprüft, ob eine Eigenschaft in einem in Objekt vorhanden ist.
Typüberprüfung
instanceof
binärer Operator, der überprüft, ob ein Objekt von einem bestimmten Typ oder Instanz einer Klasse ist
Typbestimmung
typeof
Ermittelt den Datentyp des Operanden. Der Operand kann dabei ein Objekt, ein String, eine Variable oder ein Schlüsselwort wie true oder false sein. Optional kann der Operand in Klammern angegeben werden.
Tabelle 1.8 Spezielle Operatoren in JavaScript
1.7.6 Kontrollstrukturen und Schleifen
Kontrollstrukturen und Schleifen funktionieren in JavaScript ähnlich wie in Java oder C#. Erwarten Sie also in diesem Abschnitt keine großen Überraschungen. Ein paar kleine gibt es dennoch. if/else
Listing 1.24 zeigt die Verwendung einer if-else-Kontrollstruktur: if(i > 8) {
console.log('i ist größer als 8');
} else {
console.error('i ist kleiner oder gleich 8');
}
Listing 1.24 Verwendung der »if-else«-Anweisung
Im Gegensatz zu beispielsweise Java lassen sich in JavaScript innerhalb der if-Klausel nicht nur boolesche Werte, sondern Werte beliebigen Typs verwenden. Erinnern Sie sich: Jeder Wert in JavaScript evaluiert innerhalb boolescher Bedingungen entweder zu true oder false. Insbesondere die Tatsache, dass undefined und null zu false evaluieren, ist in der Praxis sehr bequem. Um beispielsweise innerhalb einer Funktion zu überprüfen, ob ein Parameter einer Funktion definiert ist, reicht statt folgender Überprüfung … function exampleFunction(parameter) {
if(parameter !== undefined && parameter !== null) {
console.log('Definiert und nicht null');
}
}
Listing 1.25 Klassische Verwendung boolescher Bedingungen
… der Code aus Listing 1.26: function exampleFunction(parameter) {
if(parameter) {
console.log('Definiert und nicht null');
}
}
Listing 1.26 Da JavaScript jeden Wert als »true« oder »false« interpretiert, können viele boolesche Bedingungen einfacher ausgedrückt werden.
switch
Mehrfachverzweigungen können Sie in JavaScript über die switchAnweisung definieren, wie aus anderen Sprachen bekannt. Hierbei unterstützt die switch-Klausel Werte beliebigen Typs. (Als Hintergrundinformation: In Java war es beispielsweise lange nicht möglich, String-Werte als Werte für switch-Anweisungen zu verwenden. In JavaScript geht das schon lange.) Aufgrund der dynamischen Eigenschaften von JavaScript ist es zudem möglich, die Werte für die einzelnen case-Ausdrücke dynamisch über Funktionsaufrufe ermitteln zu lassen (in Java beispielsweise müssen die Werte Konstanten sein). Ergeben dabei mehrere Funktionsaufrufe den gleichen Wert, wird der case-Ausdruck ausgewählt, der zuerst eintritt. function returnFour() {
return 4;
}
function alsoReturnFour() {
return 4;
}
const s = 4;
switch(s) {
case returnFour():
console.log('returnFour()');
break;
case alsoReturnFour():
console.log('alsoReturnFour()');
break;
default: console.log('nichts');
}
// Ausgabe des Programms: "returnFour()"
Listing 1.27 Verwendung der »switch«-Anweisung
Schleifen
Bezüglich Schleifen gibt es in JavaScript keine großen Überraschungen: Es stehen while-, do-while- sowie for-Schleifen zur Verfügung. Ein Countdown von 10 bis 1 sähe jeweils wie folgt aus: let i = 10;
while (i > 0) {
console.log(i);
i--;
}
Listing 1.28 Verwendung einer »while«-Schleife let i = 10;
do {
console.log(i);
i--;
} while (i > 0);
Listing 1.29 Verwendung einer »do-while«-Schleife for (let i = 10; i > 0; i--) {
console.log(i);
}
Listing 1.30 Verwendung der »for«-Schleife
Neben diesen Standardschleifen gibt es weitere Schleifenarten, mit denen Sie beispielsweise über die Eigenschaften eines Objekts iterieren: die for...in-Schleife, die dabei in jeder Iteration den Namen der Eigenschaft zurückgibt, sowie die (in ES2015 neu eingeführte) for...of-Schleife, die in jeder Iteration den Wert hinter der Eigenschaft zurückgibt. Beide Schleifentypen werden wir in Kapitel 4, »ECMAScript 2015 und neuere Versionen«, besprechen. 1.7.7 Fehlerbehandlung
Eine Fehlerbehandlung gibt es in JavaScript erst seit ECMAScript 3. Davor gab es keine Möglichkeit, Fehler in irgendeiner Art und Weise zu behandeln. Dies ist mitunter einer der Gründe, warum der Interpreter in JavaScript viele Dinge stillschweigend hinnimmt, ohne überhaupt einen Fehler zu werfen. Prinzipiell funktioniert die Fehlerbehandlung seit ECMAScript 3 ähnlich wie auch in Java oder C#. Fehler werden über throw geworfen und in einem try-catch-finally-Konstrukt gefangen. Ein throws wie in Java, das dazu dient, in der Methodendeklaration anzugeben, welche Fehler eine Methode werfen kann, gibt es in JavaScript dagegen nicht. console.log(checkAge(22))D; // true
console.log(checkAge(-22)); // Error: Alter darf nicht negativ sein
function checkAge(age) {
if (age < 0) {
throw new Error('Alter darf nicht negativ sein.');
} else {
return true;
}
}
Listing 1.31 Eine Funktion, die einen Fehler wirft try {
console.log(checkAge(-22));
} catch(error) {
console.log(error); // RangeError: Alter darf nicht negativ sein
}
Listing 1.32 Um auf einzelne Fehler reagieren zu können, müssen die entsprechenden Funktionsaufrufe mit einem »try-catch«-Block umgeben werden.
Zudem gibt es einige weitere Unterschiede: So können im Grunde genommen beliebige Objekte mit throw geworfen werden. Es ist allerdings guter Stil, ein Objekt vom Typ Error zu werfen. Standardmäßig bietet JavaScript bereits einige Fehlertypen an, die Sie Tabelle 1.9 entnehmen können. Weitere Fehlertypen lassen sich definieren, indem Sie von einem dieser bestehenden Fehlertypen
ableiten. Wie Sie von Objekten ableiten und wie generell Vererbung in JavaScript funktioniert, zeige ich Ihnen in Kapitel 3, »Objektorientierte Programmierung mit JavaScript«. Fehler
Beschreibung
Error
Dies ist das Basisobjekt für alle Arten von Fehlern und entspricht in etwa dem, was man als Java-Entwickler als Runtime Exception bezeichnet: ein Fehler, der zur Laufzeit auftreten kann. Von Error sollten Sie ableiten, falls Sie ein eigenes Fehlerobjekt erstellen wollen.
EvalError
Wird geworfen, falls beim Aufruf der eval()Methode ein Fehler auftritt, also der Methode, die JavaScript-Code in Form eines Strings entgegennimmt und auswertet.
SyntaxError
Tritt auf, wenn der JavaScript-Interpreter auf invaliden Code trifft.
RangeError
Repräsentiert den Fehlerfall, dass ein Wert nicht im gültigen Wertebereich liegt.
TypeError
Repräsentiert den Fehlerfall, dass ein Wert nicht von einem erwarteten Typ ist.
ReferenceError Tritt auf, falls eine nicht existente Variable referenziert wird. URIError
Wird geworfen, wenn beim Parsen einer URL ein Fehler auftritt.
Tabelle 1.9 Die Standardfehlertypen im Überblick
Hinweis Ein weiterer Unterschied zu anderen Programmiersprachen ist, dass es in JavaScript in einem try-catch-finally-Konstrukt nur ein einziges catch geben kann. Dies ist leicht nachvollziehbar, gibt es doch aufgrund fehlender statischer Typisierung keine Ausdrucksmittel zum Auszeichnen verschiedener catch-Typen. Der Vollständigkeit halber: Eine Ausnahme bildet hier die Laufzeitumgebung, die in Firefox verwendet wird. Hier ist es möglich, verschiedene catch-Klauseln zu verwenden: try {
throw new RangeError();
} catch (error if error instanceof TypeError) {
console.log("TypeError");
} catch (error if error instanceof RangeError) {
console.log("RangeError");
}
Listing 1.33 Mehrere »catch«-Klauseln gibt es nicht in allen Laufzeitumgebungen.
1.7.8 Sonstiges Wissenswertes
Herzlichen Glückwunsch! Mit den Informationen aus dem bisherigen Kapitel wissen Sie schon über die wichtigsten Grundlagen von JavaScript Bescheid. Im Folgenden seien nur noch kurz einige weitere Aspekte erwähnt, bevor wir uns ab Kapitel 2, »Funktionen und funktionale Aspekte«, den etwas komplexeren Themen zuwenden. Semikolons
Semikolons hinter Anweisungen sind in JavaScript zwar optional und werden bei Fehlen vom JavaScript-Interpreter selbständig eingefügt. Da dies aber nicht immer so geschieht, wie man es erwartet, sollten Sie sich angewöhnen, jede JavaScript-Anweisung mit einem Semikolon zu schließen. Kommentare
In JavaScript stehen Ihnen zwei verschiedene Arten von Kommentaren zur Verfügung: Einzeilige Kommentare beginnen mit einem Doppelslash (//), mehrzeilige Kommentare beginnen mit einem /* und enden mit einem */. Schlüsselwörter
Folgende Schlüsselwörter gibt es in JavaScript; sie können folglich nicht als Variablennamen verwendet werden: abstract, async, await, boolean, break, byte, case, catch, char, class, const, continue, debugger, default, delete, do, double, enum, else, export, extends, final, finally, float, for, function, goto, if, implements, import, in, instanceof, int, interface, let, long, native, new, package, private, protected, public, return, short, static, super, switch, synchronized, this, throw, throws, transient, try, typeof, var, void, volatile, while, with, und yield. Sie sehen: Es gibt eine ganze Reihe von Schlüsselwörtern, wie beispielsweise interface, package oder private, die man aus Java oder C# kennt und die auf gewisse Funktionalitäten hindeuten mögen. Aber ich muss Sie enttäuschen: In JavaScript gibt es weder Interfaces noch Packages noch Support für private Variablen. In Kapitel 3, »Objektorientierte Programmierung mit JavaScript«,
werde ich Ihnen aber einige Techniken vorstellen, wie Sie entsprechende Features emulieren können. Strikter Modus
JavaScript verfügt seit ECMAScript 5 über einen sogenannten strikten Modus (Strict Mode), bei dem es gewisse Einschränkungen gibt, der aber gerade deswegen letztendlich zu robusterem Code führt (siehe auch https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Stri ct_mode). Aktivieren lässt sich der strikte Modus über die Anweisung 'use strict'; zu Anfang eines Scripts (Laufzeitumgebungen, die diesen Modus nicht unterstützen, ignorieren diese Anweisung). Beispielsweise wird ein Fehler erzeugt, wenn Sie vergessen, eine Variable mit Schlüsselwort var oder let anzulegen (wodurch implizit eine globale Variable angelegt würde). 'use strict';
let variable = 4;
variable2 = 4711; // Führt zu dem Fehler "variable2 is not defined"
Listing 1.34 Verwendung des strikten Modus
Weitere Dinge, die im strikten Modus nicht erlaubt sind: Verwendung mehrerer gleichnamiger Namen für Objekteigenschaften oder Funktionsparameter, Überschreiben der globalen Variablen undefined sowie des arguments-Objekts und vieles mehr. Ob der strikte Modus innerhalb eines Programms verwendet werden soll, geben Sie dabei entweder global oder auf Funktionsebene an.
1.8 Zusammenfassung und Ausblick Neben einer kurzen Historie von JavaScript haben Sie in diesem Kapitel gelernt, welche Laufzeitumgebungen und welche Entwicklungsumgebungen es für JavaScript gibt. Außerdem habe ich Ihnen eine Einführung in die Syntax und die grundlegenden Sprachelemente gegeben. Einige der Dinge werden wir in den nächsten Kapiteln vertiefen, beispielsweise die funktionalen und objektorientierten Aspekte. JavaScript hat zwei wesentliche Vorbilder: die Sprache Self, von der JavaScript die funktionalen Einflüsse geerbt hat, und die Sprache Scheme, von der die prototypischen Einflüsse stammen. JavaScript kommt in vielen Bereichen zum Einsatz: bei der Entwicklung von Webanwendungen, mobilen Anwendungen, Desktop-Anwendungen und Embedded-Anwendungen. Dabei ist JavaScript nicht nur auf Clientseite einsetzbar, sondern auch auf Serverseite. Für JavaScript gibt es verschiedene Laufzeitumgebungen. Grob gesagt können wir zwischen solchen Laufzeitumgebungen unterscheiden, die auf Clientseite (also im Browser) zum Einsatz kommen, und solchen, die auf Serverseite verwendet werden. Die bekannteste Laufzeitumgebung für die Serverseite ist Node.js. Mittlerweile gibt es viele gute Editoren und Entwicklungsumgebungen für die Entwicklung von JavaScriptAnwendungen. Für das Debugging verwenden Sie am besten die in den verschiedenen Browsern eingebauten Tools. Diese lassen sich in den meisten Fällen sogar in die verschiedenen
Entwicklungsumgebungen integrieren oder aus dem DebugModus einer Anwendung heraus aufrufen. JavaScript ist dynamisch typisiert und kennt sechs unterschiedliche Typen: number, string, boolean, undefined, null und object. Die Sprachmittel von JavaScript sind ähnlich denen anderer Programmiersprachen. Im Detail müssen Sie aber bestimmte Dinge beachten, wie etwa bei der Deklaration von Strings, dem Vergleich von booleschen Werten und beispielsweise der switchAnweisung. Im folgenden Kapitel werde ich Ihnen zeigen, welch hohen Stellenwert Funktionen in JavaScript einnehmen, sowie Ihnen die funktionalen Aspekte vorstellen, die sich daraus ergeben. Zudem werde ich Ihnen einige funktionale Techniken zeigen, die für die professionelle JavaScript-Entwicklung wichtig sind und die Grundlage für viele Entwurfsmuster bilden.
2 Funktionen und funktionale Aspekte Die funktionalen Aspekte von JavaScript bilden die Grundlage für viele Entwurfsmuster dieser Sprache; ein Grund, sich diesem Thema direkt zu Beginn zu widmen. Eine der wichtigsten Eigenschaften von JavaScript ist, dass es sowohl funktionale als auch objektorientierte Programmierung ermöglicht. Dieses und das folgende Kapitel stellen die beiden Programmierparadigmen kurz vor und erläutern anschließend jeweils im Detail die Anwendung in JavaScript. Ich starte bewusst mit den funktionalen Aspekten, weil viele der in Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, beschriebenen Entwurfsmuster auf diesen funktionalen Grundlagen aufbauen. Ziel dieses Kapitels ist es nicht, aus Ihnen einen Profi im funktionalen Programmieren zu machen. Verstehen Sie dies nicht falsch, aber das wäre auf knapp 70 Seiten schon recht sportlich. Vielmehr ist mein Ziel, Ihnen die wichtigsten funktionalen Konzepte, die bei der JavaScript-Entwicklung zum Einsatz kommen, zu veranschaulichen und Ihnen zu jedem Konzept Einsatzgebiete und Anwendungsbeispiele vorzustellen.
2.1 Die Besonderheiten von Funktionen in JavaScript Fassen wir kurz die Punkte aus dem letzten Kapitel zusammen, die Sie dort über Funktionen gelernt haben:
Funktionen werden in JavaScript durch Objekte repräsentiert und über das Schlüsselwort function definiert (es sei denn, Sie definieren in ES2015 eine Arrow-Funktion oder eine Objektmethode, dazu gleich mehr). Funktionen können auf unterschiedliche Arten erzeugt werden: über eine Funktionsanweisung, über einen Funktionsausdruck, über den Aufruf der Konstruktorfunktion Function und seit ES2015 als Arrow-Funktion. Implizit haben Sie innerhalb der Funktion Zugriff auf alle Funktionsparameter über das arguments-Objekt. Lassen Sie mich im Folgenden auf diese einzelnen Punkte genauer eingehen sowie einige weitere Eigenschaften und Besonderheiten von Funktionen in JavaScript vorstellen. 2.1.1 Funktionen als First-Class-Objekte
In JavaScript werden Funktionen durch Objekte repräsentiert, genauer gesagt als Instanzen des Function-Typs. Legt man beispielsweise folgende Funktion an, … function add(x, y) {
return x + y;
}
Listing 2.1 Definition einer Funktion
… werden eigentlich, wie in Abbildung 2.1 zu sehen, ein Funktionsobjekt mit dem Namen add erzeugt sowie eine gleichnamige Variable (add), die auf dieses Funktionsobjekt zeigt.
Abbildung 2.1 Funktionen werden durch Objekte repräsentiert.
Jedes Funktionsobjekt verfügt dabei standardmäßig über drei Eigenschaften: name enthält den Namen der Funktion, length die Anzahl an (in der Deklaration definierten) Funktionsparametern und prototype den sogenannten Prototyp der Funktion. Letzterer bezeichnet kurz gesagt das Objekt, auf dem das jeweilige Funktionsobjekt basiert. Details dazu gibt es im nächsten Kapitel, wenn es an die objektorientierten und prototypischen Aspekte von JavaScript geht. Neben diesen drei Eigenschaften hat jede Funktion ihrerseits eigene Funktionen bzw. Methoden: bind(), apply() und call(). Abschnitt 2.2, »Standardmethoden jeder Funktion«, beschreibt, wozu diese Methoden gut sind und in welchen Fällen Sie sie benötigen. Definition von Methoden und Funktionen Im Weiteren wollen wir Funktionen, die als Eigenschaft eines Objekts oder einer anderen Funktion definiert werden, wie im Programmierjargon üblich, Methoden nennen. Funktionen, die für sich stehen, nennen wir weiterhin Funktionen.
Funktionen sind also Objekte. Das bedeutet logischerweise, dass sie an allen Stellen verwendet werden können, an denen auch »normale« Objekte verwendet werden können: Sie können
Variablen zugewiesen, als Werte innerhalb von Arrays verwendet, innerhalb von Objekten oder gar innerhalb anderer Funktionen definiert und als Parameter oder Rückgabewert von Funktionen verwendet werden. Lassen Sie mich Ihnen in den folgenden Abschnitten die einzelnen dieser Fälle kurz anhand von ein paar Quelltextbeispielen vorstellen. Definition von Funktionen erster Klasse Aufgrund ihrer Repräsentation durch Objekte sowie der gerade beschriebenen Verwendungsmöglichkeiten von Funktionen im Code spricht man in diesem Zusammenhang auch von Funktionen erster Klasse (First-Class Functions). Funktionen haben den gleichen Stellenwert wie Objekte oder primitive Datentypen, sie sind »first class«. Funktionen, die andere Funktionen als Parameter erwarten oder als Rückgabewert liefern, nennt man zusätzlich Funktionen höherer Ordnung (Higher-Order Functions).
Funktionen Variablen zuweisen
Wenn eine Funktion einer Variablen zugewiesen wird, passiert nichts anderes, als dass die Variable anschließend auf das Funktionsobjekt zeigt. Die Funktion kann dann über die Variable aufgerufen werden, wie Listing 2.2 zeigt, in dem die von eben bekannte Funktion add der Variablen operation zugewiesen wird: const operation = add;
Listing 2.2 Zuweisung einer Funktion zu einer Variablen
Zu beachten ist, dass die Funktion add dabei nicht aufgerufen wird, was ein häufig gemachter Flüchtigkeitsfehler wäre, der dazu führen
würde, dass in der Variablen operation nur der Rückgabewert des Funktionsaufrufs gespeichert würde. Was durch die Zuweisung geschehen ist, stellt Abbildung 2.2 graphisch dar: Zusätzlich zur vorhin implizit definierten Variable add gibt es nun eine weitere (explizit definierte) Variable operation, die auf das gleiche Funktionsobjekt zeigt.
Abbildung 2.2 Funktionen sind first class, sie können beispielsweise Variablen zugewiesen werden.
Die Funktion kann nun über beide Variablen aufgerufen werden: const result = add(2,2);
const result2 = operation(2,2);
Listing 2.3 Aufruf einer Funktion über implizite und explizite Variable
Beachten Sie hierbei aber den Hinweis aus Kapitel 1, »Einführung«: Die Eigenschaften der Ursprungsfunktion bleiben erhalten, der Name der Funktion beispielsweise lautet in beiden Fällen add. Das macht Sinn: Die neue Variable operation stellt wie die ursprüngliche Variable add lediglich eine Referenzvariable auf die Funktion mit dem Namen add dar. console.log(add.name); // Ausgabe: add
console.log(operation.name); // Ausgabe: add
Listing 2.4 Der Name einer Funktion ist unabhängig vom Variablennamen.
Funktionen in Arrays verwenden
Variablen, die auf Funktionsobjekte zeigen, können Sie an allen Stellen verwenden, an denen Sie auch »normale« Variablen verwenden dürfen. Auch der Einsatz innerhalb von Arrays ist möglich, wie folgendes Beispiel zeigt. Hierbei werden zunächst die vier Funktionen add, subtract, multiply und divide definiert und einem Array als Werte übergeben. Innerhalb der Iteration über das Array werden dann die einzelnen Funktionen aufgerufen. function add(x,y) {
return x+y;
}
function subtract(x,y) {
return x-y;
}
function multiply(x,y) {
return x*y;
}
function divide(x,y) {
return x/y;
}
const operations = [
add,
subtract,
multiply,
divide
];
let operation;
for(let i=0; i x + y;
case 'subtract': return (x, y) => x - y;
case 'multiply': return (x, y) => x * y;
case 'divide': return (x, y) => x / y;
default: return() => NaN;
}
}
Listing 2.10 Arrow-Funktionen vereinfachen die Deklaration von Funktionen. [ES2015]
In Kapitel 4, »ECMAScript 2015 und neuere Versionen«, werden wir uns Arrow-Funktionen etwas mehr im Detail anschauen.
Funktionen innerhalb von Funktionen definieren
Funktionen können auch innerhalb anderer Funktionen definiert werden. Gemeint ist hiermit nicht das Definieren einer Funktion als Methode der anderen Funktion (auch das wäre möglich), sondern die Deklaration einer Funktion lokal innerhalb des Funktionskörpers der anderen Funktion. In Listing 2.11 werden beispielsweise die vier Funktionen add(), subtract(), multiply() und divide() innerhalb der Funktion operationsContainer() definiert: function operationsContainer(x, y) {
const add = function(x, y) {
return x + y;
}
const subtract = function(x, y) {
return x - y;
}
const multiply = function(x, y) {
return x * y;
}
const divide = function(x, y) {
return x / y;
}
console.log(add(x, y));
console.log(subtract(x, y));
console.log(multiply(x, y));
console.log(divide(x, y));
}
operationsContainer(2,2);
Listing 2.11 Funktionen können innerhalb anderer Funktionen definiert werden.
Die Funktionen sind allerdings von außerhalb der Funktion operationsContainer() nicht sichtbar, können als von dort nicht direkt aufgerufen werden. Als kleiner Vorgriff auf den nächsten Abschnitt sei an dieser Stelle schon einmal verraten, dass Funktionen einen eigenen Sichtbarkeitsbereich definieren: Alles, was innerhalb einer Funktion definiert wird, ist nur innerhalb der Funktion sichtbar, es sei denn, Sie definieren etwas als global.
In Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, werde ich Ihnen noch einige Techniken vorstellen, mit denen Sie Ihre Daten auf Basis von Funktionen kapseln, aber auch, wie Sie Daten, die innerhalb einer Funktion definiert sind, nach außen zugänglich machen. Funktionen als Objektmethoden definieren
Wenn Sie eine Funktion innerhalb eines Objekts definieren, spricht man wie erwähnt von einer Methode, einer Objektmethode. Aufgerufen wird diese Methode dann über die Objektreferenz. Objektmethoden lassen sich auf folgende Weise definieren: const operations = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
},
multiply: function(x, y) {
return x * y;
},
divide: function(x, y) {
return x / y;
}
}
console.log(operations.add(2,2));
console.log(operations.subtract(2,2));
console.log(operations.multiply(2,2));
console.log(operations.divide(2,2));
Listing 2.12 Funktionen können innerhalb von Objekten definiert werden, dann spricht man von Methoden bzw. genauer von Objektmethoden.
Dem kritischen Betrachter wird auffallen, dass die Angabe des function-Schlüsselwortes eigentlich überflüssig ist. In Java beispielsweise definiert man Objektmethoden einfacher: Methodenname, geklammerte Parameter sowie geschweifte Klammern für den Methodenkörper – und der Java-Compiler
erkennt, dass es sich um eine Methode handelt. Mit ES2015 wurde eine ähnliche Syntax in JavaScript eingeführt, das heißt, Sie können Objektmethoden alternativ wie folgt definieren: const operations = {
add(x, y) {
return x + y;
},
subtract(x, y) {
return x - y;
},
multiply(x, y) {
return x * y;
},
divide(x, y) {
return x / y;
}
}
Listing 2.13 Seit ES2015 besteht die Möglichkeit, Objektmethoden ohne das Schlüsselwort »function« zu definieren. [ES2015]
2.1.2 Funktionen haben einen Kontext
Wenn Sie bereits in C# oder in Java programmiert haben, kennen Sie die dortige Bedeutung von this. Über dieses Schlüsselwort spricht man innerhalb einer Objektmethode (oder eines Konstruktors) die jeweilige Objektinstanz an, das aktuelle Objekt, eben »dieses« Objekt, das genau jenes ist, für das die Methode definiert wurde (bzw. das eine Instanz der Klasse ist, für die sie definiert wurde). In JavaScript ist das anders, was nicht selten für Verwirrung sorgt, insbesondere bei Entwicklern, die bereits Erfahrung in einer der oben genannten Sprachen haben. Die unterschiedliche Bedeutung liegt vor allem darin begründet, dass in JavaScript Funktionen selbst Objekte sind und nicht wie in Java und C# zu einem Objekt oder zu einer Klasse »gehören«. Ich habe Ihnen bisher noch nicht gezeigt wie, aber die Dynamik von JavaScript erlaubt es, Funktionen, die an einer Stelle im Code definiert sind, an ganz anderer Stelle
wiederzuverwenden, beispielsweise eine global definierte Funktion als Objektmethode oder umgekehrt eine Objektmethode als globale Funktion. Dies führt dazu, dass sich this innerhalb einer Funktion nicht auf das Objekt bezieht, in dem die Funktion definiert wurde, sondern auf das Objekt, auf dem die Funktion ausgeführt wird (Ausführungskontext). Sie können sich this ein bisschen wie eine Eigenschaft der Funktion vorstellen, die bei deren Aufruf mit dem Wert des Objekts belegt wird, auf dem sie aufgerufen wird (genauer gesagt ist this wie schon zuvor arguments sogar ein impliziter Parameter, der bei jedem Funktionsaufruf innerhalb der Funktion zur Verfügung steht). Je nachdem also, ob eine Funktion als globale Funktion oder als Methode eines Objekts aufgerufen wird, hat this einen anderen Wert. Betrachten wir dazu zunächst den einfachen Fall einer Objektmethode, in der per this eine Eigenschaft des Objekts ausgelesen wird. const person = {
firstName: 'Max', // Objekteigenschaft
getFirstName: function() {
return this.firstName;
}
}
console.log(person.getFirstName()); // Ausgabe: Max
Listing 2.14 »this« im Kontext eines Objekts bezieht sich auf das Objekt.
Die Ausgabe des Programms ist hier wie erwartet Max, denn this bezieht sich hier auf das Objekt person. Das ist intuitiv und leuchtet wahrscheinlich auch jedem bei bloßem Betrachten des Quelltextes ein. So weit also nichts Neues für C#- und JavaEntwickler.
Gehen wir einen Schritt weiter und definieren zusätzlich eine globale Funktion getFirstNameGlobal(): function getFirstNameGlobal() {
return this.name;
}
console.log(getFirstNameGlobal()); // undefined
Listing 2.15 Eine einfache globale Funktion, in der »this« verwendet wird
Rufen wir diese Funktion wie in der letzten Zeile in Listing 2.15 aufgerufen, bezieht sie sich auf den globalen Kontext. In diesem Kontext ist die Variable firstName nicht definiert, weswegen wir den Wert undefined als Rückgabewert erhalten. Dass sich this in einer globalen Funktion auf das globale Objekt bezieht, können Sie einfach testen, indem Sie eine globale Variable firstName anlegen und die Funktion getFirstNameGlobal() erneut aufrufen: firstName = 'globaler Name';
function getFirstNameGlobal() {
return this.firstName;
}
console.log(getFirstNameGlobal()); // Ausgabe: globaler Name
Listing 2.16 »this« im globalen Kontext bezieht sich auf das globale Objekt.
Das globale Objekt Das globale Objekt ist von Laufzeitumgebung zu Laufzeitumgebung verschieden. In Browsern ist das globale Objekt das window-Objekt, in Node.js ist es ein anderes. Sobald eine Funktion im globalen Scope aufgerufen wird, bezieht sich this auf das globale Objekt (außer im strikten Modus: Hier hat this innerhalb einer globalen Funktion den Wert undefined).
Im strikten Modus führt das obige Programm übrigens zu einem Fehler, da der Zugriff this.firstName aufgrund des nicht definierten this fehlschlägt: const firstName = 'globaler Name';
function getFirstNameGlobal() {
return this.firstName;
}
console.log(getFirstNameGlobal()); // Fehler: this ist nicht definiert
Listing 2.17 Im strikten Modus ist »this« im globalen Kontext undefiniert.
Nehmen wir jetzt noch zwei Objekte, die jeweils die Eigenschaft firstName definieren und die globale Funktion getFirstNameGlobal() als Objektmethode wiederverwenden. Dies erreichen Sie, indem Sie von der Objektmethode (getFirstName()) wie folgt auf die globale Funktion referenzieren: const anotherPerson = {
firstName: 'Moritz',
getFirstName: getFirstNameGlobal
}
const yetAnotherPerson = {
firstName: 'Peter',
getFirstName: getFirstNameGlobal
}
console.log(anotherPerson.getFirstName()); // Ausgabe: Moritz
console.log(yetAnotherPerson.getFirstName()); // Ausgabe: Peter
Listing 2.18 »this« bezieht sich auf den Kontext der Funktion.
Damit ist klar, dass this dynamisch bei Funktionsaufruf gesetzt wird: Die Objektmethode getFirstNameGlobal() im Kontext von anotherPerson liefert den Wert »Moritz, im Kontext von yetAnotherPerson den Wert Peter. Abbildung 2.3 stellt diesen Zusammenhang grafisch dar.
Abbildung 2.3 »this« wird dynamisch bei Funktionsaufruf ermittelt und an die aufgerufene Funktion übergeben.
Die Variable this hat also abhängig vom Kontext, in dem die Funktion aufgerufen wird, einen anderen Wert. Zusammenfassend gelten folgende Regeln: Bei Aufruf einer globalen Funktion bezieht sich this auf das globale Objekt bzw. ist im strikten Modus nicht definiert. Wird eine Funktion als Objektmethode aufgerufen, bezieht sich this auf das Objekt. Wird eine Funktion als Konstruktorfunktion aufgerufen (Details dazu in Abschnitt 2.1.5, »Funktionen als Konstruktorfunktionen«), bezieht sich this auf das Objekt, das durch den Funktionsaufruf erzeugt wird. Unachtsam programmiert, sorgen insbesondere folgende vier Fälle in der Praxis recht häufig für Laufzeitfehler (der Einfachheit halber nenne ich Funktionen, die auf this zugreifen, thisFunktion): wenn eine this-Funktion einer Variablen zugewiesen wird wenn eine this-Funktion als Callback einer anderen Funktion verwendet wird
wenn sich ein Objekt eine this-Funktion eines anderen Objekts »leiht« (Function Borrowing bzw. Method Borrowing) wenn this innerhalb einer inneren Funktion vorkommt Problematisch sind diese Fälle, weil sie oft dazu führen, dass der Ausführungskontext einer Funktion nicht dem entspricht, was man als Entwickler erwartet. In Abschnitt 2.2, »Standardmethoden jeder Funktion«, werde ich Ihnen diesbezüglich die Standardmethoden bind(), call() und apply() vorstellen, mit denen Sie den Ausführungskontext einer Funktion dynamisch definieren. 2.1.3 Funktionen definieren einen Sichtbarkeitsbereich
Im Gegensatz zu vielen anderen Programmiersprachen kennt JavaScript keinen Block-Scope für Variablen, mit anderen Worten: { und } spannen keinen Gültigkeitsbereich bzw. Sichtbarkeitsbereich für Variablen auf. Stattdessen wird der Gültigkeitsbereich von solchen Variablen durch die umgebende Funktion begrenzt. Man spricht daher auch von Function-Level-Scope: Variablen, die innerhalb einer Funktion definiert werden, sind innerhalb der gesamten Funktion sichtbar sowie innerhalb anderer (innerer) Funktionen, die in der (äußeren) Funktion definiert sind. Hinweis Dieses Verhalten gilt zumindest für Variablen, die über das Schlüsselwort var angelegt werden, weswegen wir in den folgenden Beispielen die Variablen auch über dieses Schlüsselwort erzeugen werden. Bei let sieht das etwas anders aus, wie Sie im Verlauf des Buches noch sehen werden.
Lassen Sie mich die Besonderheiten hierbei anhand einiger Codebeispiele erläutern. Dazu vorab ein paar Regeln, die beim Zugriff auf Variablen gelten: Zugriff auf Variablen, die deklariert, aber nicht initialisiert sind, ergibt den Wert undefined: function example() {
var y;
console.log(y);
}
example(); // Ausgabe: undefined
Zugriff auf Variablen, die nicht deklariert sind, führt zu einem ReferenceError: function example() {
console.log(y);
}
example(); // ReferenceError
Zugriff auf Variablen, die deklariert und initialisiert sind, ergibt (nicht anders zu erwarten und nur der Vollständigkeit halber aufgeführt) den Wert der Variablen: function example() {
var y = 4711;
console.log(y);
}
example(); // Ausgabe: 4711
Werfen Sie jetzt einen Blick auf Listing 2.19: Trotz der Tatsache, dass die Variablen y und i innerhalb der if- bzw. innerhalb der forAnweisung deklariert und initialisiert werden, kann von außerhalb der jeweiligen Codeblöcke auf beide Variablen zugegriffen werden. Ausgegeben wird zweimal der Wert 4711. function example(x) {
if(x) {
var y = 4711;
}
for(var i=0; i 80) {
return `${strings[0]}${values[0]}`;
}
return `${strings[0]}${name}${strings[1]}${age}${strings[2]}`;
}
Listing 4.104 Implementierung einer Tag-Funktion [ES2015]
Auch wenn das Beispiel mehr zu Demonstrationszwecken dient, können Sie sich leicht ausmalen, welche Möglichkeiten sich hierdurch eröffnen: Neben Validierung ist es hierüber beispielsweise möglich, eigene DSLs (Domain-Specific Languages) zu erstellen. 4.11.2 Symbole
Symbole sind eine neue Art von primitivem Datentyp und wurden hauptsächlich dazu eingeführt, eindeutige Namen für Objekteigenschaften zu definieren. Objekteigenschaften, die per Symbol definiert werden, können anschließend nur über Angabe dieses Symbols ausgelesen werden. Gegenüber der Definition von Objekteigenschaften per Zeichenkette sind somit Kollisionen untereinander ausgeschlossen. Symbole werden über die Funktion Symbol() erstellt (wichtig: nicht als Konstruktorfunktion aufrufbar). Optional erwartet die Funktion eine Beschreibung des Symbols. Listing 4.105 zeigt die Anwendung: const firstName = Symbol('Vorname');
const lastName = Symbol('Nachname');
const person = {};
person[firstName] = 'Max';
person[lastName] = 'Mustermann';
console.log(person[firstName]); // Max
console.log(person[lastName]); // Mustermann
console.log(person[0]); // undefined
console.log(person[1]); console.log(person['firstName']); console.log(person['lastName']);
// undefined
// undefined
// undefined
Listing 4.105 Symbole eignen sich beispielsweise als eindeutige Werte für Objekteigenschaften. [ES2015]
4.11.3 »for-of«-Schleife
Es2015 führt eine neue for-Schleife ein, die sogenannte for-ofSchleife. Sie unterscheidet sich von der for-in-Schleife darin, dass sie über die Namen der Objekteigenschaften über die mit diesen Eigenschaften assoziierten Werte iteriert. In der Praxis sieht das aus wie in folgendem Beispiel: const numbers = [ 1, 2, 3, 4, 5 ];
numbers.name = 'Zahlen Eins bis Fünf';
// for-in-Schleife
for (let i in numbers) {
console.log(i); // 0, 1, 2, 3, 4, name
}
// for-of-Schleife
for (let i of numbers) {
console.log(i); // 1, 2, 3, 4, 5
}
Listing 4.106 Vergleich der klassischen »for-in«-Schleife mit der neuen »for-of«-Schleife [ES2015]
4.12 Zusammenfassung und Ausblick Sicherlich kann man zu den neuen Features von ES2015 ganze Bücher füllen. Trotzdem haben Sie jetzt einen guten Überblick darüber, welche Features diese Version mit sich bringt und welche Dinge dadurch einfacher werden, die in ECMAScript 5 noch über Umwege gelöst werden mussten. Eine kurze Zusammenfassung dessen, was Sie aus diesem Kapitel mitnehmen sollten, bietet folgende Liste: Zusätzlich zu Variablen, die funktionsweit sichtbar sind, können Sie über let nun auch Variablen anlegen, die nur im aktuellen Codeblock sichtbar sind. Bisher war dies nur über das IIFEEntwurfsmuster möglich. Konstanten können über das Schlüsselwort const definiert werden. Bisher musste man sich auf Konventionen verlassen oder die Konstanten in einem eigenen Objekt bzw. Modul kapseln. ES2015 trennt strikter zwischen Funktionen und Methoden: Arrow-Funktionen sind besonders hilfreich bei der funktionalen Programmierung, Objektmethoden müssen zukünftig nicht mehr über das in diesem Zusammenhang überflüssige Schlüsselwort function definiert werden. Rest-Parameter ermöglichen eine variable Angabe von Funktionsparametern, die innerhalb der Funktion als Array zur Verfügung gestellt werden. Bisher musste man für ähnliches Verhalten das arguments-Objekt in ein Array umwandeln. Der Spread-Operator ermöglicht das Abbilden von Arrays auf Funktionsparameter. Bisher musste man hierfür die Methode
apply() auf der entsprechenden Funktion aufrufen und ihr das
Array übergeben.
ES2015 erlaubt die Angabe von Standardwerten für Funktionsparameter. Bisher verwendete man dazu den ||Operator, um innerhalb der Funktion den entsprechenden Parameter gegebenenfalls mit einem Standardwert zu belegen. Benannte Parameter erleichtern es, den Überblick bei Funktionsaufrufen zu behalten. Bisher bediente man sich hierzu des Entwurfsmusters Konfigurationsobjekt. Array Destructuring und Objekt-Destructuring erlauben es, Werte bzw. Eigenschaften aus Arrays und Objekten relativ einfach mehreren Variablen zuzuweisen. Iteratoren stellen eine Alternative zu den verschiedenen Schleifenarten dar; Generatoren ermöglichen es relativ einfach, Iteratoren für komplexe Sachverhalte zu generieren. Promises erleichtern das Schreiben von asynchronem Code, indem sie die Callback-Funktionen der asynchronen Funktion kapseln. Proxies ermöglichen es, Zugriffe auf Objekte abzufangen. Die verschiedenen neuen Arten von Datenstrukturen (Map, WeakMap, Set und WeakSet) stellen Alternativen zu normalen Arrays dar. Des Weiteren führt ES2015 zahlreiche neue Methoden für die Standardobjekte Object, String, Array, RegExp, Number und Math ein. Template-Strings sind ein mächtiges Werkzeug für die Arbeit mit Zeichenketten und ersetzen teilweise die Notwendigkeit von
Template-Bibliotheken wie Mustache.js, Handlebars.js und Jade. Symbole stellen eine neue Art primitiver Datentypen dar und ermöglichen eindeutige Namen für Objekteigenschaften. Die neue for-of-Schleife erlaubt es, über die Werte von Objekteigenschaften zu iterieren. Was die Sprachfeatures angeht, sind Sie nun bestens vorbereitet für das nächste Kapitel, in dem ich Ihnen die wichtigsten Aspekte und Tools der Entwicklung von JavaScript-Anwendungen vorstelle.
5 Der Entwicklungsprozess Die Sprache an sich zu beherrschen, ist die eine Sache. Zur professionellen Entwicklung in JavaScript gehört aber auch ein entsprechender Entwicklungsprozess. In diesem Kapitel lernen Sie die wichtigsten Aspekte des Entwicklungsprozesses von JavaScript-Anwendungen kennen. Dazu zählen die Dokumentation von Quelltext, das Generieren von Projekt-Outlines bzw. Codegerüsten (Scaffolding), der Umgang mit Styleguides sowie das Sicherstellen von Codequalität, des Weiteren Minifizierung und sogenannte Obfuscation (das Umwandeln in unleserlichen Code), Package Management und der Einsatz von Build-Tools. Für jeden dieser Aspekte stelle ich Ihnen eine Auswahl von Tools vor, die sich jeweils als sehr hilfreich erwiesen haben und sich in der JavaScript-Community großer Beliebtheit erfreuen. Dem Aspekt des Testens dagegen ist ein eigenes, hieran anschließendes Kapitel gewidmet. Zusammengenommen stelle ich Ihnen in diesem und dem folgenden Kapitel mehr als 20 Tools vor. Hierbei beschränke ich mich (nicht nur aus Platzgründen) auf die wesentlichen Aspekte des jeweiligen Tools. Mein Ziel ist es, Ihnen einen Überblick zu geben: Welche Tools gibt es? Worin liegen die Unterschiede? Wie wählen Sie das richtige Tool aus? Was sind die jeweils wichtigsten Komponenten und Möglichkeiten, die Ihnen das Tool bietet? Das Kapitel stellt also ausdrücklich kein Tutorial für die jeweiligen Tools dar (diese finden Sie zur Genüge unter anderem auf den jeweiligen Websites), sondern soll neben einem Überblick vor allem
als Entscheidungshilfe bei der Wahl des für Sie passenden Tools dienen.
5.1 Einleitung Bei der Entwicklung einer JavaScript-Anwendung spielen neben der eigentlichen Implementierung folgende Aspekte eine Rolle: Styleguides: Für JavaScript gibt es, wie für andere Sprachen auch, diverse Styleguides bezüglich des Codestils. Hierin wird festgehalten, an welche Regeln und Konventionen man sich als Entwickler halten sollte. Abschnitt 5.3 widmet sich diesem Thema. Scaffolding: Hierunter versteht man das automatische Generieren einer Projektvorlage bzw. eines Codegerüsts, das als Ausgangspunkt für die Entwicklung des Projekts dient. Im Rahmen des Scaffoldings werden beispielsweise Verzeichnisstruktur, Konfigurationsdateien etc. automatisch erzeugt, benötigte (externe) Bibliotheken automatisch als Abhängigkeiten geladen und vieles mehr. Auch wenn dieser Aspekt relativ früh im Workflow eine Rolle spielt, stelle ich Ihnen die entsprechenden Tools erst in Abschnitt 5.9, »Scaffolding«, vor. Dies hat den Grund, dass eines der dort besprochenen Tools (Yeoman) auf anderen Tools basiert, die ich Ihnen aus didaktischen Gründen vorher zeigen möchte. Kompilieren durch Präprozessoren: JavaScript selbst lässt sich zwar nicht kompilieren, immer häufiger ist JavaScript aber Produkt der Kompilierung aus einer anderen Sprache. Beispielsweise lassen sich TypeScript, CoffeeScript oder Dart durch entsprechende Präprozessoren nach JavaScript kompilieren. Vorteil der genannten Sprachen: Sie verfügen unter anderem über statische Typen, vereinfachen die Syntax und
bieten einige andere Features, die JavaScript nicht bietet. Auf Präprozessoren werde ich in diesem Kapitel allerdings nicht weiter eingehen. Hier sei auf entsprechende Literatur sowie die Websites der jeweiligen Präprozessor-Sprache verwiesen. Überprüfen der Codequalität: JavaScript hat – wie Sie in den vergangenen Kapiteln selbst gesehen haben – viele Tücken (z.B. Variablen-Hoisting, dynamischer Ausführungskontext). Deswegen ist es umso wichtiger, den eigenen Quelltext auf Fehler hin zu überprüfen. In Abschnitt 5.4, »Codequalität«, zeige ich Ihnen, welche Tools Ihnen dabei helfen. Dokumentation: Guter Code ist zwar teilweise selbsterklärend, eine Dokumentation des Quelltextes schadet trotzdem nicht. Insbesondere wenn es sich um (1) schwer verständlichen Quelltext handelt oder (2) um eine öffentliche API, die von Entwicklern verstanden werden soll, die nicht unmittelbar im Team bzw. an der Entwicklung des jeweiligen Moduls beteiligt sind. Auch in JavaScript gibt es daher die Möglichkeit, innerhalb der Kommentare gewisse Informationen zu hinterlegen, aus denen anschließend beispielsweise (ähnlich wie man es als JavaEntwickler mit dem Tool Javadoc kennt) eine HTMLDokumentation generiert werden kann. Die entsprechenden Tools für JavaScript sind Inhalt von Abschnitt 5.5, »Dokumentation«. Testen: Es gibt verschiedene Arten des Testens, die bei der Entwicklung von JavaScript-Anwendungen eine Rolle spielen: unter anderem Unit-Tests und Integrationstests. Die verschiedenen Testarten sowie einiges zu den Themen Testabdeckung und Test Doubles sowie entsprechende Tools stelle ich Ihnen im folgenden Kapitel vor.
Transpilieren: Nicht alle Laufzeitumgebungen unterstützen die aktuellsten Features von JavaScript. In solchen Fällen können sogenannte Transpiler helfen: Diese Tools kompilieren Code, der aktuelle Features nutzt, in abwärtskompatiblen Code, bei dem diese Features durch Polyfill-Techniken nachgebildet sind. Konkatenation, Minifizierung und Obfuscation: Bevor der JavaScript-Quelltext (im Fall einer Webanwendung) auf den Rechner des Clients heruntergeladen wird, ist es sinnvoll, den Code zu minimieren, um die Dateigröße klein zu halten. Zudem wird in manchen Fällen über sogenannte Obfuscation der Quelltext so verändert, dass er unleserlich wird. Beide Techniken sind Thema von Abschnitt 5.6. Package Management: Hierunter versteht man, den Quelltext in (wiederverwendbare) Pakete bzw. Module zusammenzufassen. Aus Kapitel 3, »Objektorientierte Programmierung mit JavaScript«, wissen Sie bereits, dass es verschiedene Arten gibt, Quellcode zu wiederverwendbaren Modulen zusammenzufassen: über das Module-Entwurfsmuster, über AMD oder CommonJSModules und seit ES2015 über die native Modulsyntax. Abschnitt 5.7 gibt eine Einführung in zwei der populärsten Package Manager. Abbildung 5.1 gibt einen Überblick über die Einordnung dieser Aspekte in den Gesamtworkflow. Orientiert habe ich mich dabei an dem Workflow, der in dem Buch Book of Modern frontend tooling (zu finden unter http://tooling.github.io/book-of-modern-frontendtooling) beschrieben ist.
Abbildung 5.1 Überblick über den Entwicklungsprozess
5.2 Node.js und NPM Viele der im Folgenden vorgestellten Tools sind selbst vollständig oder teilweise in JavaScript geschrieben und stehen als Node.jsKommandozeilenwerkzeug zur Verfügung. Die einfachste Art und Weise der Installation führt in diesem Fall über den sogenannten Node.js Package Manager, kurz NPM (https://github.com/npm/npm). In Abschnitt 5.7, »Package Management und Module Bundling«, werde ich Ihnen dieses Tool etwas detaillierter vorstellen. Für das weitere Verständnis dieses Kapitels ist es an dieser Stelle bereits notwendig, dass Sie wissen, wie Sie (1) NPM installieren und (2) wie Sie mit NPM eine Node.js-Anwendung installieren. 5.2.1 NPM installieren
Das Tool NPM kann auf verschiedene Weise installiert werden: Für Windows und Mac stehen MSI- bzw. PKG-Installationsdateien zur Verfügung, für andere Unix-Systeme ein entsprechendes Installationsskript. Die verschiedenen Möglichkeiten der Installation sind unter https://github.com/npm/npm ausführlich beschrieben. Viel gebräuchlicher als diese separate Installation von NPM ist aber die indirekte Installation über Node.js: Seit Version 0.6.3 ist NPM nämlich in der Installation von Node.js enthalten. Letzteres kann von der Seite http://nodejs.org/download/ als Installations- bzw. ausführbare Datei für die verschiedenen Betriebssysteme heruntergeladen werden. Nach der Installation steht Ihnen NPM als Kommandozeilenbefehl npm zur Verfügung. Die Installation können Sie anschließend überprüfen, indem Sie den Befehl npm –version ausführen.
5.2.2 Node.js-Anwendungen installieren
Der Befehl, mit dem Sie eine Node.js-Anwendung (bzw. allgemeiner ein Node.js-Package) installieren, lautet npm install (oder in Kurzform: npm i). Dieser Befehl lässt sich mit verschiedenen Parametern aufrufen; für die in diesem Kapitel vorgestellten Tools benötigen wir aber nur eine Form: npm install , wobei für den Namen des Packages (bzw. des Tools) steht, das installiert werden soll. Bei Interesse finden Sie unter https://docs.npmjs.com/cli/install eine vollständige Beschreibung aller Installationsmöglichkeiten. Eine weitere grundlegende Sache, die Sie an dieser Stelle noch mitnehmen sollten: NPM kann Packages wahlweise lokal oder global installieren. Ersteres ist sinnvoll, wenn Sie ein Node.jsPackage als Abhängigkeit installieren wollen (dazu später mehr), Letzteres dann, wenn ein Node.js-Package global verfügbar sein soll. Bei den Tools, die ich Ihnen in diesem Kapitel vorstellen werde, ist dies in der Regel der Fall: Sie sollen global von der Kommandozeile aufgerufen werden können. Um ein Package als globales Package zu installieren, übergeben Sie dem install-Befehl ein -g als Parameter. Da die globalen Packages je nach Konfiguration in einem Verzeichnis liegen, auf das Sie nur mit Administratorrechten Schreibzugriff haben, ist in solchen Fällen der Befehl unter entsprechender Kennung auszuführen, unter Unixbasierten Umgebungen beispielsweise wie folgt: sudo npm install – g . Hinweis Wenn Sie die Tools nicht global installieren möchten, können Sie sie auch lokal für ein einzelnes Projekt installieren. Dies bietet sich
beispielsweise dann an, wenn Sie für verschiedene Projekte verschiedene Versionen eines Tools verwenden wollen. Das mit NPM 5.2 eingeführte »npx« erleichtert in solchen Fällen zudem den Zugriff auf die so installierten Tools.
Noch etwas: NPM bezieht alle Node.js-Anwendungen und Packages von einer Registry-Website. Standardmäßig ist dies die URL http://registry.npmjs.org/, prinzipiell lässt sich eine angepasste Registry aber auch auf einem eigenen Server hosten. Eine Übersicht über die in der Standard-Registry verfügbaren Packages finden Sie unter https://www.npmjs.org/. Dort finden Sie derzeit (Stand: Februar 2018) rund 475.000 Pakete (zum Vergleich: im Februar 2015 waren es noch 125.000 Pakete).
5.3 Styleguides und Code Conventions Für die Entwicklung im Team ist es besonders wichtig, sich bei der Entwicklung an gewisse Regeln und Konventionen zu halten. Dies schafft ein gemeinsames Verständnis vom Code, erleichtert dessen Lesbarkeit, verhindert leidige Diskussionen über den Codestil und sorgt auf diese Weise insgesamt für eine bessere Zusammenarbeit im Team. Halten sich alle Entwickler an die gleichen Konventionen, fällt es leichter, sich in den Code von jemand anderen einzuarbeiten und diesen zu verstehen, als wenn jeder seinen eigenen Programmierstil verfolgen würde. Außerdem sind Änderungen am Quelltext im Rahmen der Versionskontrolle einfacher nachzuvollziehen, wenn als Basis ein gemeinsamer Stil verwendet wird. Regeln und Konventionen können sich von Projekt zu Projekt unterscheiden, sollten zu Beginn eines Projekts aber unbedingt im Team abgestimmt und im weiteren Verlauf des Projekts konsequent von allen Entwicklern eingehalten werden. Festgehalten werden sollten beispielsweise Aspekte wie die einheitliche Benennung von Variablen, Methoden und Objekten, Formatierung des Quelltextes (beispielsweise Einrückungen) etc. Zusammengefasst werden diese Regeln und Konventionen in Form sogenannter Styleguides, von denen sich für JavaScript mittlerweile eine ganze Reihe herausgebildet haben. Eine Auswahl der populärsten zeigt Tabelle 5.1. Wenn Sie die dort aufgeführten Styleguides einmal unter der jeweils angegebenen URL aufrufen und sich zu Gemüte führen, wird Ihnen auffallen, dass sie sich nicht in allen Punkten einig sind. Beispielsweise ziehen die Styleguides von Node.js und Google bei der Deklaration von Strings einfache Anführungszeichen vor, der Styleguide von jQuery dagegen doppelte Anführungszeichen. Style Guide Kurzbeschreibung Idiomatic.js
Link
ein relativ kompakt https://github.com/ rwldrn/idiomatic.js/ gehaltener Styleguide, der in (derzeit) zehn Kategorien einige wichtige Punkte bezüglich des Programmierstils zusammenfasst.
Style Guide Kurzbeschreibung
Link
Pragmatic.js ein Styleguide, der noch kompakter als Idiomatic.js ist
https://github.com/ madrobby/pragmatic.js
Google JavaScript Style Guide
Styleguide von https://google.github.io/styleguide/jsguide.html Google, der im Wesentlichen die Regeln in zwei Oberkategorien einsortiert: solche, die den Umgang mit Sprachmitteln selbst betreffen (z.B. weitestgehende Vermeidung von eval(), siehe Kasten), sowie solche, die eher stilistische Aspekte betreffen (z.B. Namenskonventionen)
NPM Coding Style
Der Styleguide hinter dem Node Package Manager. Besteht derzeit aus zwölf Regeln.
https://docs.npmjs.com/misc/coding-style
Node.js Style Guide
ein Styleguide, der sich auf die Entwicklung von Node.js-Modulen fokussiert, dabei aber trotzdem allgemein anwendbar bleibt
https://github.com/felixge/node-style-guide
jQuery Style der Styleguide von Guide jQuery
http://contribute. jquery.org/style-guide/js/
Style Guide Kurzbeschreibung
Link
Douglas Crockford’s Code Conventions
Codekonventionen von Douglas Crockford, auf denen auch das Tool JSLint basiert (das ich Ihnen in Abschnitt 5.4.1 vorstellen werde)
http://javascript.crockford.com/code.html
airbnb JavaScript Style Guide
relativ umfangreicher Styleguide von airbnb
https://github.com/airbnb/javascript
Dojo Style Guide
Styleguide von Dojo, der sich stark an einem bereits existierenden Styleguide für Java orientiert
http://dojotoolkit.org/referenceguide/1.10/developer/styleguide.html
JavaScript Quality Guide
Styleguide von Nicolas G. Bevacqua, dem Autor des Buches JavaScript Application Design.
https://github.com/bevacqua/js
Tabelle 5.1 Übersicht über die wichtigsten JavaScript-Styleguides
Die Methode »eval()« Der Google JavaScript Guide und die meisten anderen Styleguides ebenfalls verbieten die Verwendung der Methode eval(). Über diese Methode ist es möglich, JavaScript-Code dynamisch auszuwerten. Der JavaScript-Code wird dabei als String übergeben. Die Verwendung von eval() sollten Sie jedoch vermeiden, weil sie potenzielle Risiken birgt: Zum einen kann eval() – je nachdem, aus welcher Quelle der auszuwertende Code stammt (beispielsweise Teile von Formulareingaben) – Ziel von Scripting-Injection sein, also dem Einschleusen bösartigen Codes. Zum anderen ist die Geschwindigkeit des Codes gegenüber normal interpretiertem Code langsamer und das Debuggen schwieriger. Details hierzu finden Sie unter diesem lesenswerten Blogeintrag: http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/.
Um Ihnen einen ersten Eindruck von den in den Styleguides definierten Regeln und Konventionen zu geben, möchte ich Ihnen im Folgenden eine kurze Auswahl vorstellen. Welchen Styleguide Sie dann im Einzelfall verwenden oder ob Sie Ihren eigenen Styleguide entwerfen, bleibt selbstverständlich Ihnen überlassen. Teilweise ist bei der Auswahl der Regeln sicherlich auch der persönliche Geschmack entscheidend. Hinweis In Abschnitt 5.4, »Codequalität«, stelle ich Ihnen einige Tools vor, über die sie die Qualität von JavaScript-Code überprüfen können. Teilweise existieren für diese Tools auch Plugins für die genannten Styleguides, so dass sie automatisiert überprüfen können, ob Sie sich an die jeweils dort definierten Regeln halten.
5.3.1 Einrückungen
Ein Thema, bei dem sich die Styleguides weitestgehend einig sind, ist das der verwendeten Zeichen für Einrückungen im Code. Die meisten Styleguides bevorzugen hierbei zwei Leerzeichen statt eines Tabulator-Zeichens. Hauptgrund hierfür ist, dass der Code damit in der Codeansicht der meisten Browser besser zu lesen ist. Statt Tabulatoren wie hier … function add(x, y) {
return x + y;
}
Listing 5.1 Einrückung mit Tabulator
… verwenden Sie besser doppelte Leerzeichen wie hier: function add(x, y) {
return x + y;
}
Listing 5.2 Einrückung mit doppelten Leerzeichen
Einen Vorteil bei der Verwendung von Tabulator-Zeichen gibt es allerdings auch: Viele IDEs und Editoren erlauben es, die dargestellte Breite eines solcher Zeichen einzustellen, so dass jeder Entwickler für sich die Breite individuell dies nach seinen Vorstellungen anpassen kann. 5.3.2 Semikolons
Anweisungen sollten Sie in JavaScript immer mit einem Semikolon abschließen. Das automatische Einfügen von Semikolons durch den JavaScript-Interpreter führt an einigen Stellen zu schwierig auffindbaren Bugs wie beispielsweise in Listing 5.3: function createPerson(firstName, lastName) {
return
{
firstName: firstName,
lastName: lastName
}
}
Listing 5.3 Fehlende Semikolons
Dort fehlen die Semikolons und werden automatisch zur Laufzeit vom Interpreter eingefügt und wie folgt interpretiert: function createPerson(firstName, lastName) {
return;
{
firstName: firstName,
lastName: lastName
};
}
Listing 5.4 Vom Interpreter eingefügte Semikolons
Die Funktion macht also nicht mehr das, was sie eigentlich machen soll – der Code hinter dem return wird ignoriert. Statt des Ergebnisobjekts wird der Wert undefined zurückgegeben. 5.3.3 Anführungszeichen bei Strings
Strings können Sie in JavaScript, wie Sie wissen, sowohl über einfache als auch über doppelte Anführungszeichen definieren. Ich persönlich verwende einfache Anführungszeichen, wie in den Styleguides von Node.js und Google vorgeschlagen. Dies ist zwar für Java-Entwickler etwas gewöhnungsbedürftig (in Java werden Strings mit doppelten Anführungszeichen erstellt, einfache Anführungszeichen dagegen dienen der Definition des primitiven Datentyps char), hat aber den Vorteil, dass Sie innerhalb der so definierten Strings doppelte Anführungszeichen verwenden können, ohne diesen ein Escape-Zeichen voranstellen zu müssen. Es spielt allerdings keine wirklich entscheidende Rolle, ob Sie sich für die einfachen oder doppelten Anführungszeichen entscheiden: Wichtig ist, dass Sie die jeweilige Variante einheitlich verwenden. Vermeiden Sie Code wie folgenden … const message = 'Hallo ' + person.firstName + ", wie geht es Dir?";
… und verwenden Sie stattdessen entweder konsequent einfache Anführungszeichen … const message = 'Hallo ' + person.firstName + ', wie geht es Dir?';
… oder konsequent doppelte Anführungszeichen: const message = "Hallo " + person.firstName + ", wie geht es Dir?";
5.3.4 Variablendeklaration
Auch wenn bezüglich der Deklaration von Variablen wieder die Meinungen der verschiedenen Styleguides auseinandergehen, halte ich es für sinnvoll, bei der Deklaration mehrerer Variablen nur ein Schlüsselwort zu verwenden: const counter = 0,
artists = ['Kyuss', 'Tool'],
buy = true,
person = {};
Listing 5.5 Ein Schlüsselwort für mehrere Variablen
Und nicht wie im Node.js-Styleguide vorgeschlagen mehrere: const const const const
counter = 0;
artist = ['Kyuss', 'Tool'];
buy = true;
person = {};
Listing 5.6 Ein Schlüsselwort pro Variable
Die Schreibweise mit einem Schlüsselwort hat nämlich zwei Vorteile: Alle Variablen an einer Stelle zu deklarieren ermöglicht es, auf einen Blick zu erkennen, welche Variablen innerhalb des Funktionskontextes zur Verfügung stehen. Unbeabsichtigtem Überschreiben von Variablen, das durch Variablen-Hoisting entstehen kann (zumindest bei Verwendung von var, wenn Variablen an mehreren, unterschiedlichen Stellen im Code deklariert werden), beugen Sie so vor. Der zweite Vorteil ist rein praktischer Natur, spielt aber besonders bei clientseitigem JavaScript eine Rolle: Bevor der Code auf Clientseite ausgeführt werden kann, muss er natürlich zunächst vom Server heruntergeladen werden. Trotz schneller Internetverbindungen spielt die Dateigröße dabei nach wie vor eine große Rolle (beispielsweise wenn die entsprechende Webseite auf einem mobilen Endgerät betrachtet wird). Wenn Sie nur ein Schlüsselwort verwenden, sparen Sie ganz einfach Zeichen. Das mag unwesentlich erscheinen, macht sich in der Summe aber durchaus bemerkbar. (Anmerkung: Automatische Tools wie die in Abschnitt 5.6, »Konkatenation, Minification und Obfuscation«, besprochenen können Variablendeklarationen auch nachträglich zusammenfassen.)
5.3.5 Namenskonventionen
Bei der Namensgebung sind sich ausnahmsweise alle Styleguides (soweit sie dazu überhaupt etwas sagen) einig: Namen von Objektinstanzen und Funktionen werden in Lower-Camel-Case-Schreibweise geschrieben (z.B. name, getName()), Konstanten in Großbuchstaben (z.B. MAX) und Prototypen (bzw. emulierte Klassen) in Pascal-Casebzw. Upper-Camel-Case-Schreibweise (z.B. Person, CrawlerConfiguration). Dies dürfte den meisten C#- und Java-Entwicklern bekannt vorkommen. 5.3.6 Klammern
Klammern bei if-Anweisungen etc. sollten Sie immer verwenden. Vermeiden Sie unbedingt Code wie folgenden: if(x>5)
console.log('x ist größer als fünf.');
else
console.log('x ist kleiner oder gleich fünf.');
Listing 5.7 »if-else«-Anweisung ohne geschweifte Klammern
Stattdessen schreiben Sie entweder … if(x>5) {
console.log('x ist größer als fünf.');
} else {
console.log('x ist kleiner oder gleich fünf.');
}
Listing 5.8 »if-else«-Anweisung mit geschweiften Klammern in gleicher Zeile
… oder Folgendes: if(x>5)
{
console.log('x ist größer als fünf.');
}
else
{
console.log('x ist kleiner oder gleich fünf.');
}
Listing 5.9 »if-else«-Anweisung mit geschweiften Klammern in neuer Zeile
Die Positionierung der Klammern spielt dabei keine entscheidende Rolle. Wichtig ist lediglich, dass Sie Klammern verwenden. Der Grund: Lassen Sie die Klammern weg, kann dies insbesondere bei geschachtelten if-else-Anweisungen zu Problemen führen.
5.4 Codequalität Richtlinien und Konventionen bringen natürlich nur etwas, wenn sich jeder im Team daran hält. Doch im Getümmel des Gefechts wird das schon mal vergessen. Gut, wenn sich dann der Großteil der Richtlinien durch automatische Tools überprüfen lässt, sowohl direkt während der Entwicklung oder eben erst im Rahmen des Build-Prozesses. Im Folgenden stelle ich Ihnen die bekanntesten Tools dazu vor. 5.4.1 JSLint
JSLint (https://github.com/douglascrockford/JSLint) dient der automatischen Qualitätssicherung des Codes und ermittelt sowohl syntaktische Fehler als auch stilistische Schwachstellen, die gegen bestimmte Regeln und Konventionen verstoßen. JSLint wurde von Douglas Crockford entwickelt und ist eines der ersten Prüftools für JavaScript hinsichtlich der Codequalität (diese Art von Tools nennt man übrigens auch Linting-Tools, den Prozess des Überprüfens auch Linting). Das Tool steht vorrangig als Onlinedienst zur Verfügung, kann aber über Plugins auch in diverse Build-Tools und IDEs integriert werden und ist beispielsweise Bestandteil der WebStorm-IDE. Abbildung 5.2 gibt einen Eindruck von den (eher begrenzten) Konfigurationsmöglichkeiten von JSLint, eine entsprechende WebStorm-Konfiguration sehen Sie in Abbildung 5.3.
Abbildung 5.2 Konfiguration des JSLint-Onlinedienstes (http://www.jslint.com)
Wie Sie sehen, werden unter anderem folgende Kriterien getestet: nicht verwendete Parameter Kommentare, die ein »TODO« enthalten nicht strikter Vergleich (==) mit null Verwendung von Debugging Code Verwendung von mehreren var-Anweisungen innerhalb einer Funktion Verwendung der Funktion eval()
Abbildung 5.3 JSLint-Konfiguration in WebStorm
Mittlerweile wird in der JavaScript-Community eher von der Nutzung des Tools abgeraten, weil es sich im Vergleich zu anderen, neueren Tools recht stark an dem Programmierstil von Douglas Crockford bzw. dessen Konventionen orientiert und nur wenig konfigurieren und anpassen lässt. Wer also mit den Konventionen von Crockford nicht übereinstimmt, sollte besser ein anderes Linting-Tool verwenden. 5.4.2 JSHint
JSHint (https://github.com/jshint/jshint) ist ein Fork von JSLint, erlaubt aber eine viel feinere Konfiguration und flexiblere Anpassung dessen, was überprüft werden soll. Das Tool steht sowohl als Onlineservice zur Verfügung, über den sich Codeschnipsel validieren lassen (http://www.jshint.com), als auch als Package für Node.js. Des Weiteren existieren Plugins für diverse Build-Tools und IDEs. Abbildung 5.4 zeigt die Konfigurationsmöglichkeiten in der WebStorm-IDE.
Abbildung 5.4 JSHint-Konfiguration in WebStorm
Unter anderem können folgende Kriterien getestet werden: nicht verwendete Variablen nicht definierte Variablen nicht strikter Vergleich (==) mit null Verwendung von Debugging Code innerhalb von Schleifen definierte Funktionen Verwendung von eval() Auch wenn JSHint um einiges anpassungsfähiger als JSLint ist, erlaubt es beispielsweise nicht die Definition bzw. Implementierung eigener Regeln. Hier kommt das nächste Tool ins Spiel, der derzeitige Favorit in der JavaScript-Community. 5.4.3 ESLint
Da eine Anpassung von JSHint bezüglich der Erweiterbarkeit durch eigene Regeln nicht so ohne weiteres umsetzbar gewesen wäre,
entwickelte Nicholas C. Zakas kurzerhand das Tool ESLint (http://eslint.org). ESLint ist um einiges flexibler und anpassbarer als JSHint und JSLint. Das Tool erlaubt beispielsweise die Implementierung eigener Regeln, die dynamisch bei Bedarf während des Lintings hinzugeladen werden. Mittlerweile gibt es eine beachtliche Menge solcher Prüfregeln (http://eslint.org/docs/rules), eingeteilt in die Kategorien Fehlervermeidung, Best Practices, Strict-Mode, Variablendeklarationen, Node.js, Stilistik und ES2015. ESLint steht als Node.js-Package zur Verfügung und kann über folgendem Befehl mit Hilfe von NPM installiert werden: npm install –g eslint
Das Tool kann anschließend über den Konsolenbefehl eslint genutzt werden. ESLint konfigurieren Sie entweder direkt innerhalb der Kommentare des entsprechenden JavaScript-Quelltextes oder über eine Konfigurationsdatei im JSON-Format, die Sie als Kommandozeilenparameter übergeben. Lassen Sie die Angabe weg, sucht ESLint standardmäßig nach einer Datei mit dem Namen .eslintrc. Listing 5.10 zeigt ein Beispiel einer solchen Konfigurationsdatei: {
"env": {
"browser": false,
"node": true
},
"rules": {
"no-console": 0,
"no-debugger": 1,
"no-sparse-arrays": 2
}
}
Listing 5.10 Beispielkonfiguration von ESLint
Über die Variable env lassen sich dabei verschiedene Umgebungen angeben, die während des Lintings als gegeben vorausgesetzt werden. Im vorliegenden Beispiel sorgen die Angaben von browser: false; und node: true; dafür, dass Node.jsspezifische globale Variablen als vorhanden angesehen werden und die Verwendung solcher Variablen innerhalb des zu testenden Codes nicht zu einem Fehler führt, während browserspezifische Variablen nicht als vorhanden angesehen werden, so dass deren Verwendung wiederum entsprechende Fehlermeldungen zur Folge hat. Die konkreten Regeln konfigurieren Sie über das Objekt rules. Hierbei geben Sie immer den Namen der Regel sowie die Angabe des Status der Regel an: 0 bedeutet, die Regel ist deaktiviert, 1 bedeutet, die Regel liefert für entsprechende problematische Codestellen eine Warnung, und 2 bedeutet, die Regel liefert entsprechend einen Fehler. Überprüfen Sie beispielsweise den Code aus Listing 5.11 mit ESLint und der obigen Konfigurationsdatei, dann führt dies zu der in Listing 5.12 gezeigten Konsolenausgabe. function() {
'use strict';
console.log('Beispiel');
debugger;
const array = [,,];
console.log(array);
})();
Listing 5.11 Der zu überprüfende Quelltext script.js
7:4 warning 8:16 error
× 2 problems
Unexpected 'debugger' statement Unexpected comma in middle of array
no-debugger
no-sparse-arrays
Listing 5.12 Ausgabe von ESLint
Hinweis Für einige der in Abschnitt 5.3 vorgestellten Styleguides stehen Plugins für ESLint zur Verfügung, beispielsweise für den Styleguide von airbnb (https://www.npmjs.com/package/eslintconfig-airbnb).
5.4.4 JSBeautifier
JSBeautifier (http://jsbeautifier.org) ist ein Tool für das Bereinigen von JavaScript-Code und steht als Onlineformular (siehe Abbildung 5.5), als Python-Modul sowie als Package für Node.js (https://github.com/beautify-web/js-beautify) zur Verfügung.
Abbildung 5.5 Onlineversion von JSBeautifier
Unter anderem lassen sich über das Tool die Art der Einrückung (Tabs, Anzahl Leerzeichen), die maximale Zeilenlänge, die Positionierung der geschweiften Klammern (gleiche Zeile wie vorhergehender Code vs. neue Zeile) sowie die Positionierung der Funktionsaufrufe bei verketteten Funktionsaufrufen bestimmen. Das Node.js-Modul installieren Sie über npm install -g js-beautify, und es steht anschließend über den gleichnamigen Befehl js-
beautify zur Verfügung. Folgendes Kommando liefert dann die
bereinigte Version zu einer (oder mehreren) angegebenen JavaScript-Datei(en): js-beautify Datei.js
Das Ergebnis wird standardmäßig auf die Konsole ausgegeben. Alternativ lässt es sich aber auch direkt in eine Zieldatei (über den Parameter -f plus Name der Zieldatei) oder direkt in die Ursprungsdatei (über den Parameter -r) schreiben. Für Webentwickler ebenfalls interessant: Parallel zu js-beautify stehen nach Installation des Node.js-Packages die Befehle cssbeautify bzw. html-beautify zur Verfügung, über die sich – Sie ahnen es – CSS-Dateien respektive HTML-Dateien säubern lassen. 5.4.5 Google Closure Linter
Das Tool Google Closure Linter (https://developers.google.com/closure/utilities) wurde von Google entwickelt und wird im Rahmen der Closure Tools als Open Source zur Verfügung gestellt. Wie auch die anderen vorgestellten Tools dient Closure Linter dazu, »schmutzigen« JavaScript-Code zu finden und wie JSBeautifier optional zu bereinigen. »Schmutzig« bedeutet in diesem Fall jeglicher Code, der sich nicht an den Google JavaScript Style Guide (siehe https://google.github.io/styleguide/jsguide.html) hält. Falls Sie im Team einen anderen Styleguide verwenden, stellt das Tool also eher nicht die erste Wahl dar. Installieren lässt sich Closure Linter für alle gängigen Betriebssysteme (Linux, macOS, Windows) als Python-Modul über »Easy Install«, beispielsweise für macOS mit dem Befehl easy_install http://closure-
linter.googlecode.com/files/closure_linter-latest.tar.gz (im
Detail und für die anderen Betriebssystem nachzulesen unter https://developers.google.com/closure/utilities/docs/linter_howto.) Nach erfolgreicher Installation stehen zwei Befehle zur Verfügung: gjslint, der Codestellen ausgibt, die nicht dem Styleguide entsprechen, sowie fixjsstyle, der die gefundenen Stellen im Code direkt »repariert«. Closure Linter kann wie JSLint und JSHint in WebStorm eingebunden werden. 5.4.6 Fazit
JSLint, JSBeautifier und Closure Linter lassen sich nur relativ eingeschränkt den eigenen Wünschen und Vorstellungen von »sauberem Code« anpassen. JSLint folgt streng den Code Conventions von Douglas Crockford, Closure Linter dem JavaScript Styleguide von Google und JSBeautifier bietet auch nicht wirklich viele Anpassungsmöglichkeiten. Möchten Sie also flexibler definieren können, was überprüft werden soll, verwenden Sie besser JSHint oder ESLint.
5.5 Dokumentation Im Gegensatz zu Java, bei dem für die Generierung von Dokumentation das Tool Javadoc bereits im Java Development Kit (JDK) enthalten ist, gibt es für JavaScript kein Standardtool. In diesem Abschnitt stelle ich Ihnen daher verschiedene Tools vor, die eine Dokumentation für Ihren JavaScript-Code generieren. Ähnlich wie in Java zeichnet man den Quelltext dabei über Annotationen bzw. Tags aus, die anschließend von den Tools ausgelesen werden. Leider existiert bezüglich der zur Verfügung stehenden Tags kein Konsens unter den Tools, so dass Sie sich im Vorfeld überlegen sollten, welches Tool und welche Tags Sie verwenden. 5.5.1 JSDoc 3
JSDoc 3 (https://github.com/jsdoc3/jsdoc) macht in etwa das, was Javadoc für Java macht: Basierend auf den Kommentaren und den dort enthaltenen Tags generiert es die HTML-Dokumentation für die entsprechende Codekomponente. Die zur Verfügung stehenden Tags orientieren sich in etwa an den aus Javadoc bekannten: @author beispielsweise bezeichnet den Autor des Quelltextes, @version die Version, @param Parameter und @return den Rückgabewert einer Funktion oder Methode. Über @throws können zudem eventuell geworfene Fehler einer Funktion bzw. Methode dokumentiert werden. Hinzu kommen weitere Tags: @constructor z.B. zeichnet Konstruktorfunktionen explizit aus, @this gibt den Objekttyp an, auf den sich die Referenz this im jeweiligen Kontext bezieht, und viele mehr. /**
* Ein kleiner Taschenrechner
*
* @class Calculator
* @constructor
*/
module.exports = class Calculator {
/**
* Diese Methode addiert zwei Zahlen.
*
* @method add
* @param {Number} x Zahl 1
* @param {Number} y Zahl 2
* @return {Number} Liefert das Ergebnis der Addition von x und y.
*/
add(x, y) {
return x + y;
}
}
Listing 5.13 Beispielhafte Verwendung von Dokumentations-Tags
JSDoc 3 ist komplett in JavaScript geschrieben und kann als StandaloneVersion oder als Node.js-Package verwendet werden (die Installation erfolgt dann wie gewohnt über NPM über den Befehl npm -g install jsdoc). In jedem Fall wird eine etwas angepasste Version von Rhino als Laufzeitumgebung verwendet, die in JSDoc 3 enthalten ist. Das bedeutet aber auch, dass Java installiert sein muss, um JSDoc 3 überhaupt nutzen zu können. Wenn Sie den Code aus Listing 5.13 in der Datei Calculator.js speichern, lautet der Aufruf von JSDoc wie folgt: jsdoc Calculator.js
Die HTML-Dokumentation speichert JSDoc standardmäßig im Verzeichnis out, alternativ geben Sie das Zielverzeichnis über den Parameter -d an. Neben einer Übersichtsdatei (index.html) wird unter anderem die Datei Calculator.html generiert, die zwar relativ schlicht aussieht (siehe Abbildung 5.6), sich über Templates bzw. eigenes CSS aber nach eigenen Bedürfnissen anpassen lässt.
Abbildung 5.6 Beispiel für eine mit JSDoc 3 generierte Dokumentation
5.5.2 YUIDoc
YUIDoc (http://yui.github.io/yuidoc) ist ein Dokumentationstool, das seine Ursprünge in der von Yahoo als Open Source zur Verfügung gestellten YUIBibliothek (Yahoo User Interface) hat. Im Gegensatz zu JSDoc 3 ist YUIDoc eine reine Node.js-Anwendung, daher wird kein installiertes Java (allerdings ein installiertes Node.js) benötigt. Über folgenden Befehl installieren Sie YUIDoc: npm -g install yuidocjs
Anschließend steht das Tool über den Befehl yuidoc zur Verfügung und generiert aufgerufen im Wurzelverzeichnis des Quelltextes die entsprechende HTML-Dokumentation (siehe Abbildung 5.7).
Abbildung 5.7 Beispiel für eine mit YUIDoc generierte Dokumentation
Besonders nett: Die generierte Dokumentation verfügt über eine Suchfunktion, mit deren Hilfe sich schnell einzelne Klassen oder Module finden lassen. Ein weiteres Feature, durch das sich YUIDoc positiv von seiner Konkurrenz abhebt, ist der sogenannte Server Modus. Statt die Dokumentation via Kommandozeile oder im Rahmen des Build-Prozesses zu erzeugen, erlaubt dieser Modus eine Live-Vorschau der generierten Dokumentation während ihres Schreibens. Änderungen an Codekommentaren machen sich somit direkt bemerkbar und sorgen dafür, dass die HTMLDokumentation im Hintergrund aktualisiert wird. 5.5.3 ESDoc
Ein weiteres interessantes Tool ist ESDoc (https://esdoc.org/). Es unterscheidet sich von den anderen Tools insofern, als es in der Lage ist, die Dokumentation automatisch aus JavaScript-Klassen, die in Klassensyntax geschrieben sind, zu generieren, ohne dabei auf Tags angewiesen zu sein. Nur für einige Fälle wie die Angabe der Typen von Methodenparametern müssen wie in Listing 5.14 zu sehen, entsprechende Tags angegeben werden: export default class Person {
/**
* @param {string} firstName - First name of the person.
* @param {string} lastName - Last name of the person.
* @param {number} age - Age of the person.
*/
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
greet() {
return 'Hello';
}
toString() {
return `${this.firstName} ${this.lastName}, ${this.age} old.`;
}
get firstName() {
return this._firstName;
}
set firstName(firstName) {
this._firstName = firstName;
}
get lastName() {
return this._lastName;
}
set lastName(lastName) {
this._lastName = lastName;
}
get age() {
return this._age;
}
set age(age) {
this._age = age;
}
}
Listing 5.14 Beispielklasse als Eingabe für ESDoc
ESDoc ist selbst in JavaScript geschrieben und kann über den Befehl npm install esdoc esdoc-standard-plugin für das jeweilige Projekt lokal installiert werden. Der Aufruf erfolgt anschließend über ./node_modules/.bin/esdoc (bzw. in neueren Versionen von Node.js über npx esdoc), wobei eine Konfigurationsdatei .esdoc.json im aktuellen
Verzeichnis erwartet wird, über die mindestens das Quellverzeichnis, das Zielverzeichnis und die verwendeten Plugins angegeben werden müssen: {
"source": "./src",
"destination": "./docs",
"plugins": [{"name": "esdoc-standard-plugin"}]
}
Listing 5.15 Minimale Konfigurationsdatei für ESDoc
Nach Aufruf sieht die generierte Dokumentation für die Beispielklasse aus wie in Abbildung 5.8:
Abbildung 5.8 Von ESDoc generierte Dokumentation
5.5.4 Unterstützte Tags
Wie erwähnt unterstützt nicht jedes der Dokumentationstools alle Tags der jeweils anderen, so dass Sie sich – bevor Sie überhaupt mit der Auszeichnung des Quelltextes beginnen – für eines der Tools entscheiden sollten. Hinzu kommt, dass teilweise die gleichen Tags eine etwas andere Bedeutung haben. Tabelle 5.2 zeigt die wichtigsten Tags, die Sie relativ bedenkenlos verwenden können. Spezielle Tags finden Sie auf den jeweiligen Webseiten
der Tools. Tag
Beschreibung
@abstract
Dokumentiert, dass eine Methode in ableitenden Klassen überschrieben werden muss.
@author
Dokumentiert den Autor des entsprechenden Quelltextes.
–
@chainable
Dokumentiert, dass – eine Methode die zugehörige Objektinstanz (this) als Wert zurückgibt und somit mehrere Methodenaufrufe verkettet werden können.
–
@class
Erlaubt die Angabe einer Beschreibung der entsprechenden Klasse.
Klassen werden automatisch erkannt.
@classdesc @constructor Dokumentiert eine Funktion als Konstruktorfunktion.
JSDoc YUIDoc ESDoc –
–
–
– Konstruktoren werden automatisch erkannt.
Tag
Beschreibung
@default
Dokumentiert einen Standardwert.
@deprecated
Zeichnet den entsprechenden Quelltext als veraltet aus.
@enum
Dokumentiert eine Collection von Eigenschaften als Enum.
@event
Dokumentiert ein Ereignis.
@example
Ermöglicht die Angabe eines Beispiels, das die Verwendung des entsprechenden Quelltextes demonstriert.
@fires
Gibt an, welche Events durch eine Funktion ausgelöst werden.
@method
Zeichnet eine Funktion explizit als Methode aus.
JSDoc YUIDoc ESDoc –
–
–
–
–
–
Methoden werden automatisch erkannt.
Tag
Beschreibung
JSDoc YUIDoc ESDoc
@module
Dokumentiert ein Modul.
Module werden automatisch erkannt.
@namespace
Ermöglicht die Angabe des Namensraums.
–
@param
Dokumentiert einen Funktions- bzw. Methodenparameter.
@private
Zeichnet eine Eigenschaft oder Methode als privat aus.
@property
Zeichnet eine Eigenschaft aus.
@protected
Zeichnet eine Eigenschaft oder Methode als protected aus.
@readonly
Markiert eine Eigenschaft als nur lesend zugreifbar.
–
@requires
Dokumentiert Abhängigkeiten des entsprechenden Moduls.
–
@return
Dokumentiert den
–
Tag
Beschreibung
@returns
Rückgabewert einer Funktion oder Methode.
@since
Dokumentiert die Version, mit der der entsprechende Quelltext (bzw. das Feature, die Methode, die Funktion etc.) hinzugefügt wurde.
@static
Dokumentiert eine Komponente als statisch.
@throws
Dokumentiert, welche Fehler eine Funktion bzw. Methode werfen kann.
@type
Dokumentiert den Typ eines Objekts.
JSDoc YUIDoc ESDoc –
–
Statische Methoden werden automatisch erkannt. –
Tabelle 5.2 Übersicht über die wichtigsten Tags für die Dokumentation
5.5.5 Fazit
Meine persönlichen Favoriten der vorgestellten Dokumentationstools sind YUIDoc und ESDoc: Ersteres bietet eine benutzerfreundliche, relativ hübsch gestaltete Oberfläche, die zudem über eine Suchfunktion und
verschiedene Filterfunktionen verfügt. Letzteres überzeugt vor allem dadurch, dass es Klassen, Methoden etc. anhand der Klassensyntax automatisch erkennt und somit in vielen Fällen die Verwendung von Tags überflüssig macht. JSDoc 3 macht ebenfalls einen zuverlässigen Job, allerdings ist die generierte Dokumentation eher schlicht gehalten. Wenn Sie Zeit und Muße haben, diese durch eigenes CSS zu verschönern, sind Sie aber auch mit diesem Tool gut beraten.
5.6 Konkatenation, Minification und Obfuscation Bei der Entwicklung von clientseitigem JavaScript-Code muss man verschiedene Dinge beachten, die Sie eventuell nicht vor Augen haben, wenn man bisher eher serverseitige Komponenten entwickelt haben: Anzahl an Dateien: Vor dem Ausführen von clientseitigem JavaScript-Code muss dieser Code – logisch – erst auf Clientseite heruntergeladen werden. In der Regel passiert das im Hintergrund durch den Browser, der eine Webseite interpretiert und die entsprechend verlinkten JavaScript-Dateien herunterlädt. Das Herunterladen der Dateien geschieht jedoch standardmäßig nicht parallel (es sei denn, man definiert dieses Verhalten explizit über die seit HTML5 zur Verfügung stehenden Attribute async und defer des
Listing 6.1 Die Datei »TestRunner.html«
Schreiben des Tests
Der TDD-Methodik folgend schreibt man als Nächstes den Test, also den Inhalt der Datei ArrayHelperTest.js (siehe Listing 6.2). Jeder Testfall in QUnit wird dabei über einen Aufruf der Methode test() definiert. Der erste Parameter beschreibt in Form einer Zeichenkette die Intention des Tests. Der Text, den Sie hier angeben, sollte möglich aussagekräftig und eindeutig sein, da er später in der Darstellung der Ergebnisse verwendet wird. Als zweiten Parameter von test() geben Sie den eigentlichen Inhalt des Tests, die Testprozedur, in Form einer Callback-Funktion an. Innerhalb dieser Funktion formulieren Sie die einzelnen Assertions. In dem Beispiel soll die Methode ArrayHelper.max() die höchste Zahl des übergebenen Arrays zurückgeben. Um das sicherzustellen, wird die Assertion-Methode equal() verwendet, die innerhalb des Callbacks durch das Objekt assert zur Verfügung steht. Als ersten Parameter übergeben wir ihr das tatsächliche, als zweiten Parameter das erwartete Ergebnis. Über den dritten Parameter lässt sich zudem durch eine Zeichenkette weiterer Einfluss auf die Ergebnisdarstellung nehmen. QUnit.test('Maximum eines Zahlenarrays', (assert) => {
const numbers= [4,8,47,27,56,4,5];
assert.equal(ArrayHelper.max(numbers), 56,
'Maximum in [4,8,47,27,56,4,5] ist 56');
});
Listing 6.2 Ein einfacher Unit-Test mit QUnit
Neben equal() stellt Ihnen QUnit weitere Assertions zur Verfügung, eine Übersicht dazu gibt Tabelle 6.1. Assertion
Bedeutung
Assertion
Bedeutung
ok()
Überprüft, ob der übergebene Werte true ist.
equal()
Überprüft, ob die beiden übergebenen Werte gleich (==) sind.
notEqual()
Überprüft, ob die beiden übergebenen Werte ungleich (!=) sind.
strictEqual()
Überprüft, ob die beiden übergebenen Werte strikt gleich (===) sind.
notStrictEqual() Überprüft, ob die beiden übergebenen Werte strikt ungleich (!==) sind. deepEqual()
rekursive Überprüfung auf Gleichheit
notDeepEqual()
rekursive Überprüfung auf Ungleichheit
propEqual()
strikter Typ- und Wertvergleich der (direkten) Eigenschaften eines Objekts auf Gleichheit
notPropEqual()
strikter Typ- und Wertvergleich der (direkten) Eigenschaften eines Objekts auf Ungleichheit
throws()
Überprüft, ob eine Funktion einen Fehler wirft.
Tabelle 6.1 Übersicht der Assertions in QUnit
Ausführen des Tests
Wenn Sie die Datei TestRunner.html zum jetzigen Zeitpunkt im Browser aufrufen, werden Sie feststellen, dass der Test fehlschlägt (siehe Abbildung 6.3). Logisch, es gibt ja auch noch kein Objekt ArrayHelper. Doch das lässt sich, wie im nächsten Schritt zu sehen, schnell beheben.
Abbildung 6.3 Fehlgeschlagener Test in QUnit
Implementierung der Komponente
Die Funktionalität der Methode ArrayHelper.max() ist relativ einfach zu implementieren, wenn man auf die Methode Math.max() zurückgreift und diese wie folgt unter Verwendung des Spread-Operators aufruft: const ArrayHelper = {
max(array) {
return Math.max(...array);
}
}
Listing 6.3 Implementierung der Logik
Zur Erinnerung: apply() aufgerufen auf einer Funktion wendet diese auf die in Form eines Arrays übergebenen Parameter an. Der this-Kontext, der optional als erster Parameter übergeben werden
kann, ist für das vorliegende Beispiel nicht relevant und wird daher mit dem Wert null belegt. Erneutes Ausführen des Tests
Wenn Sie nun den Testrunner erneut starten (indem Sie einfach die Testrunner-Datei im Browser neu laden), sollte der Test erfolgreich sein und das Ergebnis mit dem aus Abbildung 6.4 übereinstimmen.
Abbildung 6.4 Bestandener Test in QUnit
Lifecycle-Methoden
Prinzipiell ist es üblich, dass alle Testfälle innerhalb eines Tests auf dem gleichen Test-Fixture arbeiten. Um auszuschließen, dass sich die einzelnen Testfälle gegenseitig beeinflussen, kann es jedoch hilfreich sein, das Test-Fixture vor jedem Testfall komplett neu zu initialisieren. Diesbezüglich gibt es im QUnit verschiedene sogenannte Test-Lifecycle-Methoden: Die Methode setup() wird vor jedem Testfall aufgerufen und dient dazu, das Test-Fixture zu erzeugen und initiale Konfigurationen durchzuführen, wie etwa das Herstellen einer Datenbankverbindung, einer Netzwerkverbindung etc. Die Methode teardown() dagegen wird nach jedem Testfall aufgerufen. Hier können Sie Testressourcen wie die angesprochenen Datenbankverbindungen und Netzwerkverbindungen wieder
freigegeben, die zuvor in der setup()-Methode in Anspruch genommen wurden. Tipp Einzelne Testfälle sollten isoliert voneinander lauffähig sein. Sie sollten nicht von den Ergebnissen anderer Tests abhängig sein.
Die Implementierung von setup() und teardown() erfolgt über die Methode module(). Generell dient diese Methode dazu, Testfälle in einzelne Gruppen zusammenzufassen. Gruppierte Testfälle werden dann im Testreport unter einem gemeinsamen Label zusammengefasst. Ein Aufruf von module('ArrayHelperTest') beispielsweise sorgt dafür, dass alle nachfolgenden Tests unter dem Label »ArrayHelperTest« aufgeführt werden. Um die Methoden setup() und teardown() zu definieren, übergibt man der Methode module() als zweiten Parameter ein Objekt, das wiederum die beiden Methoden als Eigenschaften enthält. QUnit.module('ArrayHelperTest', {
setup: function() {
this.numbers = [4,8,47,27,56,4,5];
},
zahlen : [],
teardown: function() {
this.numbers = [];
}
});
Listing 6.4 Gruppierung, Setup und Aufräumarbeiten
Alle weiteren Eigenschaften und Methoden dieses Objekts stehen zudem im Ausführungskontext nachfolgender Testfälle über this zur Verfügung. QUnit.test('Maximum eines Zahlenarrays', (assert) => {
const numbers = [4,8,47,27,56,4,5];
assert.equal(ArrayHelper.max(numbers), 56,
'Maximum in [4,8,47,27,56,4,5] ist 56');
});
QUnit.test('Minimum eines Zahlenarrays', (assert) => {
const numbers = [4,8,47,27,56,4,5];
assert.equal(ArrayHelper.min(numbers), 4,
'Minimum in [4,8,47,27,56,4,5] ist 4');
});
Listing 6.5 Zugriff auf Daten, die im Setup definiert wurden
Testen asynchronen Codes
Wie Sie wissen, läuft der große Teil einer JavaScript-Anwendung in der Regel asynchron ab. In Kapitel 2, »Funktionen und funktionale Aspekte«, haben Sie bereits gesehen, dass man Ergebnis und eventuell auftretende Fehler einer asynchronen Funktion über Callbacks an den aufrufenden Code weitergibt. Betrachten Sie dazu als Beispiel folgende einfache asynchrone Methode: const DownloadManager = {
download(url, callback) {
setTimeout(() => {
// Hier normalerweise Download einer echten Datei.
callback('Inhalt der Textdatei');
}, 2000);
}
}
Listing 6.6 Eine einfache asynchrone Funktion
Um diese Methode zu testen und beispielsweise das Ergebnis zu überprüfen, müssen Sie QUnit zunächst über den Aufruf assert.async() mitteilen, dass Sie eine asynchrone Funktion testen möchten. Intern sorgt QUnit dann dafür, dass der Unit-Test erst beendet wird, wenn die von assert.async() zurückgegebene Funktion (im Beispiel done()) aufgerufen wurde: QUnit.module('DownloadManagerTest');
QUnit.test('Download einer Textatei', (assert) => {
let done = assert.asnyc();
DownloadManager.download('http://www.example.com/example.txt',
(content) => {
assert.equal(content, 'Inhalt der Textdatei');
done();
});
});
Listing 6.7 Testen einer asynchronen Funktion in QUnit
Auf diese Weise ist es möglich, auf das Ergebnis einer asynchronen Funktion zu warten und dieses wie gewohnt über Assertions zu prüfen. Erst der Aufruf von done() sorgt dafür, dass der Testrunner weiter ausgeführt wird. Über den Aufruf von assert.timeout() können Sie zudem die Anzahl an Millisekunden angeben, die QUnit maximal warten soll, bevor der Test abgebrochen wird. Über QUnit.config.testTimeout() lässt sich die Einstellung auch global für alle Tests definieren. Hinweis Zu Beginn hatte ich ja gesagt, dass QUnit sich besonders für das Testen von DOM-bezogenem Code eignet. In den bisherigen Beispielen ist davon noch nichts zu sehen. Aber keine Sorge, in Abschnitt 6.4, »DOM-Tests«, schauen wir uns dieses Thema etwas näher an.
6.1.4 mocha
mocha (http://mochajs.org) ist ein Test-Framework, das sich unabhängig von der verwendeten Laufzeitumgebung (Browser, Node.js) einsetzen lässt. Es unterscheidet sich von QUnit im Wesentlichen in zwei Punkten: Zum einen enthält es keine Assertion-Funktionalität, das heißt, Sie müssen dafür zusätzlich eine externe Bibliothek, wie beispielsweise should.js (https://github.com/shouldjs/should.js), expect.js
(https://github.com/LearnBoost/expect.js) oder chai.js (http://chaijs.com) einbinden. Zum anderen können die einzelnen Testfälle wahlweise in TDD-Schreibweise (wie in QUnit) oder in sogenannter BDD-Schreibweise formuliert werden. BDD steht für Behaviour-Driven Development (bzw. verhaltensgetriebene Entwicklung) und bezeichnet wie TDD eine Methodik der agilen Softwareentwicklung. Im Unterschied zu TDD werden in BDD die Tests jedoch so formuliert, dass sie sich wie Anforderungen bzw. wie eine Spezifikation lesen. Falls Sie mocha für den Einsatz im Browser verwenden möchten, gehen Sie ähnlich vor wie bei QUnit: Sie erstellen eine HTMLTestrunner-Datei, binden eine JavaScript- und eine CSS-Datei aus der mocha-Bibliothek sowie die entsprechenden Unit-Tests ein (Details hierzu finden Sie unter http://mochajs.org/#browser-support.) Im Folgenden möchte ich Ihnen mocha jedoch anhand der Kommandozeile vorstellen. Dazu installieren Sie es wie folgt über NPM: sudo npm install -g mocha
Anschließend steht Ihnen der Befehl mocha zur Verfügung. Doch bevor ich Ihnen zeige, wie Sie mit diesem Befehl die Testausführung starten, möchte ich Ihnen zeigen, wie Sie in mocha überhaupt UnitTests mit der BDD-Schreibweise definieren. Schreiben des Tests
Die Basis eines Unit-Tests bildet im BDD-Stil die Funktion describe(), über die eine Gruppe von Tests eingeleitet wird. Der erste Parameter steht für den Namen der Gruppe, der zweite Parameter definiert eine Callback-Funktion, die den eigentlichen Test oder weitere describe()-Aufrufe, sprich Untergruppen von
Tests, enthält. Einen einzelnen Test definieren Sie über die Funktion it(). Über den ersten Parameter lässt sich hierbei eine Beschreibung des Tests angeben, als zweiten Parameter eine Callback-Funktion, die die eigentliche Testprozedur enthält. Als Assertion-Bibliothek bietet sich standardmäßig das Package »assert« (https://nodejs.org/api/assert.html) an, das bereits Bestandteil von Node.js ist und somit nicht extra installiert werden muss. Folgendes Beispiel zeigt exemplarisch den Aufbau eines typischen Unit-Tests in BDD-Schreibweise. Getestet werden soll hier die Klasse ArtistRepository, über das einzelne Künstler verwaltet werden sollen. So soll es unter anderem möglich sein, Künstler hinzuzufügen und wieder zu löschen. Dazu soll das Objekt vorerst zwei Methoden bereitstellen: add() und clearAll(). Hinzu kommt, dass keine Künstler mit gleichem Namen enthalten sein dürfen, sprich, Versuche, einen Künstler doppelt hinzuzufügen, sollen scheitern. Des Weiteren wird eine Methode getAll() vorausgesetzt, die alle Künstler aus dem Repository als Array zurückgibt. Künstler als solche werden der Einfachheit halber als simple Objekt-Literale repräsentiert. // Einbinden des zu testenden Objekts
const ArtistRepository = require('../src/ArtistRepository');
// Einbinden der Assertion-Bibliothek
const assert = require("assert");
describe('ArtistRepository', () => {
describe('add()', () => {
it('should add the artist and increase the number of all artists',
() =>{
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Dragontears'});
artistRepository.add({name : 'Kyuss'});
assert.equal(artistRepository.getAll().length, 2);
});
it('should add the artist only if it is not already there', () => {
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Kyuss'});
artistRepository.add({name : 'Kyuss'});
assert.equal(artistRepository.getAll().length, 1);
});
});
describe('clearAll()', () => {
it('should clear all artists', () => {
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Monster Magnet'});
assert.equal(artistRepository.getAll().length, 1);
artistRepository.clearAll();
assert.equal(artistRepository.getAll().length, 0);
});
});
});
Listing 6.8 Unit-Test mit mocha
BDD-Formulierung Im obigen Beispiel sind die Formulierungen der Tests stark an die Objekte und Methoden gebunden: So gibt es eine Testgruppe ArtistRepository mit den zwei Untergruppen add() und clearAll(). Erst die Formulierungen bei den einzelnen Tests sind wirklich im BDD-Stil verfasst. Ich habe mich hierbei an dem Aufbau der Beispiele auf der mocha-Website (http://mochajs.org/#synchronous-code) orientiert. Generell können Sie mit BDD aber noch weiter gehen und die Tests nicht an Objekten und Methoden orientieren, sondern an Szenarien. Für das obige Beispiel würden die Formulierungen dann beispielsweise wie folgt lauten: describe('Adding two it('should add the () => {
/* ..... */
});
});
describe('Adding one it('should not add /* ..... */
});
});
artists to the AssertionRepository', () => {
artists and increase the number of all artists',
artist twice to the AssertionRepository', () => {
the artist the second time', function () {
Listing 6.9 Striktere BDD-Formulierung
Welches Vorgehen Sie wählen, ist natürlich Ihnen überlassen. In der Praxis eignet sich die striktere BDD-Formulierung immer dann, wenn man mehrere Objekte im Zusammenspiel innerhalb eines Unit-Tests testen möchte.
Implementierung der Komponente
Da das Ausführen des Tests jetzt noch fehlschlägt, lassen Sie mich Ihnen kurz die Implementierung der Klasse ArtistRepository zeigen. Sie ist nicht sonderlich kompliziert: Die Klasse enthält die durch den Test geforderten Methoden add(), getAll() und clearAll() sowie eine Methode contains(), die intern verwendet wird, um zu überprüfen, ob ein Künstler schon im Repository enthalten ist. module.exports = class ArtistRepository {
constructor() {
this.artists = [];
}
add(artist) {
if (!this.contains(artist)) {
if (artist && artist.name) {
this.artists.push(artist);
} else {
throw new Error('Wrong artist format.');
}
}
}
contains(newArtist) {
return this.artists.filter(
artist => artist.name === newArtist.name
).length > 0;
}
getAll(artist) {
return this.artists;
}
clearAll() {
this.artists = [];
}
}
Listing 6.10 Der zu testende Quelltext
Erneutes Ausführen des Tests
Das Ausführen der Unit-Tests geschieht über den Befehl mocha. Ohne spezielle Parameter sucht mocha diese standardmäßig in einem Verzeichnis mit dem Namen test und führt alle dort enthaltenen Tests aus. Alternativ können Sie auch einen konkreten Pfad oder ein Muster angeben, beispielsweise mocha test/ArtistRepositoryTest.js oder mocha test/**/*Test.js, um gezielt einzelne oder eine Gruppe von Tests auszuführen. Hinweis Den Testbefehl können Sie auch in den scripts-Bereich in der Datei package.json unter dem Namen test aufnehmen. Anschließend können Sie die Tests über den Befehl npm run test (bzw. in diesem Fall auch einfach nur npm test) ausführen, was in der Regel einfacher ist, als jedes Mal den kompletten Testbefehl einzugeben. {
...
"scripts": {
"test": "mocha test/**/*Test.js"
}
...
}
Listing 6.11 Testbefehl innerhalb der package.json-Datei
Hinweis
Statt die Testdatei ArtistRepositoryTest.js zu nennen, können Sie auch der Konvention folgen und ein ».test« hinter den Namen der zu testenden Klasse hängen, im vorliegenden Beispiel also ArtistRepository.test.js. Bei BDD ist zudem die Endung »spec« gebräuchlich: ArtistRepository.spec.js.
mocha unterstützt verschiedene Arten sogenannte Reporter, die für die Generierung der Testergebnisse verantwortlich sind. Definieren lässt sich ein Reporter über den Parameter -R. Die verfügbaren Reporter entnehmen Sie bitte der Website (http://mochajs.org/#reporters). Für die Praxis besonders geeignet ist der Reporter spec: mocha -R spec
Dieser gibt die Ergebnisse des Tests übersichtlich auf der Kommandozeile aus: ArtistRepository
#add()
should add the artists and increase the number of all artists
should add the artist only if it is not already there
#clearAll()
should clear all artists
3 passing (9ms)
Listing 6.12 Ausgabe des spec-Reporters
Neben einigen Reportern, die eher in die Kategorie »ganz nett« fallen (Ausgabe in Neonfarben, Ausgabe einer ASCII-Fluglandebahn, auf der das landende Flugzeug den Testfortschritt repräsentiert, etc.) und in der Praxis wohl eher nur dem Zeitvertrieb dienen, gibt es eine Reihe weiterer, durchaus nützlicher Reporter. Tabelle 6.2 zeigt eine Auswahl. Reporter Beschreibung
Reporter Beschreibung dot
minimalistische Ausgabe, bei der einzelne Tests als Punkte auf der Konsole ausgegeben werden
spec
Ausgabe, die sich wie eine Spezifikation liest
TAP
Ausgabe im TAP-Format (Test Anything Protocol), siehe http://en.wikipedia.org/wiki/Test_Anything_Protocol, das beispielsweise die Integration in CI-Systeme ermöglicht
list
ähnlich wie spec, nur dass die einzelnen Tests nicht hierarchisch, sondern als Liste ausgegeben werden
json
Ausgabe im JSON-Format
json-cov
Ausgabe im JSON-Format, das zusätzlich Informationen zur Testabdeckung enthält, sofern eine entsprechende Code-Coverage-Bibliothek eingebunden wurde (siehe Abschnitttt 6.3, »Testabdeckung«)
html-cov
Ausgabe im HTML-Format, das zusätzlich Informationen zur Testabdeckung enthält, sofern eine entsprechende Code-Coverage-Bibliothek eingebunden wurde (siehe Abschnitt 6.3)
min
minimale Ausgabe, bei der lediglich das Gesamtergebnis aller Tests ausgegeben wird
doc
Ausgabe ähnlich einer HTML-Dokumentation, die Testgruppen und Tests in -Elementen schachtelt
Reporter Beschreibung html
Reporter, der automatisch verwendet wird, wenn die Testausführung von mocha im Browser durchgeführt wird. Auf der Konsole ausgeführt, führt dieser Reporter zu einem Fehler, da bestimmte browserspezifische JavaScript-Objekte wie document dort standardmäßig nicht zur Verfügung stehen.
Tabelle 6.2 Auswahl verfügbarer Reporter
Alternative Assertion-Bibliotheken Eine interessante alternative Assertion-Bibliothek, die sich in mocha integrieren lässt, ist das vorhin erwähnte should.js. Installieren lässt sich die Bibliothek als lokale Abhängigkeit im jeweiligen Projekt über den Befehl npm install should --savedev. Anschließend kann die Bibliothek über require() innerhalb des mocha-Tests eingebunden werden. Das interessante an should.js ist, dass es Object.prototype um einige zusätzliche testspezifische Methoden erweitert, wodurch in Folge (fast) auf allen Objekten diese zusätzlichen Methoden aufgerufen werden können und sich – wie Sie in Listing 6.13 sehen – äußerst sprechende und leserliche Assertions formulieren lassen. const ArtistRepository = require('../src/ArtistRepository');
const should = require('should');
describe('ArtistRepository', () => {
describe('add()', () => {
it('should add the artist and increase the number of all artists',
() => {
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Dragontears'});
artistRepository.add({name : 'Kyuss'});
artistRepository.should.be.instanceof(ArtistRepository);
artistRepository.getAll().length.should.eql(2);
artistRepository.getAll()[0].name.should.eql('Dragontears');
artistRepository.getAll()[1].name.should.eql('Kyuss');
});
});
});
Listing 6.13 Verwendung der Assertion-Bibliothek should.js
Zu beachten: Objekte, die mit Object.create() erzeugt wurden, erben nicht die Eigenschaften aus Object.protoype und müssen über den Aufruf should(object) zunächst umgewandelt werden, damit die entsprechenden AssertionMethoden verwendet werden können: const person = Object.create({
name: 'max'
});
should(person).have.property('name', 'max');
Neben should.js sind expect, das mittlerweile Teil von Jest ist, und chai (http://www.chaijs.com/) zwei sehr bekannte und häufig verwendete Assertion-Bibliotheken.
Testen von asynchronem Code
mocha unterstützt wie QUnit das Testen von asynchronem Code. Gesteuert wird das über einen optionalen Parameter der CallbackFunktion, die der it()-Funktion übergeben wird. Bei diesem Parameter handelt es sich um eine Funktion, die aufgerufen werden muss, um den Test zu beenden. Taucht dieser (üblicherweise mit done benannte) Parameter in der Definition der Funktion auf, wartet mocha intern so lange, bis diese Callback-Funktion aufgerufen oder ein (konfigurierbarer) Timeout erreicht wurde, bevor ein Testfall abgeschlossen wird. Als Beispiel nehmen wir wieder das Objekt DownloadManager aus Abschnitt 6.1.3, »QUnit«, fügen diesem aber eine Zeile hinzu, um es mit der CommonJS-Modulspezifikation konform zu machen, so
dass es sich unter Node.js als Modul und im mocha-Test als Import verwenden lässt: const DownloadManager = {
download(url, callback) {
setTimeout(() => {
callback('Inhalt der Textdatei');
}, 2000);
}
}
module.exports.DownloadManager = DownloadManager;
Listing 6.14 Der an CommonJS angepasste »DownloadManager«
Nun ist es möglich, das Objekt per require() innerhalb des mocha-Tests einzubinden. Das und wie der Test der asynchronen download()-Methode funktioniert, zeigt Listing 6.15: const { DownloadManager } = require('../src/DownloadManager');
const assert = require("assert");
describe('DownloadManager', () => {
describe('download()', () => {
it('should download the content of a text file', (done) => {
DownloadManager.download('http://www.example.com/example.txt',
(content) => {
assert.equal(content, 'Inhalt der Textdatei');
done();
});
});
});
});
Listing 6.15 Asynchronen Code in mocha testen
Im Beispiel simuliert DownloadManager einen Download, der 2 Sekunden lang dauert. So lange wartet mocha standardmäßig nicht auf das Abschließen einer asynchronen Funktion und bricht mit einem Timeout-Fehler den Test ab. In diesem Fall ist es daher notwendig, den Timeout von mocha zu erhöhen. Dies erreichen Sie über den Parameter -t. Ihm übergeben Sie die Anzahl an Millisekunden, die ein Test warten soll, bevor er durch einen Timeout-Fehler beendet wird: mocha -R spec -t 8000.
Lifecycle-Methoden
Ähnlich wie QUnit bietet auch mocha verschiedene Test-LifecycleMethoden an, innerhalb derer sich Konfigurations- oder Aufräumarbeiten durchführen lassen. Die Methode before() wird einmal vor allen Tests innerhalb einer Testgruppe aufgerufen, die Methode after() einmal nach allen Tests innerhalb einer Testgruppe. Die Methoden beforeEach() und afterEach() hingegen werden vor bzw. nach jedem einzelnen Test in einer Testgruppe aufgerufen. describe('ArtistRepository', () => {
let artistRepository;
beforeEach(() => { // Vor jedem Test
artistRepository = new ArtistRepository();
});
afterEach(() => {}); // Nach jedem Test
before(() => {}); // Vor allen Tests
after(() => {}); // Nach allen Tests
describe('add()', () => {
it('should add the artist and increase the number of all artists',
function() {
artistRepository.add({name : 'Dragontears'});
artistRepository.add({name : 'Kyuss'});
assert.equal(artistRepository.getAll().length, 2);
});
...
});
...
});
Listing 6.16 Lifecycle-Methoden in mocha
TDD in mocha Wie erwähnt ist es mit mocha auch möglich, die Tests im TDD-Stil zu verfassen. Dafür stehen folgende Methoden zur Verfügung: suite(), die eine Gruppe von Tests definiert, test(), die einen einzelnen Test innerhalb einer Gruppe definiert, sowie setup() und teardown() als Lifecycle-Methoden.
Integration in WebStorm
WebStorm (siehe Abschnitt 1.5.1, »IntelliJ WebStorm«) unterstützt verschiedene JavaScript-Bibliotheken und -Tools. So sind unter anderem Node.js und mocha sehr gut integriert. Für beides lassen sich einzelne Run Configurations anlegen (siehe Abbildung 6.5) und direkt aus WebStorm heraus aufrufen. Im Fall von mocha werden Testausführung und Testergebnisse zudem grafisch in einem eigenen Fenster visualisiert (siehe Abbildung 6.6).
Abbildung 6.5 Konfiguration einer Run Configuration für mocha in WebStorm
Abbildung 6.6 mocha-Integration in WebStorm
6.1.5 Jest
Ein weiteres Testing-Framework, das in den letzten Jahren an Bekanntheit gewonnen hat, ist Jest (https://facebook.github.io/jest/), as von Facebook entwickelt wird. Es verfügt über eine eigene Assertion-API (über die bereits erwähnte Assertion-Bibliothek expect), die Möglichkeit, die Testabdeckung zu ermitteln (siehe Abschnitt 6.3, »Testabdeckung«), sowie Mocking-Features (siehe Abschnitt 6.2, »Test-Doubles«) und bietet damit von Haus aus schon mehr als QUnit oder mocha. Zudem erlaubt es sogenanntes Snapshot-Testing (https://facebook.github.io/jest/docs/en/snapshot-testing.html), mit Hilfe dessen sich unterschiedliche Zustände von User Interfaces anhand von Screenshots vergleichen und testen lassen. Damit bietet Jest insgesamt von Haus aus schon mehr als QUnit oder mocha. Jest lässt sich über npm install --save-dev jest installieren und kann anschließend über den Befehl jest aufgerufen werden. Prinzipiell können Tests in Jest auf die gleiche Weise wie in mocha definiert werden, das heißt, die Tests aus den entsprechenden Listings funktionieren genauso auch mit Jest (allerdings müssen dann die Testdateien mit dem Dateisuffix .test.js enden, das standardmäßig von Jest erwartet wird). Für die Definition von Assertions lässt sich zwar ebenfalls das Package assert verwenden, allerdings bietet sich die Verwendung der hauseigenen AssertionAPI an, die über expect zur Verfügung steht und über die sich Assertions über sogenannte Matcher sehr aussagekräftig formulieren lassen: // Einbinden des zu testenden Objekts
const ArtistRepository = require('../src/ArtistRepository');
describe('ArtistRepository', () => {
describe('add()', () => {
it('should add the artist and increase the number of all artists',
() =>{
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Dragontears'});
artistRepository.add({name : 'Kyuss'});
expect(artistRepository.getAll().length).toBe(2);
});
it('should add the artist only if it is not already there', () => {
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Kyuss'});
artistRepository.add({name : 'Kyuss'});
expect(artistRepository.getAll().length).toBe(1);
});
});
describe('clearAll()', () => {
it('should clear all artists', () => {
const artistRepository = new ArtistRepository();
artistRepository.add({name : 'Monster Magnet'});
expect(artistRepository.getAll().length).toBe(1);
artistRepository.clearAll();
expect(artistRepository.getAll().length).toBe(0);
});
});
});
Listing 6.17 Unit-Test mit Jest
Neben dem im Listing verwendeten Matcher toBe() stehen weitere Matcher wie toEqual(), toBeDefined() und toBeNull() zur Verfügung. Darüber hinaus ist es möglich, eigene Matcher zu implementieren, so dass sich auch Assertions wie expect(person).toHaveFirstName('Max') oder expect(person).toHaveLastName('Mustermann')
definieren lassen, wodurch die Tests um einiges lesbarer werden. 6.1.6 Weitere Frameworks
Neben QUnit, mocha und Jest existiert eine ganze Reihe weiterer Test-Frameworks. Zu den bekannteren zählen Jasmine (https://jasmine.github.io/), node-tap (http://www.node-tap.org/), AVA (https://github.com/avajs/ava) und tape (https://github. com/substack/tape). 6.1.7 Integration in Build-Tools
Der Prozess des Testens sollte fester Bestandteil des gesamten Workflows bei der Entwicklung einer Anwendung sein. Insofern ist es sinnvoll, die Tests nicht nur händisch über Konsole oder IDE auszuführen, sondern über Build-Tools in den Workflow zu integrieren. In diesem Abschnitt zeige ich Ihnen, wie sich QUnit, mocha und Jest in die beiden aus Abschnitt 5.8, »Building«, bekannten Tools Grunt und Gulp integrieren lassen. QUnit in Grunt
Im Fall von QUnit installieren Sie das Grunt-Plugin grunt-contribqunit (https://github.com/gruntjs/grunt-contrib-qunit) lokal im Projekt über npm install grunt-contrib-qunit --save-dev. Voraussetzung für eine saubere Installation ist dabei eine vorhandene package.js-Datei. Die Konfiguration innerhalb der Datei Gruntfile.js erfolgt wie gewohnt in drei Schritten: Einbinden des Plugins, anschließende Konfiguration sowie Registrieren des Tasks (siehe Listing 6.18). 'use strict';
module.exports = (grunt) => {
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.initConfig({
qunit: {
all: ['Testrunner.html']
}
});
grunt.registerTask('default', 'qunit');
};
Listing 6.18 Integration von QUnit in Grunt
Wie Sie Listing 6.18 entnehmen können, reicht für die Konfiguration die Angabe der Testrunner-Datei(en). Standardmäßig geben Sie hier die entsprechenden Dateipfade an, es lassen sich aber auch URLs konfigurieren. In beiden Fällen werden die Testrunner über
PhantomJS ausgeführt, das ich Ihnen noch detaillierter in Abschnitt 6.5.1 vorstellen werde. Der konfigurierte Task lässt sich über grunt qunit oder – da dieser Task in der Konfigurationsdatei als Default-Task konfiguriert wurde – über grunt ausführen. Die Konsolenausgabe sieht dann aus wie folgt: Running "qunit:all" (qunit) task
Testing Testrunner.html ..OK
>> 2 assertions passed (22ms)
Done, without errors.
PhantomJS PhantomJS ist ein sogenannter Headless Browser, sprich ein Browser ohne grafische Oberfläche, der aber trotzdem in der Lage ist, Webseiten wie ein »richtiger« Browser zu rendern. Das Praktische an der Verwendung eines Headless Browsers ist, dass Sie Ihre Tests ausführen können, ohne einen richtigen Browser zu öffnen. Neben dem Ausführen eines HTML-basierten Testrunners ermöglicht dies auch ein serverseitiges Ausführen von Tests, die Abhängigkeiten zum DOM haben (siehe Abschnitt 6.4, »DOMTests«).
mocha in Grunt
Für mocha stehen gleich mehrere Grunt-Plugins zur Auswahl: grunt-mocha (https://github.com/kmiyashiro/grunt-mocha), das clientseitige mocha-Tests ebenfalls unter Verwendung von PhantomJS ausführt, sowie grunt-simple-mocha (https://github.com/yaymukund/grunt-simple-mocha) und gruntmocha-test (https:// github.com/pghalliday/grunt-mocha-test), die
beide jeweils serverseitige mocha-Tests ausführen. Ich möchte Ihnen an dieser Stelle exemplarisch Letzteres vorstellen. Die Installation erfolgt wie gewohnt über NPM: npm install grunt-mocha-test --save-dev
Listing 6.19 zeigt die entsprechende Grunt-Konfiguration. Der Task für mocha nennt sich »mochaTest« und unterstützt prinzipiell die gleichen Parameter wie mocha auf der Kommandozeile. 'use strict';
module.exports = (grunt) => {
grunt.loadNpmTasks('grunt-mocha-test');
grunt.initConfig({
mochaTest: {
test: {
options: {
timeout: 8000,
reporter: 'spec'
},
src: ['test/**/*.js']
}
}
});
grunt.registerTask('default', 'mochaTest');
};
Listing 6.19 Integration von mocha in Grunt
Mit dem Befehl grunt mochaTest (bzw. grunt) lässt sich der definierte Task ausführen. Das Ergebnis mit dem spec-Reporter erzeugt die schon von eben bekannte Ausgabe: ArtistRepository
#add()
should add the artists and increase the number of all artists
should add the artist only if it is not already there
#clearAll()
should clear all artists
DownloadManager
#download()
should download the content of a text file (2002ms)
4 passing (2s)
Done, without errors.
QUnit in Gulp
Mit Hilfe des Gulp-Plugins gulp-qunit (https://github.com/jonkemp/gulp-qunit) lässt sich QUnit als Task innerhalb von Gulp nutzen. Auch dieses Plugin verwendet PhantomJS als Headless Browser und ermöglicht somit eine serverseitige Ausführung von Tests mit Abhängigkeiten zum DOM. Installiert wird gulp-qunit lokal über den Befehl npm install gulpqunit --save-dev. Listing 6.20 zeigt die entsprechende Gulp-Konfiguration gulpfile.js. Hierbei wird über src() die Testrunner-Datei eingelesen und per pipe() and QUnit weitergeleitet. Über den Befehl gulp test führen Sie den Task anschließend aus. const gulp = require('gulp');
const qunit = require('gulp-qunit');
gulp.task('test', () => {
return gulp.src('./TestRunner.html').pipe(qunit());
});
Listing 6.20 Integration von QUnit in Gulp
mocha in Gulp
Das Plugin gulp-mocha (https://github.com/sindresorhus/gulpmocha) ermöglicht die Integration von mocha in Gulp. Installieren können Sie es über den Befehl npm install --save-dev gulp-mocha. Listing 6.21 zeigt die – wahrscheinlich selbsterklärende – Konfiguration: const gulp = require('gulp');
const mocha = require('gulp-mocha');
gulp.task('mocha', () => {
return gulp.src('test/*.js')
.pipe(
mocha({
reporter: 'spec',
timeout: 8000
}
)
);
});
Listing 6.21 Integration von mocha in Gulp
6.2 Test-Doubles Häufig ist es so, dass die zu testende Komponente eine oder mehrere Abhängigkeiten zu anderen Komponenten aufweist. Im Jargon der testgetriebenen Entwicklung nennt man diese Abhängigkeiten Dependent-On Components, kurz DOC. Dies können beispielsweise Anfragen an eine Datenbank oder an einen Webservice sein. Solche Abhängigkeiten erschweren das isolierte Testen einer Komponente aus verschiedenen Gründen: Zum einen müssen die Abhängigkeiten während der Durchführung eines Tests zur Verfügung stehen (Datenbank vorhanden und Verbindung aufgebaut, Webservice gestartet etc.). Zum anderen sind die Tests oft abhängig von den Daten, die von der Abhängigkeit geliefert werden, oder erwarten oft bestimmte Daten, mit denen sie arbeiten können. Im Fall einer Datenbank als Abhängigkeit hieße das beispielsweise, dass entsprechende Testdaten enthalten sein müssten. Zu diesem Zweck werden zwar häufig Testdatenbanken eingesetzt, die in der Setup-Phase mit bestimmten Werten initialisiert und anschließend in der Teardown-Phase wieder in den Ursprungszustand gebracht werden. Allerdings ist der dazu betriebene Aufwand im Verhältnis sehr hoch, es können leichter Seiteneffekte auftreten, und die Ausführungsdauer des jeweiligen Tests steigt durch die zusätzlich auszuführenden Schritte. Bei der testgetriebenen Entwicklung möchte man aber in der Regel schnelles Feedback bekommen. Was man anstrebt, sind also Tests, die sich schnell ausführen lassen. Um den genannten Problemen entgegenzuwirken, ersetzt man während des Unit-Tests die Abhängigkeiten der zu testenden
Komponente (des SUTs) durch sogenannte Test-Doubles (siehe Abbildung 6.7).
Abbildung 6.7 Das Prinzip von Test-Doubles
Diese dienen in allererster Linie dazu, eine zu testende Komponente während des Tests weitestgehend unabhängig vom Gesamtsystem zu machen, um sie besser in Isolation testen zu können. TestDoubles werden üblicherweise in der Setup-Phase des entsprechenden Tests erstellt und konfiguriert. Dabei reicht es, dem Test-Double lediglich die Funktionalität hinzuzufügen, die später während des Tests abgerufen wird. Angenommen, Ihre zu testende Komponente ruft eine einzelne Methode eines Webservices auf, der prinzipiell noch über weitere Methoden verfügt. Dann reicht es für die Implementierung des TestDoubles, nur diese eine Methode anzubieten. Test-Doubles kommen vor allem dann zum Einsatz, wenn entweder die Tests aufgrund vieler externer Abhängigkeiten langsam sind und beschleunigt werden sollen oder wenn indirekte Ausgaben oder indirekte Eingaben der zu testenden Komponente überprüft werden
sollen, an die man von außerhalb dieser Komponente (sprich im Unit-Test) nicht herankommt. Hinweis Test-Doubles können die Ausführung von Tests beschleunigen und Ihnen dabei helfen, eine Komponente schnell in Isolation zu testen. Trotzdem sollten Sie mindestens einen Test vorsehen, in dem Sie die Komponente mit realen Abhängigkeiten testen. In der Regel nennt man solche Art von Tests Integrationstests. Nur durch diese Art von Tests können Sie sicherstellen, dass die Komponente auch im Produktivsystem richtig funktioniert.
Es gibt verschiedene Arten von Test-Doubles, von denen ich Ihnen im Folgenden anhand der JavaScript-Bibliothek Sinon.js die drei bekanntesten vorstellen möchte: Test-Spies, Test-Stubs und MockObjekte. 6.2.1 Sinon.JS
Sinon.JS (http://sinonjs.org) ist eine Bibliothek, mit deren Hilfe sich Spies, Stubs und Mocks erstellen lassen. Es stammt aus der Feder von Christian Johansen, dem Autor des Buches Test-Driven JavaScript Development, und steht unter einer Open-Source-Lizenz zur Verfügung. Die Bibliothek können Sie von der Website in Form einer JavaScript-Datei herunterladen werden oder alternativ über NPM (npm install sinon ‐‐save-dev) installieren. 6.2.2 Spies
Im einfachsten Fall läuft ein Test so ab, dass man in der ExercisePhase eine oder mehrere Funktionen/Methoden der zu testenden
Komponente aufruft und die Ergebnisse aus diesen Aufrufen in der Verify-Phase durch weitere Funktions-/Methodenaufrufe an der zu testenden Komponente überprüft. Nehmen Sie dazu als einfaches Beispiel das Testen der Array-Methode push(). Um zu überprüfen, ob diese Methode das übergebene Element dem Array hinzufügt, bietet sich folgendes Vorgehen an: zunächst über die zu testende Methode push() dem Array ein oder mehrere Elemente hinzufügen und anschließend über Zugriff auf die Eigenschaft length des Arrays dessen Länge überprüfen. Die Auswirkung von push() kann in diesem Beispiel also direkt über die Eigenschaft length überprüft werden. Häufig ist es aber so, dass das zu testende Objekt keine Eigenschaft oder Methode anbietet, über die das erwartete Ergebnis überprüft werden kann, oder die entsprechende Eigenschaft oder Methode nicht öffentlich ist. In diesem Fall spricht man auch von indirekten Ausgaben, also Ausgaben, die von der zu testenden Komponente intern erzeugt werden, aber nicht von außen zugänglich sind (siehe Abbildung 6.8). Um solche indirekten Ausgaben testen zu können, sind Spies die richtige Wahl, denn mit ihrer Hilfe lassen sich beliebige Funktionsaufrufe während des Testens abfangen und verschiedene Informationen, wie beispielsweise die übergebenen Parameter oder der Rückgabewert, zwischenspeichern. Um die indirekten Ausgaben zu überprüfen, können diese zwischengespeicherten Informationen anschließend in der Verify-Phase aus dem Spy ausgelesen werden.
Abbildung 6.8 Das Prinzip von Test-Spies
Beispiel für den Einsatz von Test-Spies
Angenommen, Sie haben eine Funktion, die eine Callback-Funktion erwartet, und Sie möchten durch einen Test sicherstellen, dass diese Callback-Funktion aufgerufen wird, wenn die ursprüngliche Funktion aufgerufen wird. Ohne Test-Spy würden Sie dies vermutlich lösen, indem Sie innerhalb der Callback-Funktion ein entsprechendes Flag auf true setzen und es anschließend im Test überprüfen. Hinweis Das folgende Listing sowie die weiteren Beispiele in diesem Abschnitt verwenden mocha als Testframework. describe('aFunction', () => {
it('should call the callback', () => {
let called = false;
const callback = () => {
called = true;
}
aFunction(callback);
assert.equal(called, true);
});
});
Listing 6.22 Überprüfen indirekter Ausgabe ohne Spy
Auch wenn dies prinzipiell funktioniert, ist der damit verbundene Aufwand doch unnötig hoch. Ein Spy erledigt das, was in Listing 6.22 manuell gemacht wurde, automatisch. Letztendlich handelt es sich bei einem Spy (zumindest in Sinon.js) um ein Funktionsobjekt, das sich verschiedene Informationen merkt, wenn es aufgerufen wird (Anzahl der Aufrufe, Parameter und Rückgabewert etc.). In Sinon.js können Sie einen Spy über die Methode spy() erstellen. Den Test von eben würden Sie dann wie folgt formulieren: const sinon = require('sinon');
describe('aFunction', () => {
it('should call the callback', () => {
const spy = sinon.spy();
aFunction(spy);
assert.equal(spy.callCount, 1);
});
});
Listing 6.23 Überprüfen indirekter Ausgabe mit Spy
Spies für existierende Funktionen und Methoden
Sie können die Methode spy() aber auch dazu nutzen, bereits existierende Funktionen bzw. Methoden zu »überwachen«. Dazu übergeben Sie der Methode einfach die entsprechende Funktion oder – wenn es sich um eine Objektmethode handelt – das Objekt plus den Namen der Methode: const spy = sinon.spy(aFunction);
const spy = sinon.spy(object, 'aMethod');
Der erzeugte Spy fungiert dann als Wrapper um die Funktion/Methode, speichert wie gehabt alle oben genannten
Informationen und delegiert den Aufruf an die gewrappte Funktion/Methode. Nehmen wir als Beispiel ein Objekt UserRepository mit der Methode listAllUsers(), die eine Liste bzw. ein Array von Nutzerobjekten zurückgibt und dazu intern einen Aufruf an einen Webservice tätigt. Als Anforderung möchten Sie im Test definieren, dass nur der erste Aufruf der Methode listAllUsers() den unterliegenden Webservice aufruft. Alle weiteren Aufrufe der Methode dagegen sollen das Ergebnis aus einem Cache zurückgeben. Eine vereinfachte Version des Objektmodells sähe wie folgt aus: // Datei UserService.js
module.exports = class UserService {
static listAllUsers() {
return [{
name: 'Max',
lastname: 'Mustermann'
}];
}
}
// Datei UserRepository.js
const UserService = require('./UserService');
module.exports = class UserRepository {
listAllUsers() {
return UserService.listAllUsers();
}
filterUsers(filter) {
return this.listAllUsers().filter(filter);
}
};
Listing 6.24 Erste Version des zu testenden Codes
Um nun die Anforderung zu überprüfen, gehen wir wie in Listing 6.25 vor. Zunächst erzeugen wir in der Setup-Phase für die Methode listAllUsers() der Klasse UserService einen TestSpy. Denn dies ist die Methode, für die wir wissen bzw. testen möchten, wie häufig sie aufgerufen wird. In der Exercise-Phase wird dann zweimal die Methode listAllUsers() von userRepository ausgeführt und in der Verify-Phase anhand der Eigenschaft callCount überprüft, ob der Webservice wie gefordert nur einmal aufgerufen wurde. Der Aufruf von restore() in der Teardown-Phase versetzt das gewrappte Objekt übrigens wieder in seinen Ursprungszustand. const UserRepository = require('../src/UserRepository');
const UserService = require('../src/UserService');
const assert = require('assert');
const sinon = require('sinon');
describe('UserRepository', () => {
let userRepository;
let spy;
before(() => {
userRepository = new UserRepository();
spy = sinon.spy(UserService, 'listAllUsers');
});
after(() => {
spy.restore();
});
describe('listAllUsers()', () => {
it('should only call web service once and cache the results',() => {
const users = userRepository.listAllUsers();
const users2 = userRepository.listAllUsers();
assert.equal(spy.callCount, 1);
});
});
});
Listing 6.25 Ein Test-Spy für eine existierende Objektmethode
Nach Ausführung des Tests schlägt dieser zunächst wie erwartet fehl, weil die Methode des Webservices nicht wie gefordert nur
einmal, sondern bei jedem Aufruf ausgeführt wird. Die Anforderung lässt sich aber beispielsweise über das aus Abschnitt 2.5.8, »Selfdefining Functions«, bekannte Entwurfsmuster der Self-defining Functions bzw. Self-overwriting Functions durch einen Cache realisieren, so dass der Test anschließend erfolgreich durchgeführt werden kann: module.exports = class UserRepository {
listAllUsers() {
const cache = UserService.listAllUsers();
UserRepository.prototype.listAllUsers = () => {
return cache;
}
return cache;
}
};
Listing 6.26 Überarbeitete Version des zu testenden Codes
Spy-API
Die Spy-API von Sinon.js definiert eine umfangreiche Menge an Eigenschaften und Funktionen. Tabelle 6.3 gibt Ihnen einen kurzen Überblick. Beachten Sie: Diese Eigenschaften und Funktionen können ebenfalls auf der gewrappten Funktion bzw. Methode aufgerufen werden. Statt spy.callCount hätten wir eben also auch UserService.listAllUsers.callCount schreiben können. Eigenschaft/Methode Beschreibung callCount
Enthält die Anzahl der Aufrufe.
called
Enthält true, wenn die Funktion oder Methode aufgerufen wurde.
Eigenschaft/Methode Beschreibung calledOnce,
calledTwice,
calledThrice
Enthält jeweils true, wenn die Funktion oder Methode mindestens einmal, zweimal bzw. dreimal aufgerufen wurde.
withArgs(
parameter1,
parameter2,
…
)
Erzeugt einen Spy, der ausschließlich Aufrufe abfängt, bei denen die angegebenen Parameter übergeben wurden.
calledBefore(
anotherSpy
)
Liefert true, falls der Spy vor dem übergebenen Spy aufgerufen wurde.
calledAfter(
anotherSpy
)
Liefert true, falls der Spy nach dem übergebenen Spy aufgerufen wurde.
calledWith(
parameter1,
parameter2,
…
)
Liefert true, falls der Spy mindestens einmal mit den übergebenen Parametern aufgerufen wurde.
alwaysCalledWith( Liefert true, falls der Spy in allen Fällen parameter1,
mit den übergebenen Parametern aufgerufen wurde. parameter2,
…
)
Eigenschaft/Methode Beschreibung calledWithNew()
Liefert true, falls der Spy als Konstruktorfunktion aufgerufen wurde.
neverCalledWith(
parameter1,
parameter2,
…
)
Liefert true, falls der Spy nicht mit den übergebenen Parametern aufgerufen wurde.
threw()
Liefert true, falls der Spy eine Exception geworfen hat.
getCall(n)
Gibt den n-ten Aufruf eines Spies zurück.
restore()
Stellt die ursprüngliche Version der durch den Spy gewrappten Funktion her.
Tabelle 6.3 Auswahl der Spy-API von Sinon.js
Merke Spies sind Funktionen, die als Wrapper für andere Funktionen deren Parameter, Rückgabewert, ihren Ausführungskontext sowie geworfene Fehler abfangen und für die spätere Auswertung merken.
6.2.3 Stubs
Test-Stubs (kurz Stubs) adressieren eine etwas andere Problematik als Test-Spies. Über Test-Stubs ist es nämlich möglich, bestimmte indirekte Aufrufe abzufangen und vordefinierte Werte
zurückzugeben. Dies ist immer dann hilfreich, wenn das Ergebnis einer zu testenden Komponente von einer indirekten Eingabe durch ein DOC abhängt und auf diese Eingabe Einfluss genommen werden soll. Während Spies also indirekte Ausgaben des SUTs abfangen, liefern Stubs die indirekten Eingaben eines SUTs (siehe Abbildung 6.9).
Abbildung 6.9 Das Prinzip von Test-Stubs
Stubs erzeugen Sie in Sinon.js mit der Methode stub(). Ohne Parameter gibt diese Methode ein anonymes Stub-Funktionsobjekt zurück, häufiger werden Sie aber wahrscheinlich folgende Varianten verwenden: // Ersetzen der übergebenen Funktion
const stub = sinon.stub(aFunction);
// Ersetzen der angegebenen Objektmethode
const stub = sinon.stub(object, 'aMethod', newMethod);
// Ersetzen aller Methoden eines Objekts
const stub = sinon.stub(object);
Stubs verfügen in Sinon.js über alle Methoden, über die auch Spies verfügen, bieten zusätzlich aber einige weitere an, von denen Tabelle 6.4 eine Auswahl zeigt.
Eigenschaft/Methode Beschreibung returns(object)
Legt das Objekt fest, das als indirekte Eingabe von dem Stub bzw. der gewrappten Funktion/Methode an die zu testende Komponente zurückgegeben werden soll.
throws()
Veranlasst den Stub, einen Fehler zu werfen.
onCall(n)
Definiert, wie sich der Stub zum Zeitpunkt des n-ten Aufrufs verhalten soll, beispielsweise:
stub.onCall(1).returns(1); stub.onCall(2).returns(2);
Tabelle 6.4 Auswahl der Stub-API von Sinon.js
Beispiel für den Einsatz von Test-Stubs
Lassen Sie mich auf das Beispiel mit dem UserRepository zurückkommen. Dieses soll nun um eine Methode filterUsers() erweitert werden, über die Nutzer nach einem bestimmten Kriterium gesucht bzw. gefiltert werden können. Das Filterkriterium soll dabei als Funktion übergeben werden. Des Weiteren gehen wir davon aus, dass die Methode der Einfachheit halber intern die bereits existierende Methode listAllUsers() verwendet, deren Rückgabewert somit die indirekte Eingabe für filterUsers() darstellt. Um prinzipiell prüfen zu können, ob die Methode filterUsers() für eine gegebene Filterfunktion die richtige Menge an
Nutzerobjekten zurückgibt, muss zunächst bekannt sein, welche Nutzerobjekte überhaupt im UserRepository enthalten sind. Kurz: Sie benötigen ein entsprechendes Test-Fixture, das Sie in der Setup-Phase des Tests initialisieren. Eine Möglichkeit wäre es, hierfür eine Testdatenbank zu verwenden, diese mit den Testdaten zu befüllen und später in der Teardown-Phase wieder zu »bereinigen«. Einfacher definieren Sie das Test-Fixture aber, indem Sie, wie in Listing 6.27 zu sehen, für die Methode listAllUsers() einen Test-Stub erzeugen und darin über returns() den Rückgabewert definieren: const UserRepository = require('../src/UserRepository');
const UserService = require('../src/UserService');
const assert = require('assert');
const sinon = require('sinon');
describe('UserRepository', () => {
let userRepository;
let stub;
beforeEach(() => {
userRepository = new UserRepository();
stub = sinon.stub(userRepository, 'listAllUsers');
stub.returns([{
name: 'Peter',
lastname: 'Mustermann'
}, {
name: 'Max',
lastname: 'Mustermann'
}, {
name: 'Moritz',
lastname: 'Mustermann'
}]);
});
afterEach(() => {
stub.restore();
});
describe('filterUsers()', () => {
it('should return users for given filter', () => {
const users = userRepository.filterUsers(
(user) => user.name.indexOf('M') === 0
);
assert.equal(users.length, 2);
});
});
});
Listing 6.27 Der Test-Stub im Einsatz
Die Implementierung der Methode filterUsers() finden Sie der Vollständigkeit halber in Listing 6.28. Wie Sie sehen, wird hier die filter()-Methode von Arrays genutzt. Dies ist zwar zugegeben nicht besonders effizient, weil es somit notwendig ist, die gesamte Nutzerliste von dem unterliegenden Webservice zu holen, um den Filter anwenden zu können. An dieser Stelle soll uns das aber nicht weiter stören. const UserService = require('./UserService');
module.exports = class UserRepository {
...
filterUsers(filter) {
return this.listAllUsers().filter(filter);
}
};
Listing 6.28 Implementierung der Filterfunktion
Merke Stubs sind wie Spies sozusagen Wrapper um andere Funktionen, simulieren im Unterschied aber eine bestimmte Funktionalität der Originalfunktion bzw. haben ein vordefiniertes Verhalten. In erster Linie dienen Stubs dazu, indirekte Eingaben für die zu testende Komponente zu liefern und Komponenten wie Klassen in Isolation testen zu können.
6.2.4 Mock-Objekte
Mock-Objekte (kurz Mocks) fangen ähnlich wie Test-Spies die indirekten Ausgaben der zu testenden Komponente ab. Im Unterschied jedoch zu den Test-Spies führen Mock-Objekte selbst bereits gewisse Überprüfungen dieser indirekten Ausgaben durch (siehe Abbildung 6.10). Das Ziel dabei ist es, sicherzustellen, dass die zu testende Komponente die jeweilige Komponente, von der sie abhängig ist, richtig verwendet.
Abbildung 6.10 Das Prinzip von Mock-Objekten
Ein weiterer Unterschied zu Spies und Stubs ist der, dass MockObjekte komplette Objekte ersetzen, während die beiden erstgenannten wie gesehen nur einzelne Funktionen bzw. Methoden ersetzen. Mocks können daher in Sinon.js nur auf Basis von Objekten, nicht aber auf Basis einzelner Funktionen definiert werden. const mock = sinon.mock(object);
Mocks stellen die gleiche API wie Spies und Stubs zur Verfügung, die Sie bereits aus den vorigen Abschnitten kennen. Das heißt, es ist mit Mock-Objekten sowohl möglich, indirekte Ausgaben zu definieren, als auch indirekte Eingaben zu definieren. Den Kern der Mock-API bilden aber die sogenannten Expectations, über die sich
verschiedene Anforderungen für eine konkrete Objektmethode definieren lassen, beispielsweise wie häufig sie aufgerufen oder mit welchen Parametern sie aufgerufen werden soll. Ein Expectation-Objekt erhalten Sie, indem Sie auf dem MockObjekt die Methode expects() aufrufen und dabei den Namen der entsprechenden Objektmethode als Zeichenkette übergeben. Auf dem zurückgegebenen Objekt definieren Sie dann über spezielle Methoden (siehe Tabelle 6.5) die Anforderungen. Abschließend prüfen Sie über verify(), ob die Anforderungen eingehalten wurden. Falls nicht, schlägt der Test fehl. // Erstellen der Expectation
const expectation = mock.expects('aMethod');
// Festlegen der Anforderung
expectation.atLeast(2);
// Überprüfen der Anforderung
expectation.verify();
Listing 6.29 Überprüfen von Anforderungen über Expectations
Eigenschaft/Methode Beschreibung atLeast(count)
Angabe der Mindestanzahl an Funktionsaufrufen: Hierüber kann angegeben werden, wie häufig eine Funktion mindestens aufgerufen werden soll.
atMost(count)
Angabe der Höchstanzahl an Funktionsaufrufen: Hierüber kann angegeben werden, wie häufig eine Funktion maximal aufgerufen werden soll.
never()
Eine Funktion soll überhaupt nicht aufgerufen werden.
Eigenschaft/Methode Beschreibung once(),
twice(),
thrice()
Eine Funktion soll genau einmal, zweimal oder dreimal aufgerufen werden.
exactly(n)
Eine Funktion soll genau n-mal aufgerufen werden.
withArgs(
arg1,
arg2,
...,
argN
)
Eine Funktion soll genau mit den angegebenen Parametern aufgerufen werden, kann aber zusätzlich auch mit anderen Parametern aufgerufen werden.
withExactArgs(
arg1,
arg2,
...,
argN
)
Eine Funktion soll ausschließlich mit den angegebenen Parametern aufgerufen werden.
on(object)
Eine Funktion soll auf dem angegebenen Objekt aufgerufen werden.
verify()
Überprüft die definierten Anforderungen und liefert einen Fehler, wenn sie nicht erfüllt sind.
Tabelle 6.5 Auswahl der Mock-API von Sinon.js
Beispiel für den Einsatz von Mock-Objekten
Lassen Sie mich erneut das Beispiel mit dem UserRepository heranziehen. Um zu testen, ob der Webservice nur einmal aufgerufen wird, mussten Sie bei den Test-Spies in der Verify-Phase explizit die entsprechende Information aus dem Spy-Objekt überprüfen. Unter Verwendung eines Mock-Objekts definieren Sie diese Anforderung bereits im Vorfeld über eine Expectation und rufen in der Verify-Phase lediglich deren verify()-Methode auf. const UserRepository = require('../src/UserRepository');
const UserService = require('../src/UserService');
const assert = require('assert');
const sinon = require('sinon');
describe('UserRepository', () => {
let userRepository;
let expectation;
before(() => {
userRepository = new UserRepository();
const mock = sinon.mock(UserService);
expectation = mock.expects('listAllUsers').atMost(1);
});
describe('listAllUsers()', () => {
it('should only call web service once and cache the results', () => {
const users = userRepository.listAllUsers();
const users2 = userRepository.listAllUsers();
expectation.verify();
});
});
});
Listing 6.30 Das Mock-Objekt im Einsatz
Merke Mit Mock-Objekten lassen sich indirekte Ausgaben und indirekte Eingaben erzeugen und zusätzlich bestimmte Überprüfungen vornehmen.
Alternative Bibliotheken
Eine alternative Bibliothek für das Erstellen von Test-Doubles ist testdouble.js (https://github.com/testdouble/testdouble.js). Eine detaillierte Auflistung der Unterschiede zu Sinon.JS finden Sie unter http://blog.testdouble.com/posts/2016-03-13-testdouble-vssinon.html. Auch das Testframework Jasmine stellt eine eigene API zur Definition von Test-Doubles zur Verfügung.
6.3 Testabdeckung Die Funktionalität des eigenen Quelltextes durch Unit-Tests zu überprüfen ist gut, bringt aber nur etwas, wenn Sie durch die Tests auch ausreichend (Sonder-)Fälle abdecken, so dass möglichst viele Verzweigungen innerhalb des zu testenden Codes durch die Tests ausgeführt werden. 6.3.1 Einführung
Um dies zu gewährleisten, werden in der Regel parallel zum UnitTesten spezielle Tools eingesetzt, die die sogenannte Testabdeckung (engl. Code Coverage bzw. Test-Coverage) ermitteln. Die Ausgabe eines solchen Tools ist ein Report, der für jede Codezeile angibt, ob diese innerhalb eines Unit-Tests ausgeführt wurde oder nicht. Zusätzliche Übersichten geben zudem Überblick darüber, wie viel Prozent des Codes »abgedeckt« ist. In der Regel strebt man bei Code, der die Anwendungslogik repräsentiert, eine Codeabdeckung von 100 % an. Code, der lediglich das Objektmodell darstellt, ist weniger kritisch: Getter- und SetterMethoden beispielsweise müssen in der Regel nicht durch UnitTests abgedeckt sein. Für JavaScript gibt es verschiedene Tools, die die Testabdeckung ermitteln. Im möchte Ihnen im Folgenden die Tools Blanket.js und Istanbul vorstellen. 6.3.2 Blanket.js
Bei Blanket.js (http://blanketjs.org) handelt es sich um eine Bibliothek für JavaScript, mit der sich auf Basis der Unit-Tests die Testabdeckung ermitteln lässt. Standardmäßig können Sie die Bibliothek in Kombination mit den Testframeworks QUnit, Jasmine und mocha verwenden. Im Folgenden stelle ich Ihnen – aufbauend auf dem bis hierhin Gelernten – das Zusammenspiel von Blanket.js mit QUnit und mocha vor. Verwendung mit QUnit
Um Blanket.js mit QUnit verwenden zu können, müssen Sie lediglich den Quelltext von Blanket.js von der Webseite herunterladen und in den entsprechenden Testrunner einbinden. Die eingebundenen JavaScript-Dateien, zu denen die Testabdeckung ermittelt werden soll, versehen Sie im entsprechenden
Listing 6.31 Einbinden von Blanket.js in der Testrunner-Datei
Wenn Sie nun den Testrunner erneut starten, werden Sie oben in der Menüleiste den neuen Eintrag Enable coverage bemerken, über den sich bestimmen lässt, ob die Testabdeckung ermittelt werden
soll. Abbildung 6.11 zeigt eine Beispielausgabe, für die ich zu Demonstrationszwecken den Test der Methode ArrayHelper.min() wieder entfernt habe. Der Inhalt dieser Methode kommt also nicht zur Ausführung, was Sie im Report an der entsprechend rot hinterlegten Codezeile feststellen können.
Abbildung 6.11 Testabdeckung mit Blanket.js für einen QUnit-basierten Test
Verwendung mit mocha
Möchten Sie Blanket.js in Kombination mit mocha einsetzen, lässt es sich über NPM innerhalb des entsprechenden Projekts installieren (npm install blanket --save-dev). Anschließend können Sie es über require('blanket')() in den jeweiligen Test einbinden und direkt aufrufen. require('blanket')(); // Einbinden und Aufruf in einem
const ArtistRepository = require('../src/ArtistRepository');
const assert = require('assert');
...
Listing 6.32 Einbinden von Blanket.js in die Testdatei
Anschließend rufen Sie mocha wie gewohnt von der Kommandozeile aus auf, müssen dabei aber darauf achten, einen der Reporter zu verwenden, der auch die Informationen zur
Testabdeckung berücksichtigt. Dies sind zum einen der Reporter json-cov, der eine JSON-Ausgabe erzeugt, und zum anderen der Reporter html-cov, der analog eine HTML-Ausgabe erzeugt: mocha -R html-cov test/ArtistRepositoryTest.js > coverage.html
Alternativ zum Einbinden der require()-Anweisung in jeder Testdatei haben Sie auch die Möglichkeit, die Abhängigkeit zu Blanket.js dem Kommandozeilenbefehl zu übergeben. Dies hat den Vorteil, dass die Testdateien an sich keine Abhängigkeiten zu Blanket.js haben und Sie diese nicht entsprechend anpassen müssen. Modulabhängigkeiten definieren Sie für mocha über den Parameter -r mit darauffolgendem Modulnamen: mocha -r blanket -R html-cov test/ArtistRepositoryTest.js > coverage.html
Abbildung 6.12 zeigt das Ergebnis.
Abbildung 6.12 Testabdeckung mit Blanket.js für einen mocha-basierten Test
Integration in Build-Tools
Blanket.js lässt sich ebenfalls in die Build-Tools Grunt (z.B. über grunt-blanket: https://github.com/alex-seville/grunt-blanket) und Gulp (z.B. über gulp-blanket-mocha: https://github.com/dylanb/gulp-blanket-mocha) integrieren. An dieser Stelle möchte ich Ihnen aber zeigen, wie Sie die Testabdeckung mit Hilfe des in diesem Kapitel bereits vorgestellten Grunt-Plugins grunt-mocha-test (siehe Abschnitt 6.1.7, »Integration in Build-Tools«) ermitteln. Dieses Plugin unterstützt nämlich schon von sich aus Blanket.js. Als Voraussetzung müssen Sie – falls noch nicht geschehen – zunächst die NPM-Packages grunt, grunt-mocha-test und blanket lokal in Ihr Projekt installieren: npm install grunt --save-dev
npm install grunt-mocha-test --save-dev
npm install blanket --save-dev
Anschließend konfigurieren Sie die Datei Gruntfile.js wie folgt: 'use strict';
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-mocha-test');
grunt.initConfig({
mochaTest: {
test: {
options: {
timeout: 8000,
reporter: 'spec',
captureFile: 'results.txt',
require: 'coverage/blanket'
},
src: ['test/**/*.js']
},
coverage: {
options: {
reporter: 'html-cov',
quiet: true,
captureFile: 'coverage.html'
},
src: ['test/**/*.js']
}
}
});
grunt.registerTask('default', 'mochaTest');
};
Listing 6.33 Integration von Blanket.js in Grunt
Zudem ist es notwendig, im entsprechenden Projekt folgenden Code in eine neue Datei mit Namen blanket.js im Ordner coverage zu kopieren. const path = require('path');
const srcDir = path.join(__dirname, '..', 'src');
require('blanket')({
pattern: srcDir
});
Listing 6.34 Inhalt der Datei »blanket.js« im Ordner »coverage«
Anschließend führen Sie den Grunt-Task wie gewohnt über grunt mochaTest aus und erhalten im angegebenen Verzeichnis den HTMLReport für die Testabdeckung. 6.3.3 Istanbul
Neben Blanket.js zählt Istanbul (https://istanbul.js.org/) zu den bekannteren JavaScript-Tools für die Ermittlung der Testabdeckung. Istanbul installieren Sie über den Befehl npm install nyc --save-dev und integrieren es relativ einfach in ein bestehendes mocha-Setup, indem Sie es dem entsprechenden Befehl voranstellen: {
"scripts": {
"test": "nyc mocha"
}
}
Listing 6.35 Verwenden von Istanbul in Kombination mit mocha
Neben mocha lässt sich Istanbul auch relativ einfach in Kombination mit anderen Frameworks wie node-tap und AVA verwenden, und es ist bereits fester Bestandteil von Jest, dem
Testing-Framework von Facebook, das ich weiter vorn in diesem Kapitel vorgestellt habe. Die Konfiguration erfolgt dann beispielsweise über einen Eintrag in der Datei package.json: "jest": {
"verbose": true,
"collectCoverage": true
}
Listing 6.36 Verwenden von Istanbul in Kombination mit Jest
Anschließend lässt sich Jest wie gewohnt über den Befehl jest aufrufen, und Istanbul sammelt im Hintergrund Angaben zur Testabdeckung:
Abbildung 6.13 Testabdeckung mit Istanbul
6.4 DOM-Tests Wenn Sie schon einmal eine Desktop-Anwendung mit C# oder Java erstellt haben, wissen Sie, dass das Testen von grafischen Oberflächen mitunter recht aufwendig sein kann. Im Zweifelsfall simuliert man mit Hilfe einer speziellen Robot-API die Mausbewegungen und Mausklicks eines Nutzers und prüft anschließend den Zustand der grafischen Oberfläche. Vom Prinzip her funktioniert das Testen von Weboberflächen nicht anders, allerdings ist es in der Regel nicht notwendig, hierfür eine kompliziert zu bedienende Robot-API zu verwenden. Interaktionen mit Komponenten einer Webanwendung (Buttons, Textfelder etc.) lassen sich alle auch programmatisch über Events steuern. Um jedoch überhaupt erst solche Aspekte testen zu können, muss der entsprechende Unit-Test Zugriff auf die Webanwendung haben, sprich, er muss im Browser ausgeführt werden oder in einer Laufzeitumgebung, die Zugriff auf das Document Object Model (DOM) bietet. Angenommen, Sie möchten eine Methode erstellen, die die Listeneinträge in einer HTML-Liste alphabetisch sortiert und das dazugehörige DOM entsprechend aktualisiert. Um einen entsprechenden Testfall zu konstruieren, benötigen Sie zwangsweise eine HTML-Liste. Ohne die können Sie die Funktionalität der Methode nicht testen. Zum Einsatz kommen hierbei sogenannte HTML-Fixtures. Dabei handelt es sich um einen mehr oder weniger komplexen DOMSchnipsel, der vor Ausführung des Tests (bzw. in der Setup-Phase) geladen wird und dann innerhalb des Tests als gerendertes DOM zur
Verfügung steht. Im Fall von QUnit definiert man ein HTML-Fixture in der Testrunner-Datei innerhalb des -Elements mit der ID qunit-fixture. Hinweis Generell sollten Sie darauf achten, Ihre Komponenten möglichst unabhängig vom DOM zu halten. Im vorliegenden Beispiel wäre es sicherlich geschickter, die Sortierung an sich in eine Komponente auszulagern, die nicht auf einer HTML-Liste, sondern auf einem Array von Zeichenketten operiert.
Für das Beispiel mit der Listensortierung könnte ein solches Fixture wie in Listing 6.37 aussehen. Dort befindet sich innerhalb des entsprechenden Bereichs im Testrunner der DOM-Schnipsel, der für den Test als Ausgangspunkt verwendet werden soll: eine unsortierte HTML-Liste.
QUnit-Beispiel
Petra Mustermann
Moritz Mustermann
Peter Mustermann
Max Mustermann
Listing 6.37 Definition einer Test-Fixture
Einen dem Beispiel entsprechenden Unit-Test, der überprüft, ob die HTML-Liste durch die Sortiermethode (DOMListSorter.sort()) wie gewünscht sortiert wird, zeigt Listing 6.38. Über die Methode querySelector() wird die HTML-Liste zunächst innerhalb des HTML-Fixture-Bereichs ermittelt. Anschließend wird die Sortiermethode mit dieser Liste aufgerufen, und es werden über den Aufruf von querySelectorAll() alle Listenelemente der Liste ermittelt. Es folgen vier Assertions, die sicherstellen, dass die (zuvor unsortierte) Liste nun alphabetisch sortiert vorliegt. QUnit.test('DOMListSorter', (assert) => {
const list = document.querySelector('#qunit-fixture ul');
DOMListSorter.sort(list);
const items = Array.prototype.slice.call(list.querySelectorAll('li'));
assert.equal(items[0].textContent.trim(), 'Max Mustermann');
assert.equal(items[1].textContent.trim(), 'Moritz Mustermann');
assert.equal(items[2].textContent.trim(), 'Peter Mustermann');
assert.equal(items[3].textContent.trim(), 'Petra Mustermann');
});
Listing 6.38 Überprüfen des DOMs innerhalb eines Unit-Tests
Die Implementierung der eigentlichen Listensortierung zeigt Listing 6.39. Die Details dazu sind an dieser Stelle nicht wichtig. Nur so viel: domList.childNodes enthält alle Kindknoten der übergebenen HTML-Liste, über die filter()-Methode werden ausgehend davon nur die Knoten vom Typ Element (Typ 1) herausgefiltert. Das heißt, Textknoten beispielsweise werden nicht übernommen. domList.childNodes ist dabei wie auch das arguments-Objekt kein echtes Array, sondern nur Array-ähnlich.
Um also Array-Methoden wie filter() nutzen zu können, müssen sie über call() aufgerufen werden. Anschließend werden die ursprünglichen Elemente aus dem DOM gelöscht und durch neue (in sortierter Reihenfolge vorliegende) Elemente ersetzt. const DOMListSorter = {
sort(domList) {
// Sortierung
const items = Array.prototype.filter.call(domList.childNodes,
(item) => {
return item.nodeType === 1;
}).sort((item1, item2) => {
return item1.textContent.localeCompare(item2.textContent);
});
// Löschen der ursprünglichen Listenelemente
while (domList.firstChild) {
domList.removeChild(domList.firstChild);
}
// Hinzufügen der neuen Listenelemente
items.forEach((item) => {
domList.appendChild(item);
});
}
}
Listing 6.39 Implementierung der Sortierfunktionaliät
Hinweis Wenn Sie das HTML-Fixture direkt im Testrunner einbinden, besteht der Nachteil darin, dass Sie für Tests, die auf verschiedenen Fixtures arbeiten, jedes Mal einen neuen Testrunner schreiben müssen. In der Praxis lagert man daher die HTML-Fixtures in einzelne HTML-Dateien aus und lädt diese je nach Test dynamisch in den Testrunner.
6.5 Funktionstests Die im vorigen Abschnitt vorgestellten DOM-Tests werden immer dann genutzt, wenn JavaScript-Code mit Abhängigkeiten zum DOM getestet werden soll. Um richtige Funktionstests einer Webseite durchzuführen, sind DOM-Tests nur eingeschränkt geeignet und lassen sich hierzu nur punktuell für das Testen gewisser UI-Aspekte einsetzen (beispielsweise zum Überprüfen auf korrekte Fehlermeldungen bei fehlerhaften Formulareingaben). Komplexere Abläufe und Interaktionen dagegen lassen sich mit anderen Tools viel einfacher und komfortabler überprüfen. 6.5.1 PhantomJS
PhantomJS (http://phantomjs.org) ist ein Browser ohne grafische Oberfläche, der »lediglich« über eine Rendering-Engine verfügt (Headless Browser). Wie ein regulärer Browser kann er HTML interpretieren, CSS auswerten und JavaScript-Code ausführen und ist somit in der Lage, ein vollständiges Document Object Model einer Webseite aufzubauen. Über eine entsprechende API lässt sich ein Headless Browser zudem wie ein »richtiger« Browser steuern. Unter Headless Browser Testing versteht man das Durchführen von GUI-Tests (Integrationstests oder Regressionstests) unter Verwendung eines solchen Headless Browsers. Die Vorteile gegenüber einem normalen Browser in Bezug auf das Testen liegen auf der Hand: Da der Ballast der Browser-GUI entfällt, ist die »kopflose« Version zum einen wesentlich schneller gestartet, kann aber zum anderen auf einem Server ohne grafische Ausgabe laufen und lässt sich demzufolge leichter in ein CI-System (Continuous Integration System) integrieren. Außerdem ist das Setup in der Regel
recht einfach, weil keine spezielle Infrastruktur notwendig ist, um den Browser laufen zu lassen. PhantomJS verwendet als Rendering-Engine WebKit, genauer gesagt die QtWebKit-Implementierung – einen Port für WebKit, dem das Qt-Framework zugrunde liegt. Installationsdateien lassen sich von der Website für jedes gängige Betriebssystem herunterladen. Nach erfolgreicher Installation steht das Kommando phantomjs zur Verfügung, das als Eingabe eine JavaScript-Datei erwartet. Hinweis Ebenfalls interessant in dem Zusammenhang Headless Browser Testing sind zum einen das Tool puppeteer (https://github.com/GoogleChrome/puppeteer), das eine API für Node.js zur Verfügung stellt, über die sich Chrome im HeadlessModus steuern lässt, sowie zum anderen das Tool SlimerJS (https://slimerjs.org/), dem nicht WebKit als Browserengine zugrunde liegt, sondern Gecko, das auch von Firefox verwendet wird.
PhantomJS verwenden
PhantomJS besteht aus mehreren Packages, die nach den Vorgaben in CommonJS aufgebaut sind und dementsprechend eingebunden werden können. Das Package »webpage« ist dabei für das Rendern der Webseiten zuständig. Wie in Listing 6.40 zu sehen, müssen Sie dafür lediglich das Package importieren und eine Objektinstanz von WebPage erstellen. Anschließend lässt sich die übergebene URL durch den Aufruf der Methode open() laden. Innerhalb der übergebenen Callback-
Funktion haben Sie dann, sobald der Prozess erfolgreich abgeschlossen ist, Zugriff auf die entsprechende Webseite. Optional lassen sich weitere Callbacks definieren (z.B. onResourceRequested und onResourceReceived), um wie im Beispiel auf HTTP-Request und HTTP-Response zuzugreifen. const webpage = require('webpage').create();
const url = 'https://www.rheinwerk-verlag.de/';
webpage.onResourceRequested = (request, networkRequest) => {
console.log("request: " + request.url);
};
webpage.onResourceReceived = (response) => {
console.log(`response: ${response.url}`);
};
webpage.open(url, (status) => {
if (status == 'success') {
console.log(JSON.stringify(webpage.cookies, null, 2));
}
phantom.exit();
});
Listing 6.40 Rendern einer Webseite mit PhantomJS
Um Code im Kontext der geladenen Webseite auszuführen und beispielsweise auf deren DOM zugreifen (oder es ändern) zu können, verwendet man die evaluate()-Methode. Sie erwartet wie open() eine Callback-Funktion, innerhalb der Sie Zugriff auf die Variable document und damit auf das DOM haben. Listing 6.41 zeigt, wie Sie auf diese Weise den Titel des Dokuments ermitteln, um ihn anschließend außerhalb des evaluate-Callbacks nutzen zu können: const webpage = require('webpage').create();
const url = 'https://www.rheinwerk-verlag.de/';
webpage.open(url, (status) => {
if (status == 'success') {
const titel = webseite.evaluate(() => {
return document.title;
});
console.log(titel);
}
phantom.exit();
});
Listing 6.41 Zugriff auf das DOM mit PhantomJS
Weitere Features
PhantomJS unterstützt DOM, CSS, JavaScript, Canvas und SVG, nicht aber jegliche Arten von Plugins (z.B. Flash), WebGL, Video und Audio sowie CSS3 3D – WebGL nicht, weil es ein OpenGL-fähiges System voraussetzen würde und das laut Entwickler der Philosophie von PhantomJS widerspricht, vollständig »headless« zu sein. Video und Audio lassen sich nicht nutzen, da dies hieße, mehrere Codecs installieren zu müssen und somit wieder abhängig von anderen Komponenten zu sein. XPath als Feature ist zwar aufgelistet, aber als nicht getestet markiert. Um genau herauszufinden, welche Features die unterliegende WebKit-Implementierung unterstützt, ist ein Test über die Modernizr-Bibliothek (http://modernizr.com) sinnvoll. Ein Beispielskript dazu liegt der PhantomJS-Installation bei und lässt sich beispielsweise unter macOS mit folgendem Befehl starten: phantomjs /usr/phantomjs/examples/features.js PhantomJS und das Testen
PhantomJS ist kein Testframework, seine Aufgabe liegt ausschließlich im Rendern und Manipulieren von Webseiten, bildet also lediglich die Grundlage für das Headless Browser Testing. Folglich enthält PhantomJS weder einen Testrunner noch eine eigene Assertion-Bibliothek, wie sie für solche Frameworks typisch sind. Für viele bekannte Testframeworks wie mocha, Jasmine, QUnit oder WebDriver stehen aber Testrunner zur Verfügung, mit denen sich
PhantomJS ohne größeren Aufwand in das jeweilige Framework integrieren lässt. Im Folgenden möchte ich aber auf eine Bibliothek eingehen, die direkt auf PhantomJS aufbaut und dieses unter anderem um ein eigenes Testframework erweitert: CasperJS. 6.5.2 CasperJS
CasperJS (http://casperjs.org) führt eine zusätzliche Abstraktionsschicht ein, die den Zugriff auf die Inhalte einer Webseite gegenüber PhantomJS deutlich vereinfacht. Mit Hilfe einer entsprechenden API ist es beispielsweise möglich, Navigationsschritte zu definieren, anhand derer ein Skript automatisch beispielsweise Formulare ausfüllt und absendet, Links betätigt und vieles mehr. CasperJS installieren
CasperJS gibt es wie PhantomJS für Mac, Linux und Windows. Als Voraussetzung für die aktuell letzte Version (1.1.0) müssen mindestens PhantomJS 1.9.1 und Python 2.6 installiert sein. Nach erfolgreicher Installation (siehe http://docs.casperjs.org/en/latest/installation.html) starten Sie ein CasperJS-Skript über das Kommando casperjs: casperjs script.js CasperJS verwenden
In Listing 6.42 sehen Sie, wie sich CasperJS dazu verwenden lässt, einen Ablauf von Navigationsschritten zu definieren. Zuerst wird die Website mit der URL http://www.heise.de/ aufgerufen und ihr Titel ausgegeben. Im zweiten Schritt wird der Link mit dem Label
Newsticker betätigt und anschließend ein Screenshot erstellt. Zuletzt öffnet das Script die iX-Homepage (http://www.heise.de/ix), gibt ihren Titel aus und schreibt den Inhalt der Seite in eine Datei. const casper = require('casper').create();
casper.start('http://www.heise.de/', () => {
this.echo(this.getTitle());
});
casper.then(() => {
this.clickLabel('Newsticker', 'a'); });
casper.then(() => {
this.echo(this.getTitle());
this.capture('ix.png', {
top: 0, left: 00, width: 800, height: 600
});
});
casper.thenOpen('http://www.heise.de/ix/', () => {
this.echo(this.getTitle());
this.download(this.getCurrentUrl(), 'ix.html');
});
casper.run();
Listing 6.42 Simulierte Nutzerinteraktion mit CasperJS
Funktionstests mit CasperJS
CasperJS enthält ein eigenes Testframework, über das sich unterschiedliche Assertions definieren lassen. Hierbei gibt es neben den aus anderen Frameworks bekannten Funktionen wie assertEquals(), assertFalsy(), assertNot() und assertTruthy() auch speziellere Funktionen wie assertElementCount(), assertHttpStatus(), assertResourceExists() und assertUrlMatch(), die auf das Testen von Webseiten zugeschnitten sind (siehe Tabelle 6.6). Listing 6.43 zeigt einige davon in der Anwendung.
casper.test.begin('Heise Web-Site Test', 4, (test) => {
casper.start('http://www.heise.de/', () => {
this.echo('URL: ' + this.getCurrentUrl());
test.assertTitle(
'heise online - IT-News, Nachrichten und Hintergründe',
'hat den richtigen Titel');
test.assertTitleMatch(/heise/, 'enthält "heise" im Titel');
test.assertHttpStatus(200, 'gibt 200 als HTTP-Status zurück');
});
casper.then(() => {
this.clickLabel('Newsticker', 'a');
});
casper.then(() => {
this.echo('URL: ' + this.getCurrentUrl());
test.assertTitleMatch(/heise/, 'enthält "heise" im Titel');
});
casper.run(() => {
test.done();
});
});
Listing 6.43 Durchführen von Tests mit CasperJS
Möchten Sie die Funktionen des Testframeworks nutzen, starten Sie CasperJS mit dem zusätzlichen Parameter test: casperjs test script.js
Daraufhin führt CasperJS die jeweiligen Tests aus und erstellt einen Testreport, wie in Listing 6.44 zu sehen. Test file: example2.js
# Heise Web-Site Test
URL: http://www.heise.de/
PASS hat den richtigen Titel
PASS enthält 'heise' im Titel
PASS gibt 200 als HTTP-Status zurück
URL: http://www.heise.de/newsticker/classic/
PASS enthält 'heise' im Titel
PASS 4 tests executed in 8.039s, 4 passed, 0 failed, 0 dubious, 0 skipped.
Listing 6.44 Konsolenausgabe des CasperJS-Tests
Assertion
Beschreibung
assertDoesntExist(selector)
Überprüft, ob es kein Element zu dem übergebenen CSSSelektor gibt.
assertElementCount(
selector,
count
)
Überprüft, ob es zu dem übergebenen CSSSelektor eine bestimmte Anzahl an Elementen gibt.
assertExists(selector)
Überprüft, ob es ein Element zu dem übergebenen CSSSelektor gibt.
assertHttpStatus(status)
Überprüft den HTTPStatus.
assertNotVisible(selector)
Überprüft, ob das Element, das durch den übergebenen CSSSelektor beschrieben wird, nicht sichtbar ist.
assertSelectorDoesntHaveText(
selector,
text
)
Überprüft, dass der übergebene Text in keinem der Elemente vorkommt, die durch den CSS-Selektor beschrieben werden.
Assertion
Beschreibung
assertSelectorHasText(
selector,
text
)
Überprüft, dass der übergebene Text in jedem der Elemente vorkommt, die durch den CSS-Selektor beschrieben werden.
assertTitle(titel)
Überprüft, ob der Titel des Dokuments mit der übergebenen Zeichenkette übereinstimmt.
assertTitleMatch(regExp)
Überprüft, ob der Titel des Dokuments mit dem übergebenen regulären Ausdruck übereinstimmt.
assertURLMatch(regExp)
Überprüft, ob die URL des Dokuments mit dem übergebenen regulären Ausdruck übereinstimmt.
assertVisible(selector)
Überprüft, ob das Element, das durch den übergebenen CSSSelektor beschrieben wird, sichtbar ist.
Tabelle 6.6 Auswahl aus der Assertion-API von CasperJS
Zugriff auf das DOM
Um innerhalb eines Tests auf das DOM zugreifen zu können, müssen Sie ähnlich wie in PhantomJS vorgehen. Direkten Zugriff auf das DOM haben Sie nur, wie in Listing 6.45 zu sehen, innerhalb des evaluate-Callbacks. casper.test.begin('Heise Homepage', 1, (test) => {
casper.start('http://www.heise.de/', () => {
const headlines = casper.evaluate(() => {
const h2s = document.getElementsByTagName('h2');
const result = new Array();
for (let i = 0; i < h2s.length; i++) {
result[i] = h2s[i].textContent;
}
return result;
});
test.assertEquals(
headlines.length,
6,
'hat die richtige Anzahl an Überschriften der Ebene 2'
);
});
casper.run(() => {
test.done();
});
});
Listing 6.45 Zugriff auf das DOM mit CasperJS
6.6 Zusammenfassung und Ausblick In diesem Kapitel haben Sie eine Auswahl von Tools und Bibliotheken für das Testen von JavaScript-Code kennengelernt. Sie wissen jetzt, wie Sie Unit-Tests erstellen, Test-Doubles verwenden und wie Sie die Testabdeckung Ihrer Unit-Tests ermitteln. Sie haben sowohl gesehen, wie sich die Tools auf der Kommandozeile verwenden lassen, als auch, wie Sie sie in den Build-Prozess integrieren. Des Weiteren habe ich Ihnen gezeigt, wie Sie JavaScript-Code mit Abhängigkeiten zum DOM testen und auf Basis von PhantomJS und CasperJS Funktionstests von Webseiten erstellen. Im Folgenden noch eine kurze Übersicht über die wichtigsten Punkte, die Sie aus diesem Kapitel mitnehmen sollten. Bei der testgetriebenen Entwicklung formuliert man vor der Implementierung einer neuen Komponente zunächst in einem Unit-Test über Assertions, was die neue Komponente leisten muss. Eine Iteration besteht bei der testgetriebenen Entwicklung aus folgenden fünf Schritten: Test schreiben, Test ausführen (schlägt fehl), Komponente implementieren, Test ausführen (besteht), Refactoring der Komponente. Ein einzelner Unit-Test besteht aus vier Phasen: In der SetupPhase werden Initialisierungsarbeiten ausgeführt, in der ExercisePhase wird die zu testende Komponente (Funktion/Methode) ausgeführt, in der Verify-Phase werden die tatsächlichen Ergebnisse mit den erwarteten Ergebnissen verglichen und in der Teardown-Phase Aufräumarbeiten durchgeführt.
In JavaScript gibt es viele verschiedene Unit-Testing-Frameworks, von denen ich Ihnen in diesem Kapitel einige vorgestellt habe: QUnit, das sich besonders gut für das Testen von clientseitigem JavaScript eignet, mocha, das sich besonders für das Testen von serverseitigem JavaScript eignet, sowie das von Facebook entwickelte Jest, das vor allem für das Testen von ReactAnwendungen zum Einsatz kommt. Alle Frameworks lassen sich jedoch auch für das Testen beliebigen JavaScript-Codes verwenden. Alle der genannten Testing-Tools können in die Build-Tools Grunt und Gulp integriert werden. Test-Doubles dienen dazu, solche Komponenten, von denen eine zu testende Komponente abhängig ist (Datenbank, Webservice etc.), während des Tests zu ersetzen. Es gibt verschiedene Arten von Test-Doubles: Test-Spies, über die sich indirekte Ausgaben der zu testenden Komponente abfangen lassen, Test-Stubs, über die sich indirekte Eingaben der zu testenden Komponente definieren lassen, und Mock-Objekte, über die indirekte Ausgaben der zu testenden Komponente überprüft werden können. In JavaScript ist Sinon.js die bekannteste Bibliothek zum Erstellen von Test-Doubles. Gerade bei komplexerem Quelltext ist es sinnvoll, über Testabdeckung herauszufinden, welcher Quelltext von den UnitTests abgedeckt ist. Als Beispiel haben Sie die JavaScriptBibliotheken Blanket.js und Istanbul kennengelernt, die sich relativ einfach in mocha integrieren lassen. JavaScript-Code, der Abhängigkeiten zu dem Document Object Model (DOM) einer Webseite hat, wird in sogenannten DOM-Tests überprüft.
Komplexere Interaktionen und Workflows lassen sich dagegen im Rahmen von Funktionstests durch Tools wie PhantomJS und CasperJS überprüfen. Sollten Sie sich eingehender mit der testgetriebenen Entwicklung in JavaScript beschäftigen wollen, empfehle ich Ihnen außerdem das Buch Testgetriebene Entwicklung mit JavaScript von Sebastian Springer, das im dpunkt.Verlag erschienen ist.
7 Fortgeschrittene Konzepte der objektorientierten Programmierung Die Grundlagen der objektorientierten Programmierung in JavaScript kennen Sie bereits aus Kapitel 3, »Objektorientierte Programmierung mit JavaScript«. Jetzt möchte ich Ihnen einige fortgeschrittene Konzepte der objektorientierten Programmierung vorstellen und zeigen, wie Sie sie in JavaScript umsetzen. Den Anfang machen die sogenannten SOLID-Konzepte, gefolgt von Fluent APIs, die in JavaScript sowohl synchron als auch asynchron implementiert werden können. Den zweiten Teil des Kapitels nimmt die sogenannte aspektorientierte Programmierung ein.
7.1 SOLID Unter dem Begriff Clean Code versteht man in der Softwareentwicklung Prinzipien und Best Practices für sauberen, sprich übersichtlichen und leicht verständlichen Quelltext, die unter anderem in dem gleichnamigen Buch von Robert C. Martin beschrieben werden. Im Folgenden zeige ich Ihnen, wie Sie einen Teil dieser Prinzipien, die sogenannten SOLID-Prinzipien, in JavaScript anwenden. Das Akronym SOLID steht dabei für folgende fünf Prinzipien der objektorientierten Programmierung: S für Single-Responsibility-Prinzip
O für Open-Closed-Prinzip L für Liskovsches Substitutionsprinzip I für Interface-Segregation-Prinzip D für Dependency-Inversion-Prinzip Folgt man diesen fünf Prinzipien, führt dies zu einer Struktur von Klassen (und Interfaces), die zum einen nur lose miteinander gekoppelt sind und zum anderen einen starken inneren Zusammenhang aufweisen. Diese beiden Eigenschaften wiederum erhöhen sowohl die Wiederverwendbarkeit als auch die Testbarkeit des Codes. Oder anders gesagt: Durch Anwendung der fünf Prinzipien verhindern Sie eine starke Kopplung zwischen Klassen, welche wiederum eine schlechte Wiederverwendbarkeit und schlechte Testbarkeit mit sich bringen würde. 7.1.1 Single-Responsibility-Prinzip (SRP)
Das Single-Responsibility-Prinzip besagt, dass eine Klasse nur eine Verantwortlichkeit bzw. Zuständigkeit haben sollte. Klassen mit vielen verschiedenen Verantwortlichkeiten dagegen sollten vermieden werden. Ein Negativbeispiel hierzu zeigt Listing 7.1: Die dort gezeigte Klasse Album ist nicht nur eine einfache Datenklasse, sondern enthält auch Logik für das Speichern in das Dateisystem. Die Klasse hat also sowohl die Verantwortlichkeit, Musikalben zu repräsentieren, als auch die Verantwortlichkeit, Musikalben in das Dateisystem zu speichern. Das wiederum erhöht die Wahrscheinlichkeit, dass die Klasse bei neuen Anforderungen angepasst werden muss: sowohl, wenn sich das Datenmodell an sich ändert, als auch, wenn sich der Prozess des Speicherns ändert.
const fs = require('fs');
const path = require('path');
module.exports = class Album {
constructor({artist, title, year}) {
this._artist = artist;
this._title = title;
this._year = year;
}
// Setter und Getter aus
// Gründen der Lesbarkeit nicht
// abgebildet.
toJSON() {
return {
artist: this.artist,
title: this.title,
year: this.year,
}
}
toFileName() {
return `${this.artist}_${this._title}`;
}
save() {
const fileName = this.toFileName();
const file = path.join(__dirname, fileName);
return new Promise((resolve, reject) => {
fs.writeFile(file, JSON.stringify(this, null, 2),
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
}
// Datei start.js
const Album = require('./Album');
const album = new Album({
artist: 'The Doors',
title: 'Strange Days',
year: 1967
});
album
.save()
.then(() => console.log('Album saved'))
.catch((error) => console.error(error));
Listing 7.1 Code, der dem Single-Responsibility-Prinzip widerspricht
Das Problem lässt sich wie in Listing 7.2 gezeigt einfach lösen, indem Sie die Methoden, die für das Speichern verantwortlich sind, in eine eigene Klasse (AlbumRepository) auslagern. Damit hat jede Klasse eine einzige Verantwortlichkeit. Hinzu kommt, dass die beiden Verantwortlichkeiten nicht mehr aneinander gekoppelt sind. Mit anderen Worten: Die Art und Weise, wie Musikalben gespeichert werden, ist nicht mehr an das Musikalbum selbst gekoppelt und kann damit leichter ersetzt werden – beispielsweise könnten Sie eine weitere Klasse AlbumMongoDBRepository implementieren, die Musikalben nicht in das Dateisystem schreibt, sondern in eine MongoDB speichert. Das wäre mit dem Code in Listing 7.1 nicht so ohne weiteres möglich. // Datei Album.js
module.exports = class Album {
constructor({artist, title, year}) {
this._artist = artist;
this._title = title;
this._year = year;
}
// Setter und Getter aus
// Gründen der Lesbarkeit nicht
// abgebildet.
toJSON() {
return {
artist: this.artist,
title: this.title,
year: this.year,
}
}
}
// Datei AlbumRepository.js
const fs = require('fs');
const path = require('path');
module.exports = class AlbumRepository {
toFileName(album) {
return `${album.artist}_${album.title}`;
}
save(album) {
const fileName = this.toFileName(album);
const file = path.join(__dirname, fileName);
return new Promise((resolve, reject) => {
fs.writeFile(file, JSON.stringify(album, null, 2),
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
}
// Datei start.js
const Album = require('./Album');
const AlbumRepository = require('./AlbumRepository');
const album = new Album({
artist: 'The Doors',
title: 'Strange Days',
year: 1967
});
const repository = new AlbumRepository();
repository
.save(album)
.then(() => console.log('Album saved'))
.catch((error) => console.error(error));
Listing 7.2 Code, der sich an das Single-Responsibility-Prinzip hält
7.1.2 Open-Closed-Prinzip (OCP)
Das Open-Closed-Prinzip besagt, dass Klassen so aufgebaut sein sollten, dass sie »offen für Erweiterungen, aber geschlossen für Modifizierungen sind«. Mit anderen Worten: Neue Funktionalitäten sollten zwar hinzugefügt werden können, jedoch ohne dabei den bestehenden Code anpassen zu müssen.
Wie Sie es nicht machen sollten, zeigt Listing 7.2. Die hier gezeigte Klasse AlbumService stellt die Methode handleRequest() zur Verfügung, die Anfragen an den Service entgegennimmt und eine entsprechende Antwort zurückgibt. Die Methode erwartet dazu als Parameter ein Objekt, das die entsprechende Anfrage repräsentiert, prüft dann über die Eigenschaft type, um welchen Typ von Anfrage es sich handelt, und führt abhängig von diesem Typ die Behandlung der Anfrage durch (im Beispiel lediglich durch eine Konsolenausgabe verkürzt dargestellt). Was auf den ersten Blick sinnvoll erscheinen mag, ist bei genauem Hinsehen weniger optimal: Fügt man neben den im Beispiel gezeigten Klassen SaveAlbumRequest, LoadAlbumRequest und UpdateAlbumRequest weitere Request-Klassen hinzu, muss jedes Mal die Klasse AlbumService angepasst und die Methode handleRequest() um einen weiteren if-Zweig ergänzt werden. Das widerspricht jedoch dem Open-Closed-Prinzip: Die Klasse AlbumService ist nicht geschlossen für Modifizierungen, sondern muss selbst modifiziert werden, damit die Anwendung um weitere Anfragetypen erweitert werden kann. // Datei Album.js
module.exports = class Album {
constructor({ id, artist, title, year }) {
this._artist = artist;
this._title = title;
this._year = year;
}
// Setter und Getter aus
// Gründen der Lesbarkeit nicht
// abgebildet.
toJSON() {
return {
artist: this.artist,
title: this.title,
year: this.year
};
}
};
// Datei Requests.js
class Request {
constructor(type, id = Math.random()) {
this.id = id;
this.type = type;
}
}
class AlbumRequest extends Request {
constructor(album, type) {
super(type);
}
}
class SaveAlbumRequest extends AlbumRequest {
constructor(album) {
super(album, 'save');
}
}
class LoadAlbumRequest extends AlbumRequest {
constructor(album) {
super(album, 'load');
}
}
class UpdateAlbumRequest extends AlbumRequest {
constructor(album) {
super(album, 'update');
}
}
module.exports = {
LoadAlbumRequest,
SaveAlbumRequest,
UpdateAlbumRequest
};
// Datei AlbumService.js
module.exports = class AlbumService {
handleRequest(request) {
const response = {
requestId: request.id
};
// Schlecht erweiterbar:
switch (request.type) {
case 'save':
console.log('Saving album');
response.result = {
saved: true
};
break;
case 'load':
console.log('Loading album');
response.result = {
loaded: true
};
break;
case 'update':
console.log('Updating album');
response.result = {
updated: true
};
break;
}
return Promise.resolve(response);
}
};
// Datei start.js
const Album = require('./Album');
const AlbumService = require('./AlbumService');
const {
LoadAlbumRequest,
SaveAlbumRequest,
UpdateAlbumRequest
} = require('./Requests');
const album = new Album({
artist: 'The Doors',
title: 'Strange Days',
year: 1967
});
const service = new AlbumService();
(async () => {
const response1 = await service.handleRequest(new SaveAlbumRequest(album));
console.log(response1);
const response2 = await service.handleRequest(new LoadAlbumRequest(album));
console.log(response2);
const response3 = await service.handleRequest(new UpdateAlbumRequest(album));
console.log(response3);
})();
Listing 7.3 Code, der dem Open-Closed-Prinzip widerspricht
Das Problem lässt sich für den vorliegenden Fall wie in Listing 7.3 gezeigt über die Anwendung des Strategy-Entwurfsmusters lösen (siehe Kapitel 8, »Die Entwurfsmuster der Gang of Four«). Für jeden
Request-Typ wird dazu eine eigene Handler-Klasse erstellt (SaveAlbumRequestHandler, LoadAlbumRequestHandler und UpdateAlbumRequestHandler). Von diesen Request-Handlern verwaltet die Klasse AlbumService in einer internen Map jeweils eine Objektinstanz (der Request-Typ ist dabei der Schlüssel, die jeweilige Handler-Instanz der Wert in dieser Map). Innerhalb von handleRequest() wird dann für den jeweils eingehenden Request anhand des Typs der zuständige Request-Handler ermittelt und dessen Methode handle() mit dem Request-Objekt aufgerufen. Auf diese Weise lassen sich beliebige weitere Request-Typen hinzufügen, ohne dass die Serviceklasse angepasst werden müsste. // Datei RequestHandlers.js
class RequestHandler {
handleRequest(request) {}
}
class SaveAlbumRequestHandler extends RequestHandler {
handle(request) {
console.log('saving album');
return {
saved: true
};
}
}
class LoadAlbumRequestHandler extends RequestHandler {
handle(request) {
console.log('loading album');
return {
loaded: true
};
}
}
class UpdateAlbumRequestHandler extends RequestHandler {
handle(request) {
console.log('updating album');
return {
updated: true
};
}
}
module.exports = {
LoadAlbumRequestHandler,
SaveAlbumRequestHandler,
UpdateAlbumRequestHandler
};
// Datei AlbumService.js
const {
LoadAlbumRequestHandler,
SaveAlbumRequestHandler,
UpdateAlbumRequestHandler
} = require('./RequestHandlers');
module.exports = class AlbumService {
constructor() {
this._requestHandlers = new Map();
this._requestHandlers.set('save', new SaveAlbumRequestHandler());
this._requestHandlers.set('load', new LoadAlbumRequestHandler());
this._requestHandlers.set('update', new UpdateAlbumRequestHandler());
}
handleRequest(request) {
const response = {
requestId: request.id
};
const requestHandler = this._requestHandlers.get(request.type);
if (requestHandler) {
response.result = requestHandler.handle(request);
}
return Promise.resolve(response);
}
};
// Datei start.js
const Album = require('./Album');
const AlbumService = require('./AlbumService');
const {
LoadAlbumRequest,
SaveAlbumRequest,
UpdateAlbumRequest
} = require('./Requests');
const album = new Album({
artist: 'The Doors',
title: 'Strange Days',
year: 1967
});
const service = new AlbumService();
(async () => {
const response1 = await service.handleRequest(new SaveAlbumRequest(album));
console.log(response1);
const response2 = await service.handleRequest(new LoadAlbumRequest(album));
console.log(response2);
const response3 = await service.handleRequest(new UpdateAlbumRequest(album));
console.log(response3);
})();
Listing 7.4 Code, der sich an das Open-Closed-Prinzip hält
Hinweis Wenn Sie es sehr genau nehmen, müssen Sie die Serviceklasse auch in der neuen Variante für jeden neuen Request-Typ anpassen, da jede neue Request-Handler-Klasse importiert und der Map hinzugefügt werden muss. Alternativ könnten Sie daher der Serviceklasse die Map auch als Konstruktorparameter übergeben oder die Request-Handler-Klassen dynamisch laden, um die Kopplung zwischen Serviceklasse und Request-HandlerKlassen zu entfernen.
7.1.3 Liskovsches Substitutionsprinzip (LSP)
Das Liskovsche Substitutionsprinzip, auch Ersetzbarkeitsprinzip genannt, bezieht sich auf die Vererbung von Klassen (bzw. Interfaces) und besagt, dass bei der Implementierung von Unterklassen (bzw. Subtypen) darauf geachtet werden sollte, durch die erweiterte Funktionalität die Basisfunktionalität der vererbenden Klasse nicht zu verändern. Dazu ein klassisches Negativbeispiel: Listing 7.4 zeigt die zwei Klassen zur Repräsentation von Rechtecken (Klasse Rectangle) und Quadraten (Klasse Square). Da Quadrate eine Sonderform von Rechtecken sind (nämlich genau solche Rechtecke, bei denen alle Seiten gleich lang sind), wird die Klasse Square als Unterklasse von Rectangle modelliert. Um sicherzustellen, dass Quadrate immer gleich lange Seiten haben, werden zudem die beiden Setter-
Methoden width() und height() überschrieben, so dass in beiden Fällen sowohl Breite als auch Höhe des Quadrats gesetzt werden. Diese Modellierung führt allerdings dazu, dass Instanzen der Klasse Square nicht an allen Stellen verwendet werden können, an denen Instanzen der Klasse Rectangle erwartet werden. Der in Listing 7.4 weiter unten gezeigte Unit-Test macht dies deutlich: Hier wird für eine Rectangle-Instanz und für eine Square-Instanz überprüft, ob nach Setzen einer Höhe von 7 und einer Breite von 8 die Fläche der Rechtecke jeweils dem erwarteten Wert 56 entspricht. Da für den Test angenommen wird, dass Quadrate das gleiche Verhalten haben wie Rechtecke, wird in beiden Fällen also das gleiche Ergebnis geprüft. Allerdings besteht der Test nur für die Rectangle-Instanz, nicht aber für die Square-Instanz. Zugegebenermaßen erscheint das Vorgehen etwas naiv, da ja bekannt sein sollte, dass die Square-Instanz sich intern (in den Methoden width() und height()) anders verhält als die Rectangle-Instanz. Allerdings liegt genau da das Problem: In der Praxis ist das interne Verhalten von Klassen dem Nutzer einer Klasse nicht zwangsweise bekannt. Für das vorliegende Beispiel könnte also durchaus davon ausgegangen werden, dass sich Square-Instanzen genauso verhalten wie Rectangle-Instanzen. // Datei Rectangle.js
module.exports = class Rectangle {
constructor() {
this._width = 0;
this._height = 0;
}
get width() {
return this._width;
}
set width(width) {
this._width = width;
}
get height() {
return this._height;
}
set height(height) {
this._height = height;
}
get area() {
return this.width * this.height;
}
}
// Datei Square.js
const Rectangle = require('./Rectangle');
module.exports = class Square extends Rectangle {
get width() {
return this._width;
}
set width(width) {
this._width = width;
this._height = width;
}
get height() {
return this._height;
}
set height(height) {
this._height = height;
this._width = height;
}
}
// Datei RectangleTest.js
const assert = require('assert');
const Rectangle = require('./Rectangle');
const Square = require('./Square');
describe('Rectangle', () => {
describe('area()', () => {
it('should return the area based on heigth and width', () => {
// Test mit Rectangle
const rectangle = new Rectangle();
rectangle.height = 7;
rectangle.width = 8;
assert.equal(rectangle.area, 56);
});
it('should return the area based on heigth and width', () => {
// Test mit Square
const rectangle = new Square();
rectangle.height = 7;
rectangle.width = 8;
assert.equal(rectangle.area, 56); // Schlägt fehl
});
});
});
Listing 7.5 Code, der dem Liskovschen Substitutionsprinzip widerspricht
Um solchen Missverständnissen bei der Verwendung der Klassen Rectangle und Square vorzubeugen, sollte die Klassenhierarchie geändert und beide Klassen wie in Listing 7.5 zu sehen auf einer Ebene angeordnet werden. Da Square-Instanzen sich offensichtlich anders verhalten als Rectangle-Instanzen, sollte die Klasse Square also nicht als Unterklasse von Rectangle-Klasse implementiert werden. Um trotzdem zu modellieren, dass beide Klasse geometrische Formen repräsentieren, bietet es sich an, außerdem eine gemeinsame Oberklasse (Shape) zu ergänzen. // Datei Shape.js
module.exports = class Shape {
}
// Datei Rectangle.js
const Shape = require('./Shape');
module.exports = class Rectangle extends Shape {
constructor(width, height) {
super();
this._width = width;
this._height = height;
}
get width() {
return this._width;
}
set width(width) {
this._width = width;
}
get height() {
return this._height;
}
set height(height) {
this._height = height;
}
get area() {
return this.width * this.height;
}
}
// Datei Square.js
const Shape = require('./Shape');
module.exports = class Square extends Shape {
constructor(length) {
super();
this._length = length;
}
get length() {
return this._length;
}
set length(length) {
this._length = length;
}
get area() {
return this.length * this.length; }
}
Listing 7.6 Code, der sich an das Liskovsche Substitutionsprinzip hält
7.1.4 Interface-Segregation-Prinzip (ISP)
Das Interface-Segregation-Prinzip besagt, dass Softwarekomponenten nicht von Interfaces abhängig sein sollten, die sie nicht benötigen. Oder anders gesagt: Interfaces mit vielen
Methoden sollten Sie vermeiden und sie stattdessen in mehrere kleinere Interfaces aufteilen, um zu verhindern, dass implementierende Klassen unter Umständen Methoden implementieren müssen, die sie eigentlich gar nicht benötigen. Da es in JavaScript das Konzept von Interfaces nicht gibt, fällt es jedoch schwierig, das Interface-Segregation-Prinzip hier anzuwenden, weswegen ich an dieser Stelle auf Beispiele verzichte. 7.1.5 Dependency-Inversion-Prinzip (DIP)
Das letzte der fünf SOLID-Prinzipien, das Dependency-InversionPrinzip, hat jedoch durchaus Berechtigung bei der Entwicklung von JavaScript-basierten Anwendungen. Es bezieht sich auf die Abhängigkeit von Modulen und besagt, dass Module höherer Ebenen (High-Level Modules) nicht von Modulen niedrigerer Ebenen (Low-Level Modules) abhängig sein sollten. Listing 7.6 zeigt den Quelltext für zwei verschiedene Module: eines für die Persistierung von Daten, ein weiteres, das Serviceklassen zur Verfügung stellt, die auf das Persistenzmodul zugreifen. Letzteres wäre übertragen auf das DIP also das »Low Level Module«, das Servicemodul dagegen das »High Level Module«, das eine direkte Abhängigkeit zu dem Persistenzmodul hat und damit stark gekoppelt ist. // Datei persistence/AlbumRepository.js
module.exports = class AlbumRepository {
save(album) {
throw new Error('Method save() must be implemented by subclass.');
}
};
// Datei persistence/FileAlbumRepository.js
const AlbumRepository = require('./AlbumRepository');
module.exports = class FileAlbumRepository extends AlbumRepository {
save(album) {
console.log('Saving album to file.');
}
}
// Datei persistence/MongoDBAlbumRepository.js
const AlbumRepository = require('./AlbumRepository');
module.exports = class MongoDBAlbumRepository extends AlbumRepository {
save(album) {
console.log('Saving album to MongoDB.');
}
}
// Datei services/AlbumService.js
const Album = require('../model/Album');
const MongoDBAlbumRepository = require('../persistence/MongoDBAlbumRepository');
module.exports = class AlbumService {
constructor() {
this._repository = new MongoDBAlbumRepository();
}
createAlbum(artist, title, year) {
const album = new Album({artist, title, year});
this._repository.save(album);
}
}
// Datei example.js
const AlbumService = require('./services/AlbumService');
const service = new AlbumService();
service.createAlbum('Deep Purple', 'Made in Japan', 1972);
Listing 7.7 Code, der dem Dependency-Inversion-Prinzip widerspricht
Besser ist es, diese Abhängigkeit von außerhalb definieren zu können, beispielsweise wie in Listing 7.7 gezeigt als Konstruktorparameter zu übergeben. Eine noch losere Kopplung dagegen erreichen Sie durch die Verwendung von DependencyInjection-Bibliotheken wie InversifyJS (https://github.com/inversify/InversifyJS/) und BottleJS (https://github.com/young-steveo/bottlejs).
// Andere Klassen wie gehabt
// Datei services/AlbumService.js
const Album = require('../model/Album');
module.exports = class AlbumService {
constructor(repository) {
this._repository = repository;
}
createAlbum(artist, title, year) {
const album = new Album({artist, title, year});
this._repository.save(album);
}
}
// Datei example.js
const AlbumService = require('./services/AlbumService');
const AlbumRepository = require('./persistence/MongoDBAlbumRepository');
const repository = new AlbumRepository();
const service = new AlbumService(repository);
service.createAlbum('Deep Purple', 'Made in Japan', 1972);
Listing 7.8 Code, der sich an das Dependency-Inversion-Prinzip hält
7.2 Fluent APIs Unter einer Fluent API versteht man in der Softwareentwicklung eine Technik für die Entwicklung von programmatischen Schnittstellen, durch die sich fast so flüssig (daher der Name) wie in natürlicher Sprache programmieren lässt. 7.2.1 Einführung
Während eine »normale« programmatische API in JavaScript wie folgt aussieht ... const ExampleClass = require('./ExampleClass');
const instance = new ExampleClass();
instance.doSomething();
instance.doSomethingElse();
instance.doAnotherThing();
... werden bei einer Fluent API die Methodenaufrufe hintereinandergeschaltet: const ExampleClass = require('./ExampleClass');
const instance = new ExampleClass();
instance
.doSomething()
.doSomethingElse()
.doAnotherThing();
Listing 7.9 Beispiel für eine Fluent API
Frameworks und Bibliotheken wie beispielsweise supertest (https://github.com/visionmedia/supertest), das Test-Framework für HTTP-Schnittstellen, stellen ihre Funktionalität in Form von Fluent APIs zur Verfügung (Listing 7.9). describe('GET /user', () => {
it('respond with json', (done) => {
request(app)
.get('/user')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done);
});
});
Listing 7.10 Fluent API am Beispiel von supertest
Im Folgenden zeige ich Ihnen, wie Sie auf Basis einer bestehenden (Nicht-Fluent-)API im ersten Schritt eine einfache synchrone Fluent API entwickeln und anschließend darauf aufbauend eine asynchrone Fluent API. 7.2.2 Synchrone Fluent APIs
Als Ausgangspunkt dient die in Listing 7.10 gezeigte Klasse SyncControl, die eine Fernbedienung für einen Musicplayer repräsentiert und die (synchronen) Methoden play() zum Starten der Wiedergabe, pause() zum Pausieren, next() und previous() zum Auswählen des nächsten bzw. vorherigen Titels, select() zum Selektieren eines bestimmten Titels und stop() zum Stoppen der Wiedergabe bereitstellt. Listing 7.10 zeigt eine einfache Implementierung, die zu Demonstrationszwecken lediglich Konsolenausgaben innerhalb der Methoden enthält. Bei der Verwendung dieser Klasse müssen Sie, wie schon im Eingangsbeispiel gezeigt und hier erneut zu sehen, vor jedem Methodenaufruf die Objektinstanz (control) mit angeben. // Datei SyncControl.js
module.exports = class SyncControl {
next() {
console.log('Next song');
}
pause() {
console.log('Pause');
}
play() {
console.log('Play');
}
previous() {
console.log('Previous song');
}
select(track) {
console.log(`Select track ${track}`);
}
stop() {
console.log('Stop');
}
}
// Datei SyncControl.example.js
const SyncControl = require('./SyncControl');
const control = new SyncControl();
control.play();
control.pause();
control.select(7);
control.play();
control.next();
control.previous();
control.play();
control.stop();
Listing 7.11 Die Klasse »SyncControl« ohne Fluent API
Um die Klasse SyncControl so weit zu ändern, dass sie statt einer normalen API eine (synchrone) Fluent API bereitstellt, ist im Wesentlichen schnell erledigt: Was wir ja möchten, ist, auf dem Rückgabewert jeder Methode der Klasse erneut eine Methode der Klasse aufzurufen, ohne wie zuvor den erneuten Umweg über die Objektinstanz zu gehen. Die Lösung: Jede Methode liefert als Rückgabewert einfach die Objektinstanz direkt zurück, sprich, es wird einfach an das Ende jeder Methode die Anweisung return this; angehängt (siehe Klasse FluentSyncControl in Listing 7.11). // Datei FluentSyncControl.js
module.exports = class FluentSyncControl {
next() {
console.log('Next song');
return this;
}
pause() {
console.log('Pause');
return this;
}
play() {
console.log('Play');
return this;
}
previous() {
console.log('Previous song');
return this;
}
select(track) {
console.log(`Select track ${track}`);
return this;
}
stop() {
console.log('Stop');
return this;
}
}
// Datei FluentSyncControl.example.js
const FluentSyncControl = require('./FluentSyncControl');
const control = new FluentSyncControl();
control
.play()
.pause()
.select(7)
.play()
.next()
.previous()
.play()
.stop();
Listing 7.12 Die Klasse »FluentSyncControl« mit synchroner Fluent API
Hinweis
Eine Einschränkung hat das beschriebene Vorgehen natürlich: Es funktioniert nur für solche Methoden, die nicht bereits einen anderen Rückgabewert haben. Diesen Fall müssten Sie bei der Implementierung einer Fluent API anders lösen, beispielsweise Rückgabewerte innerhalb der Objektinstanz zwischenspeichern und dann über andere Methoden abrufen (die nicht Teil der Fluent API sind).
7.2.3 Asynchrone Fluent APIs mit Callbacks
So weit, so gut. Die Implementierung einer synchronen Fluent API ist also relativ einfach und unkompliziert. Anders sieht es aus, wenn die Methoden der API nicht synchron, sondern asynchron sind. Die Implementierung einer solch asynchronen Fluent API ist schon etwas aufwendiger. Listing 7.12 zeigt anhand der Klasse FluentAsyncControlWrong, wie es nicht funktioniert. Jeder Methode wurde hier zunächst ein Callback-Parameter hinzugefügt und über einen Aufruf von setTimeout() asynchrones Verhalten simuliert. Ansonsten wurde alles belassen wie zuvor. Und genau darin liegt das Problem: Es handelt sich zwar weiterhin um eine Fluent API, das heißt, die Methoden lassen sich wie zuvor in Listing 7.11 hintereinandergeschaltet aufrufen. Allerdings laufen die Methodenaufrufe dabei synchron ab, nicht asynchron. Das wiederum führt dazu, dass die Aufrufe der API-Methoden vor den Ergebnissen der asynchronen Aufrufe abgeschlossen sind und damit vor dem Aufruf der entsprechenden Callback-Handler. Die Ausgabe lautet daher: Play
Pause
Select track 7
Play
Next song
Previous song
Play
Stop
Play callback
Pause callback
Select track callback
Play callback
Next song callback
Previous song callback
Play callback
Stop callback
// Datei FluentSyncControlWrong.js
module.exports = class FluentAsyncControlWrong {
next(callback) {
console.log('Next song');
setTimeout(() => {
console.log('Next song callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
pause(callback) {
console.log('Pause');
setTimeout(() => {
console.log('Pause callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
play(callback) {
console.log('Play');
setTimeout(() => {
console.log('Play callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
previous(callback) {
console.log('Previous song');
setTimeout(() => {
console.log('Previous song callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
select(track, callback) {
console.log(`Select track ${track}`);
setTimeout(() => {
console.log('Select track callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
stop(callback) {
console.log('Stop');
setTimeout(() => {
console.log('Stop callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
return this;
}
}
// Datei FluentSyncControlWrong.example.js
const FluentAsyncControlWrong = require('./FluentAsyncControlWrong');
const control = new FluentAsyncControlWrong();
control
.play()
.pause()
.select(7)
.play()
.next()
.previous()
.play()
.stop();
Listing 7.13 Falsche Implementierung einer asynchronen Fluent API
Die richtige Implementierung einer asynchronen Fluent API ist dagegen in Listing 7.13 zu sehen. Die Klasse FluentAsyncControl verzichtet im Gegensatz zu der zuvor gezeigten Klasse FluentAsyncControlWrong auf das return this; am Ende
jeder Methode. Stattdessen erstellt die neue Helfermethode toFluent() aus einer Instanz von FluentAsyncControl ein Objekt, das eine Fluent API definiert. Die Methode erwartet als ersten Parameter ein Promise-Objekt, das den jeweils zuvor ausgeführten asynchronen Aufruf repräsentiert, sowie als zweiten Parameter die jeweilige Objektinstanz. Als Rückgabewert liefert die Methode ein neues Objekt, das die gleichen Methoden bereitstellt wie die zugrundeliegende Klasse FluentAsyncControl. Jede dieser Wrapper-Methoden wiederum ruft erneut die Methode toFluent() auf und übergibt dabei ebenfalls ein Promise-Objekt, das erst erfüllt wird, wenn das eingangs übergebene Promise-Objekt erfüllt und die jeweils gewrappte asynchrone Methode abgeschlossen wurde. Da nun sichergestellt ist, dass bei jedem Methodenaufruf zunächst die zuvor aufgerufene asynchrone Methode abgeschlossen ist, lautet die Ausgabe des Beispielsprogramms nun: Play
Play callback
Pause
Pause callback
Select track 7
Select track callback
Play
Play callback
Next song
Next song callback
Previous song
Previous song callback
Play
Play callback
Stop
Stop callback
// Datei FluentAsyncControl.js
class FluentAsyncControl {
next(callback) {
console.log('Next song');
setTimeout(() => {
console.log('Next song callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
pause(callback) {
console.log('Pause');
setTimeout(() => {
console.log('Pause callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
play(callback) {
console.log('Play');
setTimeout(() => {
console.log('Play callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
previous(callback) {
console.log('Previous song');
setTimeout(() => {
console.log('Previous song callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
select(track, callback) {
console.log(`Select track ${track}`);
setTimeout(() => {
console.log('Select track callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
stop(callback) {
console.log('Stop');
setTimeout(() => {
console.log('Stop callback');
if (typeof callback === 'function') {
callback();
}
}, 500);
}
}
const toFluent = (previousActions = Promise.resolve(), instance) => {
return {
next: () =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.next(resolve);
})), instance),
pause: () =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.pause(resolve);
})), instance),
play: () =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.play(resolve);
})), instance),
previous: () =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.previous(resolve);
})), instance),
select: (track) =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.select(track, resolve);
})), instance),
stop: () =>
toFluent(previousActions
.then(() => new Promise((resolve, reject) => {
instance.stop(resolve);
})), instance)
};
}
const build = () => {
const control = new FluentAsyncControl();
return toFluent(undefined, control);
}
module.exports.build = build;
// Datei FluentAsyncControl.example.js
const FluentAsyncControl = require('./FluentAsyncControl');
const control = FluentAsyncControl.build();
control
.play()
.pause()
.select(7)
.play()
.next()
.previous()
.play()
.stop();
Listing 7.14 Die Klasse »FluentAsyncControl« mit asynchroner Fluent API
Das Ganze lässt sich wie in Listing 7.14 gezeigt weiter optimieren, indem Sie das Wrappen der Methoden in eine weitere Helfermethode auslagern. Die Methode wrap() erhält als Parameter ein Promise-Objekt, die Objektinstanz sowie den Namen der zu wrappenden Methode und deren Parameter. ...
const wrap = (previousActions, instance, method, ...args) =>
previousActions.then(() => new Promise((resolve, reject) => {
instance[method](...args, resolve);
}));
const toFluent = (previousActions = Promise.resolve(), instance) => {
return {
next: () => toFluent(
wrap(previousActions, instance, 'next'),
instance
),
pause: () => toFluent(
wrap(previousActions, instance, 'pause'),
instance
),
play: () => toFluent(
wrap(previousActions, instance, 'play'),
instance
),
previous: () => toFluent(
wrap(previousActions, instance, 'previous'),
instance
),
select: (track) => toFluent(
wrap(previousActions, instance, 'select', track),
instance
),
stop: () => toFluent(
wrap(previousActions, instance, 'stop'),
instance
)
};
}
const build = () => {
const control = new FluentAsyncControlOptimized();
return toFluent(undefined, control);
}
module.exports.build = build;
Listing 7.15 Optimierte Version der asynchronen Fluent API
7.2.4 Asynchrone Fluent APIs mit Promises
Basiert die ursprüngliche API nicht auf Callbacks, sondern wie in Listing 7.15 zu sehen auf Promises, müssen Sie lediglich die wrap()Methode anpassen: const wrap = (previousActions, instance, method, ...args) =>
previousActions.then(() => instance[method](...args));
Oder alternativ unter Verwendung von async/await: const wrap = async (previousActions, instance, method, ...args) => {
await previousActions;
await instance[method](...args);
}
// Datei FluentAsyncControlPromises.js
class FluentAsyncControlPromises {
next() {
console.log('Next song');
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
pause() {
console.log('Pause');
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
play() {
console.log('Play');
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
previous() {
console.log('Previous song');
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
select(track) {
console.log(`Select track ${track}`);
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
stop() {
console.log('Stop');
return new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
}
}
const wrap = (previousActions, instance, method, ...args) =>
previousActions.then(() => instance[method](...args));
const toFluent = (previousActions = Promise.resolve(), instance) => {
return {
next: () => toFluent(
wrap(previousActions, instance, 'next'),
instance
),
pause: () => toFluent(
wrap(previousActions, instance, 'pause'),
instance
),
play: () => toFluent(
wrap(previousActions, instance, 'play'),
instance
),
previous: () => toFluent(
wrap(previousActions, instance, 'previous'),
instance
),
select: (track) => toFluent(
wrap(previousActions, instance, 'select', track),
instance
),
stop: () => toFluent(
wrap(previousActions, instance, 'stop'),
instance
)
};
}
const build = () => {
const control = new FluentAsyncControlPromises();
return toFluent(undefined, control);
}
module.exports.build = build;
Listing 7.16 Asynchrone Fluent API basierend auf Promises
Hinweis Die gezeigten APIs haben alle keine Grammatik: Es spielt keine Rolle, in welcher Reihenfolge die einzelnen Methodenaufrufe hintereinandergeschaltet werden. Dem gegenüber stehen APIs mit Grammatik, bei der die Reihenfolge der Methodenaufrufe wichtig ist. Aber auch dies ließe sich auf Basis des gezeigten Quelltextes realisieren, und zwar, indem abhängig vom jeweiligen Methodenaufruf ein anderes Objekt (mit anderen Methoden) zurückgegeben wird. Um beispielsweise zu verhindern, dass zweimal hintereinander die Methode start() aufgerufen werden kann, könnte nach dem Aufruf von start() ein Objekt zurückgegeben werden, das genau nicht über die Methode start() verfügt.
7.2.5 Zugriff auf Original-API
Möchten Sie optional weiterhin auf die Original-(Nicht-Fluent-)API zugreifen können, gehen Sie wie in Listing 7.16 vor und fügen dem Objekt, das von toFluent() zurückgegeben wird, einfach eine entsprechende Methode instance() hinzu, die die zugrundeliegende Objektinstanz zurückgibt. Auf dieser Instanz lassen sich dann die ursprünglichen Methoden aufrufen. // Datei FluentAsyncControlOriginalAPI.js
...
const wrap = async (previousActions, instance, method, ...args) => {
await previousActions;
await instance[method](...args);
}
const toFluent = (previousActions = Promise.resolve(), instance) => {
return {
instance: () => instance,
// Andere Methoden wie gehabt
};
}
const build = () => {
const control = new FluentAsyncControlOriginalAPI();
return toFluent(undefined, control);
}
module.exports.build = build;
// Datei FluentAsyncControlOriginalAPI.example.js
const FluentAsyncControlOriginalAPI = require('./FluentAsyncControlOriginalAPI');
const control = FluentAsyncControlOriginalAPI.build();
const originalAPI = control.instance()
// Verwendung der Fluent API
control
.play()
.pause()
.select(7)
.play()
.next()
.previous()
.play()
.stop();
// Verwendung der Original-API mit Promises
originalAPI
.play()
.then(() => originalAPI.pause())
.then(() => originalAPI.select(7))
.then(() => originalAPI.play())
.then(() => originalAPI.next())
.then(() => originalAPI.next())
.then(() => originalAPI.previous())
.then(() => originalAPI.play())
.then(() => originalAPI.stop());
// Verwendung der Original-API mit async/await
(async () => {
await originalAPI.play();
await originalAPI.pause();
await originalAPI.select(7);
await originalAPI.play();
await originalAPI.next();
await originalAPI.previous();
await originalAPI.play();
await originalAPI.stop();
})();
Listing 7.17 Asynchrone Fluent API mit Zugriff auf Original-API
7.2.6 Generische Fluent API Factory
Auf Basis des Gezeigten lässt sich mit ein paar wenigen Anpassungen eine generische Methode implementieren, die für beliebige asynchrone APIs eine entsprechende Fluent API erzeugt. Den entsprechenden Quelltext dazu sehen Sie in Listing 7.17. Neu sind hier lediglich die Funktion getObjectMethods(), die für eine Objektinstanz die definierten Methoden zurückgibt (ohne dabei jedoch auf Prototypen zu achten, hier müssten Sie die Funktion entsprechend anpassen), sowie die Funktion fluentify(), die die Umwandlung für eine gegebene Objektinstanz durchführt. Die Verwendung der Funktion zeigt Listing 7.18. const wrap = async (previousActions, instance, method, ...args) => {
await previousActions;
await instance[method](...args);
}
const getObjectMethods = (instance) => {
const prototype = Object.getPrototypeOf(instance);
const methods = Object.getOwnPropertyNames(prototype)
.filter(property => typeof prototype[property] === 'function')
.filter(property => property !== 'constructor');
return methods;
}
const API = (previousActions = Promise.resolve(), instance) => {
const methods = getObjectMethods(instance);
const result = {};
methods.forEach(method => {
result[method] = (...args) => API(wrap(previousActions, instance, method, ...args), instance)
});
return result;
}
const fluentify = (instance) => API(undefined, instance);
module.exports = fluentify;
Listing 7.18 Generische Bibliothek für Fluent APIs const AsyncControl = require('./AsyncControl');
const instance = new AsyncControl();
instance
.play()
.then(() => instance.pause())
.then(() => instance.select(7))
.then(() => instance.play())
.then(() => instance.next())
.then(() => instance.next())
.then(() => instance.previous())
.then(() => instance.play())
.then(() => instance.stop());
const fluentify = require('./fluentify');
const control = fluentify(instance);
control
.play()
.pause()
.select(7)
.play()
.next()
.previous()
.play()
.stop();
Listing 7.19 Verwendung der generischen Bibliothek
7.3 Aspektorientierte Programmierung in JavaScript Über die aspektorientierte Programmierung lassen sich bestimmte Funktionalitäten vom eigentlichen Anwendungscode trennen. 7.3.1 Einführung
Bei der aspektorientierten Programmierung (kurz AOP) handelt es sich um ein Programmierparadigma, das im Zusammenhang mit der objektorientierten Programmierung zum Einsatz kommt und deren Idee es ist, generische Funktionalitäten wie etwa Logging, Caching, Transaktionsmanagement etc. (sprich sogenannte CrossCutting Concerns oder im Jargon der aspektorientierten Programmierung auch Aspekte) vom eigentlichen Anwendungscode getrennt zu halten und den Code dadurch insgesamt klarer und modularer zu halten. Betrachten wir dazu ein einfaches Beispiel: die Ausgabe der Argumente, mit denen eine Methode zur Laufzeit aufgerufen wird. Dazu könnten Sie an den Anfang der jeweiligen Methode einen entsprechende Logging-Aufruf einbauen und diesen mit einem Feature Toggle versehen, um das Logging bei Bedarf an- und wieder abzuschalten. Eventuell greifen Sie dabei auch auf spezielle LoggingBibliotheken wie winston (https://github.com/winstonjs/winston) zurück, die in dieser Hinsicht deutlich flexibler sind als reine console.log()-Aufrufe. Aber all das ändert nichts an der Tatsache, dass der Logging-Code direkt im Anwendungscode steht. Der Code für den Aspekt Logging ist also wie in Abbildung 7.1 skizziert auf mehrere Stellen in der Anwendung verteilt. Bei der AOP dagegen werden wie in Abbildung 7.2 dargestellt Aspekte wie das
Logging getrennt von dem eigentlichen Anwendungscode gehalten und über spezielle Techniken mit diesem »verknüpft«.
Abbildung 7.1 Vermischung von Anwendungslogik und Cross-Cutting Concerns
Abbildung 7.2 AOP extrahiert den Code für Cross-Cutting Concerns
Auf diese Weise lassen sich einzelne Aspekte relativ flexibel einzelnen Klassen auch im Nachhinein hinzufügen (oder wieder daraus entfernen), ohne dass die jeweilige Klasse bzw. entsprechende Stelle im Code an sich geändert werden muss. Hinweis
Die erste Bibliothek (für Java), die aspektorientierte Programmierung ermöglichte, war AspectJ (https://eclipse.org/aspectj/). Ein weiteres prominentes Beispiel ist aber auch etwa das Spring-Framework. Bei diesen und anderen Frameworks werden in der Regel die generischen Funktionalitäten entweder über XML-Dateien oder über Annotationen definiert und konfiguriert und anschließend entweder über einen speziellen Compiler in den endgültigen Code kompiliert oder aber zur Laufzeit dynamisch dazugeladen (in beiden Fällen spricht man auch davon, die Aspekte in den Code zu »weben«, abgeleitet vom englischen Begriff »weaving«).
7.3.2 Begrifflichkeiten
Bevor wir uns anschauen, welche Möglichkeiten es gibt, AOP auch in JavaScript umzusetzen, seien zunächst noch einige Begrifflichkeiten erklärt, die bei AOP verwendet werden (siehe auch Abbildung 7.3): Aspect: Bezeichnet wie schon erwähnt eine generische Funktionalität wie beispielsweise Logging, Caching oder Transaktionsmanagement. Join Point: Bezeichnet ein Ereignis in der Ausführung eines Programms, beispielsweise »Ausführen eines Code-Blocks«, »Aufruf einer Methode«, »Initialisierung einer Klasse«, »Ausführen eines catch-Blocks« oder Ähnliches. Advice: Bezeichnet die Aktion, die von einem Aspekt ausgeführt werden soll, sobald ein bestimmter »Join Point« erreicht wurde (beispielsweise: »Gebe die Argumente des Methodenaufrufs auf die Konsole aus«).
Pointcut: Besteht aus einem oder mehreren »Join Points«, sprich, über »Pointcuts« lassen sich mehrere »Join Points« zusammenfassen. Zusätzlich gibt es eine Reihe verschiedener Typen von Advices: Before-Advice: ein Advice, der vor einem bestimmten Join Point ausgeführt wird und der beispielsweise Zugriff auf die Argumente eines Methodenaufrufs ermöglicht After-Returning-Advice: ein Advice, der nach einem bestimmten Join Point ausgeführt wird und der beispielsweise Zugriff auf den Rückgabewert einer Methode ermöglicht After-Throwing-Advice: ein Advice, der nach dem Werfen eines Fehlers ausgeführt wird und über den Sie Zugriff auf den Fehler haben Around Advice: ein Advice, der um einem bestimmten Join Point herum ausgeführt wird und über den Sie sowohl Zugriff auf die Argumente einer Methode als auch auf deren Rückgabewert haben
Abbildung 7.3 Begrifflichkeiten in der aspektorientierten Programmierung
Auf diese Weise lassen sich also Aspekte festlegen wie: »Immer, wenn die Methode x() der Klasse X() (= erster Join Point) oder die Methode y() der Klasse Y() (= zweiter Join Point) aufgerufen wird
(= zusammen ein Pointcut), gebe die Argumente auf die Konsole aus (= Advice).« 7.3.3 AOP durch Methodenneudefinition
Während es technisch gesehen in anderen Sprachen wie Java nicht ganz trivial ist, AOP zu implementieren (wenn man es von Hand und nicht mit Hilfe eines der genannten Frameworks machen möchte), ist es in JavaScript dank seiner Dynamik deutlich einfacher. Betrachten wir dazu im Folgenden als Ausgangspunkt die in Listing 7.19 gezeigte Klasse Calculator mit zwei Methoden sum() und prod(), die die Summe bzw. das Produkt zweiter Zahlen berechnen. Wie Sie im Listing sehen, enthalten beide Methoden Logging-Befehle, die eine entsprechende Meldung mit Namen der Methode und den jeweils übergebenen Argumenten auf die Konsole ausgeben. Wie bereits beschrieben, ist dies ein typischer Fall der Vermischung von Anwendungslogik und generischer Funktionalität (bzw. einem Cross-Cutting Concern). 'use strict';
class Calculator {
sum(x, y) {
console.log(`Aufruf von sum() mit den Argumenten ${x}, ${y}`);
return x + y;
}
prod(x, y) {
console.log(`Aufruf von prod() mit den Argumenten ${x}, ${y}`);
return x * y;
}
}
let calculator = new Calculator();
console.log(calculator.sum(5, 6));
console.log(calculator.prod(5, 6));
Listing 7.20 Hier wird Anwendungslogik und Logging gemischt.
Im Folgenden möchte ich nun zeigen, mit welchen Techniken sich Aspekte wie das Logging in JavaScript getreu der aspektorientierten Programmierung aus dem eigentlichen Anwendungscode extrahieren lassen. Dank der Tatsache, dass sich Objektmethoden in JavaScript zur Laufzeit überschreiben bzw. neu definieren lassen, ist die erste Technik relativ naheliegend: Die Idee dabei ist es, die Methoden, die mit einem Aspekt »verknüpft« werden sollen, zu überschreiben und dann innerhalb der neuen Methoden sowohl die Logik für den Aspekt auszuführen als auch die Originalmethode aufzurufen. Ein Beispiel dazu zeigt Listing 7.20 (wobei Sie den Code in einer realen Anwendung natürlich auf verschiedene Dateien verteilen würden). Die Methoden sum() und prod() werden hier jeweils durch neue Funktionen überschrieben, in denen zunächst über Method Borrowing auf die übergebenen Argumente zugegriffen wird, diese dann auf die Konsole ausgegeben werden und anschließend über apply() die jeweilige Originalmethode aufgerufen wird. // Datei Calculator.js
class Calculator {
sum(x, y) {
return x + y;
}
prod(x, y) {
return x * y;
}
}
const calculator = new Calculator();
// Anfang AOP-Code
// 1.) Originalmethoden merken
const originalSum = calculator.sum;
const originalProd = calculator.prod;
// 2.) Methoden überschreiben
calculator.sum = (...args) => {
// 3.) Advice implementieren, hier: Logging
console.log(`Aufruf von sum() mit den Argumenten ${args.join(', ')}`);
// 4.) Aufruf der Originalmethode
return originalSum(...args);
};
calculator.prod = (...args) => {
console.log(`Aufruf von prod() mit den Argumenten ${args.join(', ')}`);
return originalProd(...args);
};
// Ende AOP-Code
console.log(calculator.sum(5, 6));
console.log(calculator.prod(5, 6));
Listing 7.21 Einfachste Form der AOP in JavaScript
Der ursprüngliche Code, sprich der Code, in dem die Klasse Calculator definiert wird, enthält nun keine Konsolenausgaben mehr. Allerdings ist der Teil des Codes, der den AOP-Code darstellt, alles andere als kompakt und enthält darüber hinaus – schlimmer noch – doppelten Code. Es ist daher sinnvoll, über eine generische Funktion nachzudenken, die das Verhalten eines Before-Advices implementiert und die beliebige andere Funktionen bzw. Methoden mit einer Decoratorfunktion (bzw. einem Advice) versieht, die dann wiederum vor dem Aufruf der dekorierten Funktion/Methode aufgerufen wird. Eine solche Funktion before() bzw. ihre Verwendung sehen Sie in Listing 7.21. Diese Funktion erwartet eine andere Funktion als Parameter (decorator) und gibt eine Funktion zurück, die ihrerseits eine Funktion als Parameter erwartet (fn). Der Aufruf before(log) erzeugt demnach eine Funktion, die eine andere Funktion entgegennimmt, dann die log()-Funktion aufruft und anschließend die übergebene Funktion. class Calculator {
sum(x, y) {
return x + y;
}
prod(x, y) {
return x * y;
}
}
const calculator = new Calculator();
// Anfang AOP-Code
const before = decorator => fn => function(...args) {
decorator(fn, args);
return fn.apply(this, args);
}
const log = (fn, ...args) =>
console.log(`Aufruf von ${fn.name} mit den Argumenten ${args.join(', ')}`);
const logBefore = before(log);
calculator.sum = logBefore(calculator.sum);
calculator.prod = logBefore(calculator.prod);
// Ende AOP-Code
console.log(calculator.sum(5, 6));
console.log(calculator.prod(5, 6));
Listing 7.22 Generische »before()«-Funktion
7.3.4 Die JavaScript-Bibliothek meld
Möchten Sie nicht alle Arten von Advices selbst implementieren, können Sie auch auf Bibliotheken wie meld (https://github.com/cujojs/meld) zurückgreifen, die bereits Implementierungen für Before-Advices, After-Advices etc. bereitstellen. Die Bibliothek meld kann sowohl im Browser als auch unter Node.js genutzt werden. Im ersteren Falle erfolgt die Installation typischerweise über Yeoman (yeoman install meld), im zweiten Falle über den Node.js Package Manager mit dem Befehl npm install meld (im Folgenden werde ich nur den Einsatz unter Node.js besprechen). Nach erfolgreicher Installation lässt sich die Bibliothek unter Node.js wie gewohnt über require('meld') einbinden. Das auf diese Weise importierte Objekt stellt anschließend verschiedene Methoden zur Verfügung, mit denen Sie einzelne Advices für Objektinstanzen bzw. deren Methoden definieren (siehe Tabelle 7.1). Im Einzelnen sind dies analog zu den eingangs beschriebenen Advice-Typen die Methoden before(), after(), around(), afterThrowing() und afterReturning(). Advice
Beschreibung
Advice
Beschreibung
meld.before
Wird vor dem Aufruf einer Methode aufgerufen.
meld.after
Wird nach dem Aufruf einer Methode aufgerufen (unabhängig davon, ob die Methode normal beendet wird oder ob ein Fehler auftritt).
meld.around
Wird um den Aufruf einer Methode aufgerufen.
meld.afterReturning Wird nach dem Aufruf einer Methode aufgerufen, sofern diese ohne Fehler beendet wird. meld.afterThrowing
Wird beim Auftreten eines Fehlers aufgerufen.
Tabelle 7.1 Übersicht über die verschiedenen Advices in meld
Das angepasste Beispiel zur Definition eines Before-Advices zeigt Listing 7.22, wobei der Code dem aus Listing 7.21 relativ ähnlich ist. Der Methode meld.before() übergeben Sie als erstes Argument das entsprechende Objekt, als zweites Argument den Namen der Methode, für die der Advice definiert werden soll, und als drittes Argument eine Funktion, die die Logik enthält, die durch den Advice angestoßen werden soll. Innerhalb dieser Callback-Funktion haben Sie, wie auch schon in Listing 7.21 gezeigt, Zugriff auf die Argumente, mit denen die jeweils »dekorierte Methode« aufgerufen wird. const meld = require('meld');
class Calculator {
sum(x, y) {
console.log(`Berechne ${x} + ${y}`);
return x + y;
}
}
const calculator = new Calculator();
meld.before(calculator, 'sum', (...args) => {
console.log(
`Aufruf von calculator.sum() mit Argumenten: ${args.join(', ')}`
);
});
console.log(calculator.sum(5, 6));
// Aufruf von calculator.sum() mit Argumenten: 5, 6
// Berechne 5 + 6
// 11
Listing 7.23 Before-Advice, der die Argumente einer Funktion ausgibt
Die Definition eines After-Advices, über den Sie Zugriff auf den Rückgabewert einer Funktion bzw. Methode haben, sehen Sie in Listing 7.23, wobei hier die Methode after() verwendet wird. Alternativ können Sie über afterThrowing() und afterReturning() aber auch Advices definieren, die entweder nur aufgerufen werden, wenn ein Fehler auftritt (afterThrowing()), oder nur, wenn die Funktion normal beendet wurde (afterReturning()). Die Methode after() dagegen definiert Advices, die in beiden Fällen aufgerufen werden. ...
const calculator = new Calculator();
meld.after(calculator, 'sum', result => {
console.log(
`Aufruf von calculator.sum() ergab: ${result}`
);
});
console.log(calculator.sum(5, 6));
// Berechne 5 + 6
// Aufruf von calculator.sum() ergab: 11
// 11
Listing 7.24 After-Advice, der den Rückgabewert einer Funktion ausgibt
Wie die vorangegangenen Beispiele zeigen, haben Sie bei einem Before-Advice Zugriff auf die Argumente und bei einem After-
Advice (bzw. einem After-Returning-Advice) Zugriff auf den Rückgabewert einer Methode/Funktion. Es ist jedoch nicht möglich, innerhalb eines After-Advices auf die Argumente oder innerhalb eines Before-Advices auf den Rückgabewert zuzugreifen. Benötigen Sie dennoch beides (also Argumente und Rückgabewert) oder möchten Sie beispielsweise wissen, zu welchem Zeitpunkt eine Funktion gestartet und zu welchem Zeitpunkt sie beendet wurde (beispielsweise, um die Ausführungsdauer zu berechnen), müssen Sie wie in Listing 7.24 zu sehen auf den eingangs erwähnten AroundAdvice ausweichen. Dieser stellt über seine Callback-Funktion einen Parameter joinpoint zur Verfügung, über den Sie sowohl an die Argumente gelangen (Eigenschaft args) als auch an den Rückgabewert (über den Aufruf der Methode proceed(), die die Originalmethode aufruft und deren Ergebnis zurückgibt). ...
const calculator = new Calculator();
meld.around(calculator, 'sum', joinpoint => {
console.log(
`Aufruf von calculator.sum() mit Argumenten: ${joinpoint.args.join(', ')}`
);
const result = joinpoint.proceed();
console.log(`Aufruf von calculator.sum() ergab: ${result}`);
return result;
});
console.log(calculator.sum(5, 6));
// Aufruf von calculator.sum() mit Argumenten: 5, 6
// Berechne 5 + 6
// Aufruf von calculator.sum() ergab: 11
// 11
Listing 7.25 Around-Advice zur Ausgabe von Argumenten und Rückgabewert
Praktisch: Möchten Sie einen zuvor definierten Advice zu einem späteren Zeitpunkt wieder deaktivieren, reicht es, wie in Listing 7.25 gezeigt, auf dem von den Methoden before(), after(), around() etc. zurückgegebenen Objekt die Methode remove() aufzurufen.
...
const calculator = new Calculator();
const remover = meld.before(calculator, 'sum', (...args) => {
console.log(`Aufruf von calculator.sum() mit Argumenten: ${args.join(', ')}`);
});
console.log(calculator.sum(5, 6));
// Aufruf von calculator.sum() mit Argumenten: 5, 6
// Berechne 5 + 6
// 11
remover.remove();
console.log(calculator.sum(5, 6));
// Berechne 5 + 6
// 11
Listing 7.26 Entfernen eines Advices
Ein weiteres Beispiel für die Anwendung von AOP und die Verwendung eines Around-Advices ist die Implementierung einer Caching-Funktionalität: Wird eine Funktion oder Methode mit den gleichen Argumenten aufgerufen, berechnet sie das Ergebnis nicht erneut, sondern liefert das bereits zuvor berechnete Ergebnis aus einem internen Cache zurück. Listing 7.26 zeigt, wie sich die Methode sum() mit Hilfe der Bibliothek meld und einer Closure um entsprechende Funktionalität erweitern lässt. Als Cache wird hier ein einfaches Objekt verwendet, dessen Eigenschaften die Schlüssel darstellen (alternativ könnten Sie hier auch auf die in ES2015 eingeführten Maps zurückgreifen) und das durch die Closure »eingeschlossen« wird. Innerhalb der inneren Funktion wird zunächst der Schlüssel auf Basis der jeweils übergebenen Argumente generiert. Ist für diesen Schlüssel bereits ein Wert im Cache (mit anderen Worten: die Funktion wurde bereits mit den übergebenen Argumenten aufgerufen), wird dieser Wert zurückgegeben. Existiert dagegen kein Wert für den Schlüssel im Cache, wird über joinpoint.proceed() die eigentliche Funktion/Methode
aufgerufen und das Ergebnis dieses Aufrufs unter dem Schlüssel in den Cache geschrieben. ...
meld.around(calculator, 'sum', (() => {
const cache = {};
return joinpoint => {
const key = joinpoint.args.join(', ');
let result;
if(!cache[key]) {
console.log(`Berechne Ergebnis für: ${key}`);
cache[key] = joinpoint.proceed();
} else {
console.log(`Hole Ergebnis aus Cache für: ${key}`);
}
return cache[key];
}
})());
const result1 = calculator.sum(5, 6);
// --> Berechne Ergebnis für: 5, 6
const result2 = calculator.sum(5, 6);
// --> Hole Ergebnis aus Cache für: 5, 6
const result3 = calculator.sum(8, 9);
// --> Berechne Ergebnis für: 8, 9
const result4 = calculator.sum(8, 9);
// --> Hole Ergebnis aus Cache für: 8, 9
Listing 7.27 Caching über AOP
7.3.5 AOP über Decorators
Eines der neueren JavaScript-Features, die eventuell in eine der nächsten Versionen des ECMAScript-Standards Einzug halten, sind sogenannte Decorators (oder Dekoratoren; siehe https://github.com/tc39/proposal-decorators). Diese Decorators ähneln vom Prinzip her (und im Übrigen auch von der Syntax her) sogenannten Annotationen, wie man sie beispielsweise aus Java kennt, und ermöglichen es, Klassen bzw. Methoden zu »markieren«. Decorators werden momentan noch von kaum einer JavaScriptLaufzeitumgebung unterstützt. Es ist jedoch über spezielle Plugins möglich, entsprechenden Support für den JavaScript-Transpiler
BabelJS (https://babeljs.io/) zu aktivieren. Um also die im Folgenden beschriebenen Codebeispiele (mit Hilfe von Grunt) selbst auszuprobieren, führen Sie innerhalb des entsprechenden Ordners folgende Befehle aus und installieren damit grunt, load-grunt-tasks, grunt-babel, babel-preset-es2015 und babel-plugin-transformdecorators-legacy (letzteres übrigens als »legacy« markiert, da Decorators vorher standardmäßig von BabelJS unterstützt wurden, mittlerweile aber aufgrund der noch ausstehenden Übernahme in den ECMAScript-Standard wieder deaktiviert bzw. entfernt wurden). npm install grunt
npm install --save-dev
load-grunt-tasks
grunt-babel
babel-preset-es2015
babel-plugin-transform-decorators-legacy
Decorators in TypeScript TypeScript unterstützt Decorators übrigens nativ. Auch das Webframework Angular (https://angular.io/) verwendet – sofern man denn TypeScript für die Entwicklung benutzt – zur Definition einzelner Komponenten Decorators (siehe Kapitel 9, »Architekturmuster und Konzepte moderner JavaScriptWebframeworks«).
Ein Decorator ist zunächst einmal nichts anderes als eine Funktion mit drei Parametern: dem Zielobjekt, auf dem der Decorator ausgeführt wird, dem Namen bzw. Schlüssel der Eigenschaft des Objekts, für die der Decorator ausgeführt wird, sowie dem entsprechenden Property-Descriptor dieser Eigenschaft:
function decorator(target, key, descriptor) {
}
Listing 7.28 Prinzipieller Aufbau einer Decorator-Funktion
Optional kann ein Decorator einen Property-Descriptor zurückgeben, der dann für die entsprechende Eigenschaft verwendet wird. Beispielsweise könnten Sie relativ einfach einen Dekorator erstellen, der verhindert, dass eine Eigenschaft überschrieben wird: function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
Listing 7.29 Decorator, der das Überschreiben einer Eigenschaft verhindert
Um eine Methode mit einem Decorator zu markieren bzw. zu dekorieren, schreiben Sie den Namen des Decorators plus ein vorangestelltes @-Zeichen vor die jeweilige Methode (siehe Listing 7.29). function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Calculator {
@readonly
sum(x, y) {
return x + y;
}
}
let calculator = new Calculator();
console.log(calculator.sum(5, 6)); // 11
calculator.sum = (x, y) => x - y; // Nicht möglich, da @readonly
console.log(calculator.sum(5, 6)); // 11
Listing 7.30 Funktionsweise von Decorators
7.3.6 Die Bibliothek aspect.js
Eine AOP-Bibliothek, die das Konzept der Decorators unterstützt, ist aspect.js (https://github.com/mgechev/aspect.js) von Minko Gechev, dem Autor des Buches »Switching to Angular 2« (übrigens nicht zu verwechseln mit http://aspectjs.com/, einer weiteren AOP-Bibliothek für JavaScript). Sie installieren aspect.js, indem Sie das entsprechende GitRepository klonen: git clone https://github.com/mgechev/aop.js --depth 1
Da aspect.js selbst in TypeScript geschrieben ist, müssen Sie zudem einen entsprechenden Compiler bzw. eine entsprechende Laufzeitumgebung installieren: npm install -g ts-node
npm install -g typescript
Die Implementierung des Logging-Beispiels in aspect.js zeigt Listing 7.30. Einzelne Aspekte werden als separate Klassen definiert (im Beispiel LoggerAspect), wobei Sie über Decorators innerhalb dieser Aspekt-Klassen die Methoden (bzw. Advices) auszeichnen, die aufgerufen werden sollen. Dabei stellt aspect.js analog zu den beschriebenen Advices die in Tabelle 7.2 aufgelisteten Decorators zur Verfügung. Für welche Klasse(n) bzw. welche Methode(n) ein Advice angewendet werden soll, steuern Sie über ein Konfigurationsobjekt, das Sie dem jeweiligen Decorator als Parameter übergeben: classNamePattern beschreibt durch einen regulären Ausdruck die Klasse(n), methodNamePattern analog die entsprechende Methode(n).
import {beforeMethod, Wove, Metadata} from '../aop.js/lib/aspect';
class LoggerAspect {
@beforeMethod({
classNamePattern: /^Calculator/,
methodNamePattern: /^sum/
})
invokeBeforeMethod(meta: Metadata) {
console.log(
`Aufruf von ${meta.className}.${meta.method.name}() mit Argumenten: ${meta.method.args.join(', ')}`
);
}
}
@Wove()
class Calculator {
sum(x: number, y: number) {
console.log(`Berechne ${x} + ${y}`);
return x + y;
}
}
const calculator = new Calculator();
console.log(calculator.sum(5, 6));
// Aufruf von Calculator.sum() mit Argumenten: 5, 6
// Berechne 5 + 6
// 11
Listing 7.31 Verwendung der Bibliothek aspect.js [TypeScript]
Advice
Beschreibung
@beforeMethod(
methodSelector
)
Wird vor dem Aufruf einer Methode aufgerufen.
@afterMethod(
methodSelector
)
Wird nach dem Aufruf einer Methode aufgerufen.
@aroundMethod(
methodSelector
)
Wird um den Aufruf einer Methode aufgerufen.
Advice
Beschreibung
@onThrowOfMethod(
methodSelector
)
Wird beim Auftreten eines Fehlers aufgerufen.
@beforeStaticMethod(
methodSelector
)
Wird vor dem Aufruf einer statischen Methode aufgerufen.
@afterStaticMethod(
methodSelector
)
Wird nach dem Aufruf einer statischen Methode aufgerufen.
@aroundStaticMethod(
methodSelector
)
Wird um dem Aufruf einer statischen Methode aufgerufen.
@onThrowOfStaticMethod( Wird beim Auftreten eines Fehlers bei einer statischen methodSelector
Methode aufgerufen. ) @beforeSetter(
memberSelector
)
Wird vor dem Aufruf eines Setters aufgerufen.
@afterSetter(
memberSelector
)
Wird nach dem Aufruf eines Setters aufgerufen.
@aroundSetter(
memberSelector
)
Wird um dem Aufruf eines Setters aufgerufen.
Advice
Beschreibung
@onThrowOfSetter(
memberSelector
)
Wird beim Auftreten eines Fehlers bei einem Setter aufgerufen.
@beforeGetter(
memberSelector
)
Wird vor dem Aufruf eines Getters aufgerufen.
@afterGetter(
memberSelector
)
Wird nach dem Aufruf eines Getters aufgerufen.
@aroundGetter(
memberSelector
)
Wird um dem Aufruf eines Getters aufgerufen.
@onThrowOfGetter(
memberSelector
)
Wird beim Auftreten eines Fehlers bei einem Getter aufgerufen.
Tabelle 7.2 Übersicht über die verschiedenen Advices in »aspect.js«
7.4 Zusammenfassung und Ausblick In diesem Kapitel haben Sie einige weiterführende Konzepte der objektorientierten Programmierung kennengelernt: Die SOLID-Prinzipien sind fünf grundlegende Prinzipien der objektorientierten Programmierung für sauberen Code. Ihre Einhaltung führt in der Regel zu Klassen mit einem starken inneren Zusammenhang und loser Kopplung, was auch im Falle von JavaScript-basierten Anwendungen sinnvoll ist. Fluent APIs bezeichnen eine Technik für die Entwicklung von programmatischen Schnittstellen, durch die sich fast so flüssig wie in natürlicher Sprache programmieren lässt. In JavaScript unterscheidet man zwischen synchronen Fluent APIs und asynchronen Fluent APIs. Die aspektorientierte Programmierung (AOP) ermöglicht es, Anwendungscode mit zusätzlicher Funktionalität zu erweitern, ohne diesen selbst verändern zu müssen. Mit anderen Worten: Mit Hilfe von AOP halten Sie Anwendungscode frei von CrossCutting Concerns.
8 Die Entwurfsmuster der Gang of Four Wie Sie in den vorigen Kapiteln gesehen haben, werden viele Features, die in anderen Programmiersprachen zum Sprachumfang gehören, in JavaScript über spezielle Entwurfsmuster geregelt. Doch wie sieht es in die andere Richtung aus? Entwurfsmuster bezeichnen in der Softwareentwicklung bewährte Herangehensweisen für wiederkehrende Problemstellungen. In der objektorientierten Programmierung dürften wohl die Entwurfsmuster der Gang of Four (GoF-Entwurfsmuster) zu den bekanntesten zählen, beschrieben in dem Buch Entwurfsmuster – Elemente wiederverwendbarer objektorientierter Software. In diesem Kapitel werde ich betrachten, welche Relevanz diese Entwurfsmuster bei der JavaScript-Entwicklung haben.
8.1 Einführung Die GoF-Sammlung von 23 Entwurfsmustern gliedert sich in die drei Kategorien Erzeugungsmuster (Creational Design Patterns), Strukturmuster (Structural Design Patterns) und Verhaltensmuster (Behavioral Design Patterns). Viele dieser Muster entstanden aus Einschränkungen der Sprachen, für die sie konzipiert wurden, wie beispielsweise C++ und Smalltalk. Auch in Java und prinzipiell anderen Sprachen, denen Klassen zugrunde liegen, finden die GoF-Entwurfsmuster häufig Verwendung. Die meisten der Entwurfsmuster involvieren dabei
mehrere Klassen und Interfaces, was in Folge schnell zu jeder Menge (Boilerplate-)Code für eine häufig »triviale« Aufgabe führt. Betrachtet man funktionale Programmiersprachen, stellt man fest, dass dort viele Probleme gar nicht existieren oder sich anders eleganter (und mit weniger Code) lösen lassen. Durch die funktionalen Eigenschaften von JavaScript stellt sich nun die Frage, ob das Verwenden der GoF-Entwurfsmuster hier überhaupt sinnvoll ist. Im Folgenden zeige ich Ihnen, welchen Stellenwert die Muster in JavaScript haben, wie sie sich gegebenenfalls realisieren lassen und welche Alternativen oder Vereinfachungen es dank funktionaler und auch dank prototypischer Aspekte gibt. Möchten Sie sich eingehender mit den GoF-Entwurfsmustern auseinandersetzen, empfehle ich Ihnen das entsprechende Standardwerk der Gang of Four. Bezüglich JavaScript gibt es zum Thema insbesondere zwei Bücher: Learning JavaScript Design Patterns von Addy Osmani, das auch als Onlinebuch unter http://addyosmani.com/resources/essentialjsdesignpatterns/book zur Verfügung steht, sowie Pro JavaScript Design Patterns von Dustin Diaz und Ross Harmes. Hinweis Alle der GoF-Entwurfsmuster basieren auf der Verwendung von Klassen. Viele der Entwurfsmuster beziehen außerdem Interfaces und abstrakte Klassen ein. Auch wenn wir in den letzten Kapiteln gesehen haben, dass sich Interfaces und Klassen in JavaScript emulieren lassen und es seit ES2015 sogar eine Klassensyntax gibt, ist JavaScript letztendlich eine Sprache, die auf der prototypischen Objektorientierung basiert. Eine 1 : 1-Abbildung der GoF-Entwurfsmuster ist also in jedem Fall nicht möglich. Die im Verlauf dieses Kapitels gezeigten Klassendiagramme in der UML-
Notation (Unified Modeling Language) repräsentieren daher den ursprünglichen Aufbau des jeweiligen Entwurfsmusters, nicht unbedingt den Aufbau in der JavaScript-Variante des jeweiligen Entwurfsmusters.
Tipp Entwurfsmuster sollten nicht um des Entwurfsmusters willen eingesetzt werden. Nutzen Sie die Stärken von JavaScript. Vermeiden Sie, Interfaces und Klassen in JavaScript zu emulieren, nur um damit die GoF-Entwurfsmuster umsetzen zu können. Das ergibt keinen Sinn und bläht den Code unnötig auf!
Hinweis Die Implementierung einzelner Entwurfsmuster variiert in JavaScript natürlich, abhängig davon, welche Art von Objektorientierung Sie verwenden: In der pseudoklassischen Objektorientierung ist die Implementierung eine andere, als wenn Sie prototypische Objektorientierung oder die neue Klassensyntax einsetzen. Statt für jedes Entwurfsmuster alle drei Varianten durchzuspielen, habe ich im Folgenden versucht, die Beispiele möglichst abwechslungsreich zu gestalten.
8.2 Erzeugungsmuster Erzeugungsmuster beschreiben, wie der Name sagt, die Gruppe der Entwurfsmuster, die der Erzeugung von Objekten dienen. Der Fokus liegt dabei darauf, das Erzeugen von Objekten von der Repräsentation der Objekte zu entkoppeln. Insgesamt gibt es folgende fünf Erzeugungsmuster: Abstract Factory, Factory Method, Builder, Prototype und Singleton. 8.2.1 Objekte an einer zentralen Stelle erzeugen (Abstract Factory/Factory Method)
Starten wir mit zwei Entwurfsmustern, die durchaus auch in JavaScript ihre Daseinsberechtigung haben: mit der Fabrikmethode oder Factory Method und der abstrakten Fabrik oder Abstract Factory. Beschreibung des Entwurfsmusters
Hinsichtlich der Wartbarkeit eines Quelltextes ist es in den meisten Fällen besser, Objekte nicht direkt über den Konstruktor, sondern über spezielle Methoden (eben Fabrikmethoden) zu erzeugen. Der Vorteil ist dabei der, dass die Stelle im Code, an der die Fabrikmethode aufgerufen wird, davon unabhängig ist, wie das entsprechende Objekt (in diesem Kontext auch Produkt genannt) erzeugt wird. Abbildung 8.1 zeigt die beteiligten Komponenten dieses Entwurfsmusters. AbstractProduct bezeichnet die abstrakte Basisklasse von Produkten, von der konkrete Produktklassen ableiten. Creator stellt die Schnittstelle für die Fabrikmethode
bereit, die Implementierung geschieht in der Klasse ConcreteCreator. Anstatt Instanzen über new Product() anzulegen, rufen Sie einfach die Methode factoryMethod() auf.
Abbildung 8.1 Klassendiagramm für das Factory-Method-Entwurfsmuster
Einen Schritt weiter geht das Abstract-Factory-Entwurfsmuster. Im Gegensatz zur Factory Method, die die Erzeugung eines einzelnen Produkts kapselt, kapselt eine Abstract Factory das Erstellen von Produkten einer ganzen Produktfamilie. Intern bedient es sich dazu in der Regel verschiedener Fabrikmethoden. Abbildung 8.2 zeigt die entsprechenden Komponenten dieses Musters. Auf der einen Seite sehen Sie die abstrakte Fabrik (AbstractFactory) mit ihren verschiedenen Implementierungen, auf der anderen Seite das abstrakte Produkt (AbstractProduct), seinerseits mit entsprechenden Implementierungen.
Abbildung 8.2 Klassendiagramm für das Abstract-Factory-Entwurfsmuster
Relevanz in JavaScript
Auch wenn es in JavaScript weder Interfaces noch Klassen gibt, lohnt sich der Einsatz von Fabrikmethoden bzw. abstrakten Fabriken, wobei der Begriff »abstrakt« in diesem Kontext (wegen des fehlenden Konzepts abstrakter Klassen) nur wenig sinnvoll ist und ich daher im Folgenden nur von Factories sprechen werde. Die pseudoklassische Vererbung basiert, wie Sie aus Abschnitt 3.3.2 wissen, auf Konstruktorfunktionen, ebenso die Klassensyntax. Sie haben außerdem bereits gesehen, zu welchen Fehlern es führen kann, wenn solche Funktionen als normale Funktionen aufgerufen werden. Ich habe Ihnen zwar in diesem Zusammenhang eine entsprechende Fallback-Technik vorgestellt, über die fälschlich als normale Funktionen aufgerufene Konstruktorfunktionen trotzdem das richtige Ergebnis (nämlich eine Objektinstanz) zurückgeben,
aber warum sollten Sie das Erzeugen von Objekten nicht von vornherein über speziell dafür vorgesehene Methoden vornehmen? Lassen Sie mich Ihnen zeigen, wie sich eine Factory in JavaScript implementieren lässt. Als Objektmodell verwenden wir das in Listing 8.1 gezeigte: die Oberklasse Vinyl mit den beiden Unterklassen LP (»Long Player«) und EP (»Extended Player«). Diese stellen die zu erzeugenden Produkte dar. class Vinyl {
constructor(config) {
this.color = config.color || 'schwarz';
this.name = config.name || 'Untitled';
this.artist = config.artist || 'VA';
}
}
class LP extends Vinyl {
constructor(config) {
super(config);
this.diameter = config.diameter || 30;
}
}
class EP extends Vinyl {
constructor(config) {
super(config);
this.diameter = config.diameter || 17.5;
}
}
Listing 8.1 Ein einfaches Objektmodell in Klassensyntax [ES2015]
Listing 8.2 zeigt die Implementierung und die Anwendung der Factory. Anhand des übergebenen Konfigurationsobjekts wird der Typ des zu erstellenden Objekts ermittelt und eine entsprechende Objektinstanz erzeugt. Der Vorteil dabei: Die einzige Stelle, an der eine Konstruktorfunktion aufgerufen wird, befindet sich innerhalb der Fabrikmethode. class VinylFactory {
constructor() {
this.vinylClass = LP;
}
create(config) {
switch(config.vinylType){
case 'lp':
this.vinylClass = LP;
break;
case 'ep':
this.vinylClass = EP;
break;
}
return new this.vinylClass(config);
};
}
const vinylFactory = new VinylFactory();
const ep = vinylFactory.create({
vinylType: 'ep',
color: 'yellow'
});
const lp = vinylFactory.create({
vinylType: 'lp',
name: 'Third Eye Surgery',
artist: 'Baby Woodrose'
});
console.log(ep instanceof EP); // true
console.log(ep.diameter); // 17.5
console.log(lp instanceof LP); // true
console.log(lp.diameter); // 30
Listing 8.2 Das Factory-Entwurfsmuster in JavaScript [ES2015]
Zum Vergleich zeigen die folgenden beiden Listings noch den entsprechenden Code unter Verwendung pseudoklassischer Objektorientierung: function Vinyl(config) {
this.color = config.color || 'schwarz';
this.name = config.name || 'Untitled';
this.artist = config.artist || 'VA';
}
function LP(config) {
Vinyl.call(this, config);
this.diameter = config.diameter || 30;
}
LP.prototype = Object.create(Vinyl.prototype);
LP.prototype.constructor = Vinyl;
function EP(config) {
Vinyl.call(this, config);
this.diameter = config.diameter || 17.5;
}
EP.prototype = Object.create(Vinyl.prototype);
EP.prototype.constructor = Vinyl;
Listing 8.3 Ein einfaches Objektmodell mit pseudoklassischer Vererbung [ES5] function VinylFactory() {}
VinylFactory.prototype.vinylClass = LP;
VinylFactory.prototype.create = function (config) {
switch(config.vinylType){
case 'lp':
this.vinylClass = LP;
break;
case 'ep':
this.vinylClass = EP;
break;
}
return new this.vinylClass(config); };
const vinylFactory = new VinylFactory();
const ep = vinylFactory.create({
vinylType: 'ep',
color: 'yellow'
});
const lp = vinylFactory.create({
vinylType: 'lp',
name: 'Third Eye Surgery',
artist: 'Baby Woodrose'
});
console.log(ep instanceof EP); // true
console.log(ep.diameter); // 17.5
console.log(lp instanceof LP); // true
console.log(lp.diameter); // 30
Listing 8.4 Das Factory-Entwurfsmuster in JavaScript [ES5]
Merke Factory Method und Abstract Factory sind auch in JavaScript sinnvoll, um das Erzeugen von Objekten an zentraler Stelle zu verwalten.
8.2.2 Nur ein Objekt von einem Typ erstellen (Singleton)
In klassenbasierten Sprachen können standardmäßig von jeder Klasse mehrere Objektinstanzen erzeugt werden. Doch nicht immer möchte man das erlauben. Hier kommt das SingletonEntwurfsmuster ins Spiel: Es bewirkt, dass von einer bestimmten Klasse nur eine Instanz erzeugt werden kann. Beschreibung des Entwurfsmusters
In der Regel geht man so vor, dass die Singleton-Objektinstanz von der jeweiligen Klasse in einer statischen Variablen verwaltet wird. Der Zugriff geschieht dann meistens über die öffentliche Klassenmethode getInstance() (siehe UML-Diagramm in Abbildung 8.3), wobei die Objektinstanz dabei nicht schon beim Laden der Klasse, sondern erst beim ersten Aufruf von getInstance() erzeugt wird (Lazy Instantiation).
Abbildung 8.3 Klassendiagramm für das Singleton-Entwurfsmuster
Relevanz in JavaScript
Da es in JavaScript kein Konzept für Klassen gibt (die Klassensyntax definiert ja keine echten Klassen), ist implizit jedes Objekt, das Sie erstellen, ein Singleton. Listing 8.5 zeigt die einfachste Form eines Singletons in JavaScript. const singleton = {};
Listing 8.5 Die einfachste Form eines Singletons in JavaScript ist ein simples Objekt.
Um die beiden fehlenden Aspekte des Zugriffs auf die Instanz per Methode und der Lazy Instantiation in JavaScript nachzubilden, bedient man sich in der Regel, wie in Listing 8.6 gezeigt, des ModuleEntwurfsmusters. Die Variable Singleton stellt in diesem Fall sozusagen die Klasse dar, init() eine private und getInstance() eine öffentliche Methode. Ist die Variable instance noch nicht definiert, wird sie beim ersten Aufruf von getInstance() über init() berechnet bzw. initialisiert. const Singleton = ( function () {
let instance;
function init() {
const randomNumber = Math.random();
return {
getRandomNumber() {
return randomNumber;
}
};
};
return {
getInstance() {
if(!instance) {
instance = init();
}
return instance;
}
};
})();
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1.getRandomNumber() === s2.getRandomNumber()); // true
Listing 8.6 Das Singleton-Entwurfsmuster mit Lazy Instantiation [ES5]
Alternativ zu der gezeigten Technik lässt sich Lazy Instantiation über das aus Kapitel 2, »Funktionen und funktionale Aspekte«, bekannte Entwurfsmuster Self-overwriting Function realisieren. Zur Erinnerung: Dabei überschreibt sich eine Funktion bei Aufruf selbst. Zu sehen ist dies in Listing 8.7. Hier wird beim ersten Aufruf von getInstance() zunächst die Variable instance instanziiert und anschließend getInstance() neu gesetzt. Weitere Aufrufe der Funktion führen danach zu keiner weiteren Objektinstanz. const Singleton = (
function () {
return {
getInstance() {
// Die Instanz wird nur einmal initialisiert
const instance = function(){
const randomNumber = Math.random();
return {
getRandomNumber() {
return randomNumber;
}
}
}();
// Neudefinition der Funktion
this.getInstance = function() {
return instance;
}
return this.getInstance();
}
};
})();
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1.getRandomNumber() === s2.getRandomNumber()); // true
Listing 8.7 Lazy Instantiation via Self-overwriting Function [ES5]
Eine weitere Variante des Singleton-Entwurfsmusters, dieses Mal unter Verwendung der Klassensyntax, zeigt Listing 8.8. Im Konstruktor wird hier zunächst geprüft, ob die Eigenschaft Singleton.instance definiert ist. Ist dies nicht der Fall, wird die Eigenschaft mit der aktuell erzeugten Instanz (this) belegt. Andernfalls wird Singleton.instance zurückgeliefert, wodurch
wiederum sichergestellt ist, dass mehrere Aufrufe von new Singleton() immer dasselbe Objekt zurückgeben. Eigentlich ist dies aber gar nicht notwendig, da über module.exports = instance sowieso nur eine (nämlich immer dieselbe) Instanz nach außen exportiert wird. Der Aufruf von Object.freeze(instance) verhindert zudem, dass Änderungen an der Instanz vorgenommen werden. // Datei Singleton.js
class Singleton {
constructor() {
if (!Singleton.instance) {
this.randomNumber = Math.random();
Singleton.instance = this;
}
return Singleton.instance;
}
getRandomNumber() {
return this.randomNumber;
}
}
const instance = new Singleton();
Object.freeze(instance);
module.exports = instance;
// Datei start.js
const s1 = require('./Singleton');
const s2 = require('./Singleton');
console.log(s1.getRandomNumber());
console.log(s2.getRandomNumber());
console.log(s1.getRandomNumber() === s2.getRandomNumber()); // true
Listing 8.8 Singleton mit Klassensyntax [ES2015]
Merke Streng genommen ist jedes Objekt, das Sie in JavaScript erstellen, ist aufgrund fehlender Klassen implizit ein Singleton. Trotzdem ist die striktere Umsetzung des Entwurfsmusters auch in
JavaScript sinnvoll, beispielsweise um zentral eine einzige Instanz einer Klasse zu verwenden.
8.2.3 Erstellen von komplexen Objekten (Builder)
Die Idee beim Builder-Entwurfsmuster ist es, das Erstellen von Objekten zu vereinfachen und wie bei der Abstract Factory in eine eigene Klasse auszulagern. Der Fokus liegt beim BuilderEntwurfsmuster aber vor allem darauf, das Erzeugen solcher Objekte zu vereinfachen, bei deren Erzeugung mehrere Funktionsaufrufe involviert sind. Beschreibung des Entwurfsmusters
Im klassischen Builder-Entwurfsmuster sind die in Abbildung 8.4 dargestellten Komponenten beteiligt: Builder stellt die Schnittstelle zur Erzeugung der einzelnen Bestandteile des Objekts (bzw. auch Produkts) bereit. ConcreteBuilder implementiert diese Schnittstelle und verwaltet zusätzlich das von ihm erstellte Objekt (eine Instanz von Product), das über die Methode getResult() zurückgegeben werden kann. Die DirectorKomponente ist für die Konstruktion des Objekts verantwortlich, sie steuert die einzelnen Aufrufe an die Builder-Komponente und weiß beispielsweise genau, in welcher Reihenfolge die Methoden (buildPartA(), buildPartB(), buildPartC() und buildPartD()) aufgerufen werden müssen.
Abbildung 8.4 Klassendiagramm für das Builder-Entwurfsmuster
Relevanz in JavaScript
Prinzipiell ist das Builder-Entwurfsmuster auch in JavaScript sinnvoll, nämlich immer dann, wenn Sie das Erzeugen komplexer Objekte in strukturierter Form auslagern möchten. Listing 8.9 zeigt dazu ein Beispiel in Klassensyntax, Listing 8.10 ein Beispiel mit pseudoklassischer Objektorientierung (wobei in beiden Fällen Interface und Klasse, also Builder und ConcreteBuilder, nicht getrennt modelliert werden). Gezeigt ist hier, wie Sie mit Hilfe eines Builders das Erzeugen von HTML-Tabellen vereinfachen können. Eine HTML-Tabelle besteht vereinfacht betrachtet aus Tabellenüberschriften sowie Spalten und Zeilen (um das Beispiel übersichtlich zu halten, sind Datensätze nicht enthalten). Die Tabelle stellt also das Produkt dar, die drei genannten Komponenten die Bestandteile dieses Produkts.
TableBuilder stellt für jeden dieser Teile eine entsprechende Methode zur Verfügung (buildHeaders(), buildColumns(), buildRows()). Das Director-Objekt kapselt den Aufruf dieser
Methoden und weiß auch, in welcher Reihenfolge die Methoden des Builders aufgerufen werden müssen. So setzt z.B. ein Aufruf von buildRows() einen vorhergehenden Aufruf von buildColumns() voraus, da die Anzahl der Spalten bekannt sein muss, um eine Zeile in der Tabelle erstellen zu können. Das TableDirector-Objekt ist prinzipiell nichts anderes als eine Facade (siehe Abschnitt 8.3.6, »Einheitliche Schnittstelle für mehrere Schnittstellen (Facade)«) für das Erstellen von Tabellen. Dadurch, dass die einzelnen Methoden von TableBuilder jeweils eine Referenz auf die aktuelle Objektinstanz (this) zurückgeben, können die Methodenaufrufe bequem hintereinandergereiht werden (Stichwort Fluent APIs, siehe Kapitel 7, »Fortgeschrittene Konzepte der objektorientierten Programmierung«). class TableBuilder {
constructor() {
this.table = document.createElement('table');
this.numberOfRows = 0;
this.numberOfColumns = 0;
this.headerNames = [];
}
buildHeaders(headers) {
this.headerNames = headers;
const tr = document.createElement('tr');
for(let i = 0; i < this.headerNames.length; i++) {
const th = document.createElement('th');
const text = document.createTextNode(this.headerNames[i]);
th.appendChild(text);
tr.appendChild(th);
}
this.table.appendChild(tr);
return this;
}
buildColumns(numberOfColumns) {
this.numberOfColumns = numberOfColumns;
return this;
}
buildRows(numberOfRows) {
this.numberOfRows = numberOfRows;
for(let i = 0; i < this.numberOfRows; i++) {
const tr = document.createElement('tr');
for(let j = 0; j < this.numberOfColumns; j++) {
const td = document.createElement('td');
tr.appendChild(td);
}
this.table.appendChild(tr);
}
return this;
}
getResult() {
return this.table;
}
};
class TableDirector {
constructor() {
this.tableBuilder = new TableBuilder();
}
constructArtistTable() {
const table = this.tableBuilder
.buildHeaders(['Artist', 'Title'])
.buildColumns(2)
.buildRows(4)
.getResult();
return table;
}
}
const tableDirector = new TableDirector();
const table = tableDirector.constructArtistTable();
// document.getElementById('artist-table').appendChild(table);
console.log(table);
Listing 8.9 Das Builder-Entwurfsmuster in JavaScript [ES2015] const TableBuilder = function() {
var table = document.createElement('table');
var numberOfRows = 0;
var numberOfColumns = 0;
const headerNames = [];
return {
buildHeaders : function(headers) {
this.headerNames = headers;
var tr = document.createElement('tr');
for(var i = 0; i {
NewLogger.reportLog(message);
}
Logger.warn = (message) => {
NewLogger.reportWarn(message);
}
Logger.error = (message) => {
NewLogger.reportError(message);
}
Listing 8.14 Anwendung des Adapter-Entwurfsmusters [ES2015]
Wenn Sie das Originalobjekt auf diese Weise anpassen, müssen Sie im Einzelfall natürlich aufpassen, dass diese Änderung keine ungewollten Nebenwirkungen hat (beispielsweise wenn es bereits Methoden mit gleichem Namen gibt). In der Praxis ist es zudem nicht immer so einfach wie im Beispiel. Oft unterscheiden sich alte und neue Schnittstelle erheblich, nicht nur durch die Methodennamen, sondern beispielsweise in der Art und Weise ihrer Methodenaufrufe, der Anzahl der Methodenparameter oder im Rückgabewert, was in Folge zu speziellen, mitunter recht komplexen Adaptern führen kann. Merke
Vom Prinzip her ist das Adapter-Entwurfsmuster auch in JavaScript sinnvoll. Je nach Komplexität der zu adaptierenden Schnittstelle kann auf das Entwurfsmuster dank dynamischer Aspekte von JavaScript aber auch häufig verzichtet werden.
8.3.2 Abstraktion und Implementierung entkoppeln (Bridge)
In der klassenbasierten Objektorientierung bezeichnet eine abstrakte Klasse eine Klasse, die nicht vollständig implementiert ist oder für die ein Teil der Methoden (oder alle Methoden) abstrakt und damit nicht definiert ist. Die Implementierung der Methoden ist dann den von der abstrakten Klasse ableitenden Unterklassen überlassen. Auf diese Weise entsteht jedoch eine eng gekoppelte Vererbungshierarchie. Für bestimmte Problemstellungen ist es daher sinnvoll, statt einer einzelnen Vererbungshierarchie mehrere parallele Vererbungshierarchien zu verwenden. Beschreibung des Entwurfsmusters
Das Bridge-Entwurfsmuster kann hierbei helfen und dient dazu, die Implementierung von ihrer Abstraktion zu entkoppeln. Statt dass die implementierende Klasse von der abstrakten Klasse ableitet, erhält letztere eine Referenz auf ihre Implementierung (siehe Abbildung 8.7).
Abbildung 8.7 Klassendiagramm für das Bridge-Entwurfsmuster
Das dahinterstehende Prinzip lautet Komposition vor Vererbung (Composition over Inheritance) und ist eines der grundlegenden Prinzipien objektorientierter Programmierung. Relevanz in JavaScript
Das Bridge-Entwurfsmuster hat in JavaScript nahezu keine Relevanz, da es keinen konzeptionellen Unterschied zwischen Abstraktion und Implementierung gibt. Da ich keinen sinnvollen Einsatz für dieses Entwurfsmuster sehe, verzichte ich an dieser Stelle darauf, Ihnen nur der Implementierung willen eine Umsetzung in JavaScript zu zeigen. Merke
Das Bridge-Entwurfsmuster ist in JavaScript kaum sinnvoll, da es hier ohnehin keine Unterscheidung zwischen abstrakten und konkreten Klassen gibt.
8.3.3 Objekte in Baumstrukturen anordnen (Composite)
Relativ häufig hat man es bei der Modellierung des Objektmodells mit Objekten zu tun, die hierarchisch in Form von Baumstrukturen angeordnet werden. Beschreibung des Entwurfsmusters
Die Idee beim Composite-Entwurfsmuster ist es, über ein gemeinsames Interface sowohl »primitive« Objekte als auch Container für diese Objekte zu repräsentieren, um beide einheitlich verwenden zu können. Abbildung 8.8 zeigt das entsprechende Klassendiagramm.
Abbildung 8.8 Klassendiagramm für das Composite-Entwurfsmuster
Component stellt hierbei das gemeinsame Interface dar, das sowohl von der Klasse für die »primitiven« Objekte (Leaf) als auch von der Container-Klasse (Composite) implementiert wird. Letztere stellt zudem Methoden zum Verwalten der Instanzen von Component zur Verfügung. Da diese auch ihrerseits Instanzen von Composite
sein können, ist es auf diese Weise möglich, beliebig verschachtelte Objekthierarchien zu bilden (beispielsweise wie in Abbildung 8.9).
In der Praxis kommt dieses Entwurfsmuster häufig bei GUIFrameworks zum Einsatz, wo bestimmte GUI-Komponenten sowohl als Container für andere Komponenten dienen können als auch selbst Teil einer anderen Komponente sein können.
Abbildung 8.9 Beispiel für eine auf dem Composite-Entwurfsmuster basierende Baumstruktur
Relevanz in JavaScript
Da man es auch bei der JavaScript-Entwicklung oft mit Baumstrukturen zu tun hat, ist es nicht verkehrt, in diesen Fällen das Composite-Entwurfsmusters zu verwenden. Listing 8.15 zeigt die Implementierung dieses Entwurfsmusters in Klassensyntax anhand eines Objektmodells zur Repräsentation von Verzeichnisstrukturen.
Listing 8.16 zeigt die entsprechende Implementierung in pseudoklassischer Variante. File stellt dabei jeweils die Leaf-Komponente dar, Directory repräsentiert ein Verzeichnis und stellt damit die CompositeKomponente dar. Directory kann sowohl Instanzen von File als auch von Directory enthalten. Gemeinsame Oberklasse ist Component. class Component {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
class File extends Component {}
class Directory extends Component {
constructor(name) {
super(name);
this.children = [];
}
add(child) {
this.children.push(child);
}
remove(child) {
const length = this.children.length;
for (let i = 0; i < length; i++) {
if (this.children[i] === child) {
this.children.splice(i, 1);
return;
}
}
}
getChild(i) {
return this.children[i];
}
hasChildren() {
return this.children.length > 0;
}
}
const printDirectoryStructure = (component, indent) => {
console.log(Array(indent++).join('--') + component.name);
if (component instanceof Directory) {
for (let i = 0, length = component.children.length; i < length; i++) {
printDirectoryStructure(component.getChild(i), indent);
}
}
};
const projectDirectory = new Directory('project');
const cssDirectory = new Directory('css');
const cssFile1 = new File('styles1.css');
const cssFile2 = new File('styles2.css');
cssDirectory.add(cssFile1);
cssDirectory.add(cssFile2);
const scriptDirectory = new Directory('js');
const scriptFile1 = new File('scripts1.js');
const scriptFile2 = new File('scripts2.js');
scriptDirectory.add(scriptFile1);
scriptDirectory.add(scriptFile2);
projectDirectory.add(cssDirectory);
projectDirectory.add(scriptDirectory);
printDirectoryStructure(projectDirectory, 1);
Listing 8.15 Anwendung des Composite-Entwurfsmusters [ES2015] const Component = function(name) {
this.name = name;
this.getName = function() {
return this.name;
};
};
const File = function(name) {
Component.call(this, name);
};
const Directory = function(name) {
this.children = [];
Component.call(this, name);
};
Directory.prototype = {
add: function(child) {
this.children.push(child);
},
remove: function(child) {
const length = this.children.length;
for (let i = 0; i < length; i++) {
if (this.children[i] === child) {
this.children.splice(i, 1);
return;
}
}
},
getChild: function(i) {
return this.children[i];
},
hasChildren: function() {
return this.children.length > 0;
}
};
function printDirectoryStructure(component, indent) {
console.log(Array(indent++).join('--') + component.getName());
if (component instanceof Directory) {
for (let i = 0, length = component.children.length; i < length; i++) {
printDirectoryStructure(component.getChild(i), indent);
}
}
}
const projectDirectory = new Directory('project');
const cssDirectory = new Directory('css');
const cssFile1 = new File('styles1.css');
const cssFile2 = new File('styles2.css');
cssDirectory.add(cssFile1);
cssDirectory.add(cssFile2);
const scriptDirectory = new Directory('js');
const scriptFile1 = new File('scripts1.js');
const scriptFile2 = new File('scripts2.js');
scriptDirectory.add(scriptFile1);
scriptDirectory.add(scriptFile2);
projectDirectory.add(cssDirectory);
projectDirectory.add(scriptDirectory);
printDirectoryStructure(projectDirectory, 1);
Listing 8.16 Anwendung des Composite-Entwurfsmusters [ES5]
Die Ausgabe der obigen Programme zeigt Listing 8.17: project
--css
----styles1.css
----styles2.css
--js
----scripts1.js
----scripts2.js
Listing 8.17 Ausgabe der Beispielprogramme
Hinweis Sie könnten die Implementierung noch weiter vereinfachen, indem Sie auf Klassensyntax oder Pseudoklassen verzichten, stattdessen prototypische Vererbung verwenden und innerhalb der Methode printDirectory() statt des Typs nur das Vorhandensein der Eigenschaft children prüfen (Stichwort Duck-Typing, siehe Abschnitt 3.6.2, »Interfaces emulieren mit Duck-Typing«).
Merke Das Composite-Entwurfsmuster können Sie auch bei der Modellierung von Objekten in JavaScript einsetzen. Die Unterscheidung zwischen Leaf- und Composite-Komponente können Sie dabei entweder wie gezeigt über Klassen bzw. Pseudoklassen oder über Duck-Typing realisieren.
8.3.4 Eigenschaften unter Objekten teilen (Flyweight)
Wenn Sie es in einer Anwendung mit vielen Objektinstanzen zu tun haben, hat dies schnell Auswirkungen auf die Performance. Hier kann das Flyweight-Entwurfsmuster helfen. Beschreibung des Entwurfsmusters
Das Flyweight-Entwurfsmuster dient dazu, bei großen Mengen von Objekten zu verhindern, dass Informationen, die für alle gleich sind, in jedem einzelnen Objekt vorliegen. Stattdessen befinden sie sich in einem separaten Objekt (extrinsischer Zustand) getrennt von den individuellen Informationen (intrinsischer Zustand).
Das Klassendiagramm in Abbildung 8.10 zeigt die beteiligten Komponenten. Flyweight definiert die Schnittstelle für FlyweightObjekte. Implementierungen dieser Schnittstelle sind in zwei Gruppen unterteilt: ConcreteFlyweight sind die »echten« Flyweight-Objekte, die nur über einen intrinsischen Zustand verfügen, sprich nur über die Information, die nicht mit anderen Objekten gemeinsam ist. UnsharedConcreteFlyweight dagegen bezeichnet stellvertretend die Gruppe von Klassen, die genau keine Flyweight-Objekte sind und den gesamten Zustand enthalten, sprich sich keine Informationen mit anderen Objekten teilen. Diese Klasse ist im Klassendiagramm lediglich aufgeführt, um zu verdeutlichen, wo »normale« Objekte in diesem Entwurfsmuster anzuordnen sind.
Abbildung 8.10 Klassendiagramm für das Flyweight-Entwurfsmuster
Relevanz in JavaScript
Auch in JavaScript kann das Flyweight-Entwurfsmuster helfen, die Speicherauslastung bei Ausführung einer Anwendung zu optimieren, wenn Sie eine Vielzahl ähnlicher Objektinstanzen verwenden.
Folgende beiden Listings zeigen dazu ein Beispiel. Modelliert ist hier das Objektmodell für eine fiktive Plattform für das Anbieten von Musikalben. Der Einfachheit halber verfügt jedes Musikalbum über Eigenschaften zu Titel, Veröffentlichungsjahr, Künstler, zum Anbieter und zum Angebotspreis. Außerdem enthält jedes Musikalbum eine ID. Listing 8.18 zeigt die Version ohne FlyweightEntwurfsmuster, Listing 8.19 die Version mit FlyweightEntwurfsmuster. In beiden Listings werden über eine Schleife 1.000.000 Angebote erstellt, wobei der Einfachheit halber folgende Einschränkungen gelten: Titel und Künstler sind immer gleich (das ist aber für das Beispiel irrelevant). Die ID befindet sich immer im Wertebereich 1 bis 9, mit anderen Worten: Es werden nur neun Musikalben angeboten. Die ID wird über die Funktion getRandomID() generiert. Um beide Varianten bezüglich des Speicherverbrauchs zu vergleichen, wird auf das Node.js-Modul memwatch-next (https://github.com/marcominetti/node-memwatch) zurückgegriffen, das über den Node.js Package Manager mit dem Befehl npm install memwatch-next --save installiert werden kann. const memwatch = require('memwatch-next');
const hd = new memwatch.HeapDiff();
class Album {
constructor(title, year, artist, id, member, price) {
this.title = title;
this.year = year;
this.artist = artist;
this.id = id;
this.member = member;
this.price = price;
}
}
const getRandomID = (min, max) => {
return `${Math.floor(Math.random() * (max - min) + min)}`;
}
const albums = [];
for(let i = 0; i < 1000000; i++) {
albums.push(new Album('Title', 2000, 'Artist', getRandomID(1,10), 'Member' + i, 5.00));
}
const diff = hd.end();
console.log(diff.before.size); // 3.93 mb
console.log(diff.after.size); // 120.93 mb
console.log(albums.length); // 1000000
Listing 8.18 Beispielprogramm ohne Flyweight-Entwurfsmuster [ES2015]
Sie sehen in Listing 8.18, dass die Differenz der Speicherbelegung vor und nach der Schleife 117 MB beträgt. In Listing 8.19 dagegen beträgt die Differenz nur knapp 94 MB. Durch Anwendung des FlyweightEntwurfsmusters sparen wir also in diesem konkreten Fall etwa 20 %. Die Anwendung des Entwurfsmusters erfolgt in folgenden Schritten: 1. Entfernen der kontextabhängigen Informationen aus dem Objekt. Im Beispiel sind dies die Eigenschaften member und price aus der Klasse Album. 2. Implementierung einer Factory (siehe Abschnitt 8.2.1, »Objekte an einer zentralen Stelle erzeugen (Abstract Factory/Factory Method)«), die die Flyweight-Objekte verwaltet. Im Beispiel ist dies die Klasse AlbumFactory. Sie verwaltet in der Eigenschaft existingAlbums alle eindeutigen Alben, sprich für jede ID nur eine Objektinstanz. 3. Implementierung eines Managers, der die kontextabhängigen Informationen verwaltet. Im Beispiel ist dies die Klasse AlbumManager, die in der Eigenschaft albumDatabase unter entsprechender Angebots-ID (offerID) Objekte mit den
Eigenschaften member und price sowie einer Referenz auf das Album selbst verwaltet. const memwatch = require('memwatch-next');
const hd = new memwatch.HeapDiff();
// Flyweight
class Album {
constructor(title, year, artist, id) {
this.title = title;
this.year = year;
this.artist = artist;
this.id = id;
}
}
// FlyweightFactory
class AlbumFactory {
constructor() {
this.existingAlbums = {};
}
createAlbum(title, year, artist, id) {
const existingAlbum = this.existingAlbums[id];
if(existingAlbum) {
return existingAlbum;
} else {
const album = new Album(title, year, artist, id);
this.existingAlbums[id] = album;
return album;
}
}
getNumberOfAlbums() {
return Object.keys(this.existingAlbums).length;
}
}
class AlbumManager {
constructor(albumFactory) {
this.albumFactory = albumFactory;
this.albumDatabase = {};
}
addAlbum(offerID, title, year, artist, id, member, price) {
const album = this.albumFactory.createAlbum(title, year, artist, id);
this.albumDatabase[offerID] = {
member: member,
price: price,
album: album
};
}
getNumberOfAlbums() {
return Object.keys(this.albumDatabase).length;
}
updatePrice(offerID, price) {
const album = this.albumDatabase[offerID];
album.price = price;
}
}
const getRandomID = (min, max) => {
return `${Math.floor(Math.random() * (max - min) + min)}`;
}
const albumFactory = new AlbumFactory();
const albumManager = new AlbumManager(albumFactory);
for(let i = 0; i < 1000000; i++) {
albumManager.addAlbum(i, 'Title', 2000, 'Artist', getRandomID(1,10),
'Member' + i, 5.00);
}
const diff = hd.end();
console.log(albumFactory.getNumberOfAlbums()); // 9
console.log(albumManager.getNumberOfAlbums()); // 1000000
console.log(diff.before.size); // 4.21 mb
console.log(diff.after.size); // 98.1 mb
Listing 8.19 Beispielprogramm mit Flyweight-Entwurfsmuster [ES2015]
Merke Auch in JavaScript kann sich der Einsatz des FlyweightEntwurfsmusters lohnen, wenn innerhalb einer Anwendung viele ähnliche Objektinstanzen erzeugt werden.
8.3.5 Objekte mit zusätzlichen Funktionalitäten ausstatten (Decorator)
In Sprachen wie Java ist es nicht möglich, Objektinstanzen dynamisch zur Laufzeit um zusätzliches Verhalten zu erweitern. Das
Decorator-Entwurfsmuster adressiert genau diese Einschränkung. Beschreibung des Entwurfsmusters
Abbildung 8.11 zeigt die beteiligten Komponenten. ConcreteComponent bezeichnet die zu dekorierende Klasse, Decorator entsprechend die dekorierende Klasse mit zusätzlichem Verhalten (stellvertretend die Methode addedBehaviour()).
Abbildung 8.11 Klassendiagramm für das Decorator-Entwurfsmuster
Beide implementieren die Schnittstelle Component, so dass garantiert ist, dass die dekorierende Klasse an allen Stellen wie auch die dekorierte Klasse verwendet werden kann.
Zur Laufzeit wird dann, um einer Objektinstanz von ConcreteComponent zusätzliches Verhalten hinzuzufügen, eine Instanz von Decorator erzeugt und um die Instanz von ConcreteComponent »gewrappt«. Relevanz in JavaScript
Das Decorator-Entwurfsmuster stellt in statisch typisierten Sprachen also einen Weg dar, dynamisch zur Laufzeit das Verhalten von Objekten zu erweitern (im Gegensatz zur Vererbung, die zur Compile-Zeit ausgewertet wird). JavaScript dagegen ist ohnehin eine dynamische Sprache, die es ermöglicht, Objekte dynamisch zur Laufzeit zu verändern. Prinzipiell wäre das DecoratorEntwurfsmuster in seiner klassischen Form in JavaScript also gar nicht notwendig. Trotzdem macht es den Code insgesamt viel sauberer, wenn Sie Objekte nicht dynamisch verändern, sondern über Anwendung des Decorator-Entwurfsmusters. Zusätzlich ist in JavaScript das Dekorieren einzelner Funktionen bzw. Methoden möglich, das ich Ihnen im Folgenden vorstellen möchte. Listing 8.20 (Klassensyntax) und Listing 8.21 (pseudoklassische Variante) zeigen dazu ein Beispiel. Zunächst gibt es hier die einfache Klasse Album, die nichts anderes als ein Musikalbum repräsentiert. Neben dem Namen des Albums und dem Künstler kann über ein Konfigurationsobjekt auch der Preis für ein Album übergeben und später über die Methode getPrice() abgefragt werden. Da wir als Nächstes nicht die Klasse Album, sondern die Methode getPrice() dekorieren wollen, stellt diese Methode also sinngemäß die ConcreteComponent aus obigem Klassendiagramm dar.
Als Decorator kommt analog die Funktion sign() zum Einsatz. Sie soll den Anwendungsfall repräsentieren, dass ein Album von dem entsprechenden Künstler signiert wird und damit im Preis steigt. Sie erwartet als Parameter die zu dekorierende Funktion sowie den Kontext. Letzterer ist notwendig, um einen reibungslosen Aufruf der zu dekorierenden Funktion zu gewährleisten. Das eigentliche Dekorieren übernimmt die generische Helferfunktion decorate(). Sie macht nichts anderes, als für eine zu dekorierende Funktion und eine Decorator-Funktion eine Closure (siehe Abschnitt 2.5.3) zurückzugeben, innerhalb der die Decorator-Funktion aufgerufen wird. Diese Closure wird dann in einer separaten Zuweisung als neue Methode getPrice() am entsprechenden Objekt gesetzt. class Album {
constructor(config) {
this.name = config.name || "Untitled";
this.artist = config.artist || "VA";
this.price = config.price;
}
getPrice() {
return this.price;
}
}
const sign = (aFunction, context) => {
const price = aFunction.call(context);
return 2 * price;
}
const decorate = (decorator, aFunction, context) =>
() => decorator.call(context, aFunction, context);
const album = new Album({price: 40.0, name: "No more shall we part", artist: "Nick Cave"});
console.log(album.getPrice());
album.getPrice = decorate(sign, album.getPrice, album);
console.log(album.getPrice());
Listing 8.20 Anwendung des Decorator-Entwurfsmusters [ES2015]
function Album(config) {
this.name = config.name || "Untitled";
this.artist = config.artist || "VA";
this.price = config.price;
// Zu dekorierende Funktion
this.getPrice = function() {
return this.price;
}
}
// Decorator-Funktion
function sign(aFunction, context) {
const price = aFunction.call(context);
return 2 * price;
}
// Generische Helferfunktion
function decorate(decorator, aFunction, context) {
return function() {
return decorator.call(context, aFunction, context);
}
}
const album = new Album({price: 40.0, name: "No more shall we part",
artist: "Nick Cave"});
console.log(album.getPrice()); // 40.0
album.getPrice = decorate(sign, album.getPrice, album);
console.log(album.getPrice()); // 80.0
Listing 8.21 Anwendung des Decorator-Entwurfsmusters [ES5]
Merke Objekte können in JavaScript zwar dynamisch zur Laufzeit verändert werden, dennoch ist die Anwendung des DecoratorEntwurfsmusters sinnvoll. Zusätzlich ist es möglich, einzelne Funktionen bzw. Methoden zu »dekorieren«.
8.3.6 Einheitliche Schnittstelle für mehrere Schnittstellen (Facade)
Häufig haben Sie es bei der Verwendung von Bibliotheken mit einer Vielzahl verschiedener mehr oder weniger komplexer Schnittstellen zu tun.
Beschreibung des Entwurfsmusters
Das Facade-Entwurfsmuster dient dazu, eine einzige einheitliche Schnittstelle für eine Menge von Schnittstellen eines Systems bereitzustellen und den Zugriff auf diese Schnittstellen zu vereinheitlichen und zu zentralisieren. Die Facade soll dem Client die Verwendung des Subsystems erleichtern. Abbildung 8.12 stellt diesen Zusammenhang grafisch dar: Anstatt mit den einzelnen Komponenten des Subsystems direkt zu interagieren, läuft die gesamte Kommunikation über die Facade. Der Client muss sich nicht mit den Eigenarten der verschiedenen Schnittstellen des Subsystems auseinandersetzen, sondern kann bequem über die Facade auf die bereitgestellte Funktionalität zugreifen. Relevanz in JavaScript
Ein klassisches Beispiel für die Anwendung des FacadeEntwurfsmusters in JavaScript ist die Cross-Browser-Unterstützung, etwa zur Abstraktion des Event-Handlings. Zur Registrierung von Events an DOM-Elementen gibt es je nach Browser verschiedene Möglichkeiten: Im Internet Explorer ab Version 5 steht die Methode attachEvent() zur Verfügung, in den meisten anderen Browsern (und seit IE9) dagegen die Methode addEventListener().
Abbildung 8.12 Klassendiagramm für das Facade-Entwurfsmuster
Hinzu kommt der Fallback-Mechanismus über das Setzen von entsprechenden Eigenschaften (onclick, onchange etc.) am jeweiligen DOM-Element. Eine Facade kann den Zugriff bei diesem mehr oder weniger komplexen Sachverhalt vereinfachen, wie in Listing 8.22 und Listing 8.23 gezeigt. EventFacade verbirgt über die Methode addEvent() die browserspezifischen Details vor dem Client. Lediglich das DOM-Event, der Eventtyp und die Callback-Funktion müssen übergeben werden. Die technischen Details sind innerhalb der Methode verborgen. class EventFacade {
static addEvent(element, event, callback) {
if (typeof callback === 'function') {
if (window.addEventListener) {
element.addEventListener(event, callback, false);
} else if (document.attachEvent) {
element.attachEvent('on' + event, callback);
} else {
element['on' + event] = callback;
}
}
}
}
const button = document.getElementById('button');
EventFacade.addEvent(button, 'click', () => {
console.log('Button gedrückt');
});
Listing 8.22 Anwendung des Facade-Entwurfsmusters [ES2015]
const EventFacade = {
addEvent: function(element, event, callback) {
if (typeof callback === 'function') {
if (window.addEventListener) {
element.addEventListener(event, callback, false);
} else if (document.attachEvent) {
element.attachEvent('on' + event, callback);
} else {
element['on' + event] = callback;
}
}
}
};
const button = document.getElementById('button');
EventFacade.addEvent(button, 'click', function() {
console.log('Button gedrückt');
});
Listing 8.23 Anwendung des Facade-Entwurfsmusters [ES5]
Merke Das Facade-Entwurfsmuster kann auch in JavaScript dabei helfen, den Zugriff auf uneinheitliche oder komplexe Schnittstellen eines Subsystems zu vereinheitlichen bzw. zu vereinfachen.
8.3.7 Den Zugriff auf Objekte abfangen (Proxy)
Das Konzept von Proxies habe ich Ihnen ja bereits in Abschnitt 4.8 vorgestellt. Die Idee ist dabei, den Zugriff auf ein Objekt durch ein vorgelagertes Objekt abzufangen, beispielsweise um sogenannte Cross-Cutting Concerns wie Caching, Logging oder Validierung zu implementieren. Beschreibung des Entwurfsmusters
Die Komponenten dieses Entwurfsmusters sehen Sie in Abbildung 8.13. Proxy leitet von der gleichen Klasse ab wie das
Objekt (RealSubject), auf das Zugriffe abgefangen werden sollen. Damit kann das Proxy-Objekt an den gleichen Stellen verwendet werden wie das Originalobjekt. Zudem enthält es eine Referenz auf das Originalobjekt, um Anfragen auf dieses Objekt weiterleiten zu können.
Abbildung 8.13 Klassendiagramm für das Proxy-Entwurfsmuster
Relevanz in JavaScript
In JavaScript stehen Proxies, wie Sie wissen, seit ES2015 nativ zur Verfügung. Listing 8.24 zeigt das bereits aus Kapitel 4, »ECMAScript 2015 und neuere Versionen«, bekannte Beispiel eines Proxys, der die Anzahl der Zugriffe auf Objekte abfängt und mitzählt. const profiler = {
accesses: 0,
get(proxy, name) {
this.accesses++;
return proxy[name];
},
getAccesses() {
return this.accesses;
}
};
const max = {
name: 'Max'
};
const person = new Proxy(max, profiler);
for (let i = 0; i < 9; i++) {
console.log(person.name);
}
console.log(profiler.getAccesses());
Listing 8.24 Proxy als Profiler [ES2015]
Die Tatsache, dass Proxies mittlerweile Teil der Sprache sind, deutet darauf hin, wie wichtig und gefragt das Konzept in der Vergangenheit auch in der JavaScript-Entwicklung war (und immer noch ist). Listing 8.25 zeigt der Vollständigkeit halber an dieser Stelle noch einmal die aus Kapitel 4 bekannte Implementierung eines Proxys in ES5: var target = {'firstName' : 'Max'};
var proxy = Object.create(Object.getPrototypeOf(target), {});
Object.getOwnPropertyNames(target).forEach(function(property) {
var pd = Object.getOwnPropertyDescriptor(target, property);
Object.defineProperty(proxy, property, {
set: function(value) {
console.log('setze ' + value + ' für Eigenschaft ' + property);
target[property] = value;
},
get: function() {
console.log('liefere ' + target[property] +
' von Eigenschaft ' + property);
return target[property];
}
});
// Kopieren der Property-Decriptor-Attribute
return proxy;
});
console.log(proxy.firstName); // 'Max'
target.lastName = 'Mustermann';
console.log(proxy.lastName); // undefined
Listing 8.25 Emulation eines loggenden Proxys [ES5]
Merke Die Implementierung des Proxy-Entwurfsmusters ist in JavaScript seit ES2015 nicht mehr notwendig, da Proxies nun Bestandteil der Sprache sind. In ES5 können Proxies emuliert werden.
8.4 Verhaltensmuster In die Kategorie der Verhaltensmuster fallen die Entwurfsmuster, die die Kommunikation zwischen Objekten vereinfachen. Insgesamt gibt es folgende elf Erzeugungsmuster: Iterator, Observer, Template Method, Command, Strategy, Mediator, Memento, Visitor, State, Interpreter und Chain of Responsibility. 8.4.1 Über Datenstrukturen iterieren (Iterator)
Auch das Konzept von Iteratoren kennen Sie bereits aus Abschnitt 4.6.1. Die Idee dieses Entwurfsmusters ist es, den Aspekt der Iteration über eine Datenstruktur von dieser Datenstruktur zu entkoppeln, ohne deren interne Struktur nach außen sichtbar zu machen. Der Vorteil ist dabei, dass relativ einfach (und eben entkoppelt von der eigentlichen Datenstruktur) verschiedene Iteratorvarianten implementiert werden können. Beschreibung des Entwurfsmusters
Stellen Sie sich vor, Sie möchten über ein Array mit Zahlen iterieren. Dann wäre zu klären: Soll vom Beginn des Arrays oder vom Ende iteriert werden? Sollen eventuell einzelne Zahlen in der Iteration nicht beachtet werden, z.B. nur die geraden Zahlen ausgegeben werden? Anstatt dieses Wissen innerhalb der Datenstruktur zu definieren, kapselt man es in separaten Iteratoren. Abbildung 8.14 zeigt die an diesem Entwurfsmuster beteiligten Komponenten. Aggregate bzw. ConcreteAggregate stellen die Datenstruktur dar, Iterator bzw. ConcreteIterator die entsprechende Iterator-
Komponente. Die Client-Komponente wählt je nach Anwendungsfall den passenden Iterator aus und leitet die Iteration auf der Datenstruktur ein.
Abbildung 8.14 Klassendiagramm für das Iterator-Entwurfsmuster
Relevanz in JavaScript
In Abschnitt 4.6.1 haben Sie bereits gesehen, dass Iteratoren seit ES2015 nativ zur Sprache gehören und damit nicht erst in klassischer Form als Entwurfsmuster implementiert werden müssen. Listing 8.26 zeigt zur Wiederholung das aus Kapitel 4, »ECMAScript 2015 und neuere Versionen«, bekannte Beispiel zur Definition eines Iterators, der die Elemente eines Arrays in umgekehrter Reihenfolge ausgibt. Ein Iterator zeichnet sich in ES2015 dadurch aus, dass er eine Methode next() anbietet, die ein Objekt mit zwei Eigenschaften zurückgibt: die boolesche Eigenschaft done, die kennzeichnet, ob das Ende des Iterators erreicht wurde, sowie, falls dies nicht der Fall ist, die Eigenschaft value, die das entsprechende Element enthält.
Definieren Sie einen Iterator zudem an der jeweiligen Datenstruktur in der Eigenschaft Symbol.iterator, können Sie ihn auch innerhalb von Schleifen verwenden. const artists = ['Kyuss', 'QOTSA', 'Ben Harper', 'Monster Magnet'];
const artistsWrapper = {}
artistsWrapper.artists = artists;
artistsWrapper[Symbol.iterator] = function() {
const artists = this.artists;
let counter = this.artists.length-1;
// Rückgabe des Iterator-Objekts
return {
next: function(){
if (counter< 0) {
return {
done: true
};
} else {
return {
value: artists[counter--],
done: false
};
}
}
}
};
for(let artist of artistsWrapper) {
console.log(artist);
}
Listing 8.26 Definition und Verwendung eines Iterators [ES2015]
Iteratoren in ECMAScript 5 Auch ohne ES2015 ist es nicht schwierig, das IteratorEntwurfsmuster in JavaScript umzusetzen. Folgende beiden Listings zeigen zwei Beispiele: ersteres einen Iterator, der über die Elemente eines Arrays iteriert, letzteres einen Iterator, der über die Attribute eines DOM-Elements iteriert. In beiden Fällen bietet die Iterator-Komponente zwei Methoden an: die Methode next(), die das nächste Element zurückgibt, sowie die Methode hasNext(), die überprüft, ob es überhaupt ein nächstes Element gibt. Die
Position des Iterators wird in der Eigenschaft index gespeichert, die bei jedem Aufruf von next() um eins hochgezählt wird. const Iterator = function(elements) {
this.index = 0;
this.elements = elements;
}
Iterator.prototype.next = function() {
return this.elements[this.index++];
}
Iterator.prototype.hasNext = function() {
return this.index < this.elements.length;
}
const artists = [
'Ben Harper',
'Sprititual Beggars',
'Monster Magnet',
'Queens of the Stone Age'
];
const iterator = new Iterator(artists);
while(iterator.hasNext()) {
console.log(iterator.next());
}
Listing 8.27 Definition und Verwendung eines Iterators für Arrays [ES5]
In Listing 8.28 ist zu beachten: Da es sich bei den Eigenschaften eines DOM-Elements um kein Array handelt, wird es über Method Borrowing (siehe Abschnitt 2.2.2, »Funktionen aufrufen über die Methode ›call()‹«) zunächst in ein Array umgewandelt. const Iterator = function(node) {
this.index = 0;
this.elements = Array.prototype.slice.call(node.attributes);
}
Iterator.prototype.next = function() {
return this.elements[this.index++];
}
Iterator.prototype.hasNext = function() {
return this.index < this.elements.length;
}
const element = document.createElement('div');
element.setAttribute('id', 'button');
element.setAttribute('class', 'submit');
const iterator = new Iterator(element);
while(iterator.hasNext()) {
console.log(iterator.next());
}
Listing 8.28 Definition und Verwendung eines Iterators zur Ausgabe von Attributen eines DOM-Elements [ES5]
Merke Das Konzept von Iteratoren gehört seit ES2015 fest zur Sprache JavaScript. In ES5 können sie relativ einfach implementiert werden.
8.4.2 Den Zugriff auf Objekte beobachten (Observer)
Häufig ist es so, dass bestimmte Komponenten in einer Anwendung andere Komponenten benachrichtigen sollen, falls sich ihr Zustand ändert, damit sich diese anderen Komponenten aktualisieren können. Ein klassisches Beispiel hierfür sind die einzelnen Komponenten einer grafischen Oberfläche (wie beispielsweise Textfelder oder Tabelleneinträge), die sich gegenseitig bei Änderungen aktualisieren, z.B., wenn sich bei Eingabe in ein Textfeld der Wert eines anderen Textfeldes anpasst. Beschreibung des Entwurfsmusters
Das Ziel des Observer-Entwurfsmuster ist es, Zustandsänderungen eines Objekts abzufangen und andere, an der Zustandsänderung interessierte Objekte darüber zu informieren. Letztere können sich dazu an dem Objekt registrieren, das selbst eine Liste dieser Observer-Objekte verwaltet. Dieser Zusammenhang ist im Klassendiagramm in Abbildung 8.15 dargestellt: Subject stellt das Interface für Objekte dar, die beobachtet werden sollen, Observer das Interface für die
beobachtenden Objekte. Über attachObserver(), detachObserver() und notifyObserver() können Observer hinzugefügt, entfernt oder eben informiert werden. Ein Aufruf letzterer Methode ruft am jeweiligen Observer die Methode update() auf.
Abbildung 8.15 Klassendiagramm für das Observer-Entwurfsmuster
Relevanz in JavaScript
Das Observer-Entwurfsmuster wird relativ häufig in JavaScript verwendet. Listing 8.29 (Klassensyntax) und Listing 8.30 (pseudoklassische Variante) zeigen dazu ein einfaches Beispiel. Artist stellt das zu beobachtende Objekt dar und verwaltet intern eine Liste (List) von Fans (Instanzen von Person), die über addFan() hinzugefügt und über removeFan() entfernt werden können. Ein Aufruf der Methode newAlbum() sorgt dafür, dass intern über notifyNewAlbum() alle registrierten Observer, sprich die Fans, informiert werden und ihre entsprechende update()Methode aufgerufen wird. class List {
constructor() {
this.list = [];
}
add(object) {
return this.list.push(object);
}
count() {
return this.list.length;
}
get(index) {
if( index > -1 && index < this.list.length ){
return this.list[index];
}
}
removeAt(index) {
this.list.splice(index, 1);
}
}
// Subject
class Artist {
constructor(name) {
this.name = name;
this.albums = [];
this.fans = new List();
}
addFan(fan) {
this.fans.add(fan);
}
removeFan(fan) {
this.fans.removeAt(this.fans.indexOf(fan, 0));
}
newAlbum(album) {
this.albums.push(album);
this.notifyNewAlbum(album);
}
notifyNewAlbum(album) {
const fanCount = this.fans.count();
for (let i = 0; i < fanCount; i++) {
this.fans.get(i).update(album);
}
}
}
// Observer
class Person {
constructor(name) {
this.name = name;
}
update(album) {
console.log(`${this.name}: ${album}`);
}
}
const philip = new Person('Philip');
const christoph = new Person('Christoph');
const artist = new Artist('Tool');
artist.addFan(philip);
artist.addFan(christoph);
artist.newAlbum('Lateralus');
Listing 8.29 Das Observer-Entwurfsmuster in JavaScript [ES2015] function List() {
this.list = [];
}
List.prototype.add = function(object){
return this.list.push(object);
};
List.prototype.count = function(){
return this.list.length;
};
List.prototype.get = function(index){
if(index > -1 && index < this.list.length){
return this.list[ index ];
}
};
List.prototype.removeAt = function(index){
this.list.splice(index, 1);
};
// Subject
function Artist(name) {
this.name = name;
this.albums = [];
this.fans = new List();
}
Artist.prototype.addFan = function(fan){
this.fans.add(fan);
};
Artist.prototype.removeFan = function(fan){
this.fans.removeAt(this.fans.indexOf(fan, 0));
};
Artist.prototype.newAlbum = function (album) {
this.albums.push(album);
this.notifyNewAlbum(album);
};
Artist.prototype.notifyNewAlbum = function (album) {
const fanCount = this.fans.count();
for (let i = 0; i < fanCount; i++) {
this.fans.get(i).update(album);
}
};
// Observer
function Person(name) {
this.name = name;
}
Person.prototype.update = function(album){
console.log(this.name + ": " + album);
};
const philip = new Person("Philip");
const christoph = new Person("Christoph");
const artist = new Artist("Tool");
artist.addFan(philip);
artist.addFan(christoph);
artist.newAlbum("Lateralus");
Listing 8.30 Das Observer-Entwurfsmuster in JavaScript [ES5]
Observer in ECMAScript Ursprünglich war geplant, eine Methode Object.observe() in den ECMAScript-Standard zu übernehmen, die den Zustand von Objekten beobachtet. Dieser Vorschlag ist mittlerweile jedoch wieder verworfen worden.
Ereignisgesteuerte Programmierung Das Observer-Entwurfsmuster bildet in JavaScript konzeptionell auch die Grundlage für die sogenannte ereignisgesteuerte Programmierung. Dabei registrieren sich einzelne Komponenten (Event-Handler, Event-Listener oder eben auch Observer genannt) für bestimmte Events. Wird dann ein Event ausgelöst, tritt die entsprechende Komponente in Aktion. Listing 8.31 zeigt
ein Beispiel für die Anwendung in Node.js unter Verwendung des events-Moduls, das in der Installation von Node.js enthalten ist. const events = require('events');
const artist = new events.EventEmitter();
class Person {
constructor(name) {
this.name = name;
}
update(album) {
console.log(`${this.name}: ${album}`);
}
}
const philip = new Person('Philip');
christoph = new Person('Christoph');
artist.on('newAlbum', philip.update.bind(philip));
artist.on('newAlbum', christoph.update.bind(christoph));
artist.emit('newAlbum', 'Lateralus');
Listing 8.31 Ereignisgetriebene Entwicklung unter Node.js [ES2015]
Merke Das Observer-Entwurfsmuster kommt in JavaScript relativ häufig zum Einsatz. Ein verwandtes Konzept ist das der ereignisgesteuerten Programmierung, die insbesondere bei JavaScript-basierten Webanwendungen sowie in Node.js verwendet wird.
8.4.3 Eine Vorlage für einen Algorithmus definieren (Template Method)
Wenn sich innerhalb einer Anwendung gewisse Abläufe zwar in einzelnen Schritten unterscheiden, im Wesentlichen aber relativ
ähnlich sind, kann das sogenannte Template-MethodEntwurfsmuster dabei helfen, doppelten Code zu vermeiden. Beschreibung des Entwurfsmusters
Dieses Entwurfsmuster dient dazu, in einer Klasse durch eine Methode eine Vorlage für einen Algorithmus zu definieren, wobei einige der dazugehörigen Schritte abstrakt bleiben und von ableitenden Klassen implementiert werden. Die Oberklasse liefert quasi das Grundgerüst für den Algorithmus (in Abbildung 8.16 die Methode templateMethod()), die Details werden in den Unterklassen definiert (in Abbildung 8.16 die Methoden step1(), step2(), step3() und step4()).
Abbildung 8.16 Klassendiagramm für das Template-Method-Entwurfsmuster
Relevanz in JavaScript
Prinzipiell spricht nichts gegen die Verwendung des TemplateMethod-Entwurfsmusters in JavaScript, unabhängig davon, welche Art der Vererbung Sie verwenden. Ein Beispiel für die Umsetzung mit Klassensyntax zeigt Listing 8.32. Database ist die Basisklasse, sie definiert die Methode getArtists(), die wiederum die Template Method darstellt. Die einzelnen Schritte bestehen aus dem Verbinden zur Datenbank (connect()), dem Zugriff auf die
Ergebnisse (getResults()) und dem Trennen von der Datenbank (disconnect()). Die Schritte des Verbindens und Trennens der Datenbank sind immer gleich und bereits in der Klasse Database implementiert. Sie stellen die fixen Schritte in diesem Algorithmus dar. Die Methode getResults() dagegen ist in der Klasse Database nicht implementiert und muss stattdessen von den Unterklassen (im Beispiel ArtistRepository) implementiert werden. class Database {
getObjects() {
this.connect();
const result = this.getResults();
this.disconnect();
return result;
}
connect() {
console.log('Connect');
}
disconnect() {
console.log('Disconnect');
}
}
class ArtistRepository extends Database {
getResults() {
console.log('Get results');
return [{
name: 'Deep Purple'
},
{
name: 'Queens of the Stone Age'
}];
}
}
const artistRepository = new ArtistRepository();
const artists = artistRepository.getObjects();
console.log(artists);
// [
// { name: 'Deep Purple' },
// { name: 'Queens of the Stone Age' }
// ]
Listing 8.32 Implementierung des Template-Method-Entwurfsmusters [ES2015]
Variante: Template Method über Komposition
Eines der Prinzipien der objektorientierten Programmierung ist es, die Komposition der Vererbung vorzuziehen (Composition over Inheritance). Der Grund: Vererbung führt schnell zu hoher Kopplung zwischen Klassen. Komposition dagegen ermöglicht es, Klassen viel flexibler miteinander zu kombinieren. Dank First-Class-Funktionen können Sie das Template-MethodEntwurfsmuster in JavaScript auch, wie in Listing 8.33 zu sehen, ohne Vererbung realisieren, und zwar, indem Sie die Methoden, die vorher in der Unterklasse implementiert wurden, der Template Method als Parameter übergeben. Auf diese Weise entfällt die Unterklasse ArtistRepository. Ein schönes Beispiel dafür, wie sich objektorientierte und funktionale Konzepte von JavaScript kombinieren lassen. class Database {
getObjects(getResultsTemplateMethod) {
this.connect();
const result = getResultsTemplateMethod();
this.disconnect();
return result;
}
connect() {
console.log('Connect');
}
disconnect() {
console.log('Disconnect');
}
}
const database = new Database();
const artists = database.getObjects(
() => {
console.log('Get results');
return [{
name: 'Deep Purple'
},
{
name: 'Queens of the Stone Age'
}];
}
);
console.log(artists);
// [
// { name: 'Deep Purple' },
// { name: 'Queens of the Stone Age' }
// ]
Listing 8.33 Alternative Implementierung des Template-Method-Entwurfsmusters über Komposition statt Vererbung [ES2015]
Merke Das Template-Entwurfsmuster kann dank First-Class-Funktionen in JavaScript ohne Vererbung, sondern mit Hilfe von Komposition umgesetzt werden. Es spricht aber prinzipiell auch nichts gegen eine klassische Umsetzung dieses Entwurfsmusters.
8.4.4 Funktionen als Parameter übergeben (Command)
In Sprachen wie Java setzen Methodenaufrufe immer eine zugehörige Objektinstanz (im Fall einer Objektmethode) bzw. eine entsprechende Klasse (im Fall einer Klassenmethode) voraus und sind damit nicht »first class«, sie können nicht wie Objekte behandelt werden. Beschreibung des Entwurfsmusters
Um dennoch in solchen Sprachen First-Class-Funktionen nachzubilden, verwendet man daher im Allgemeinen das Command-Entwurfsmuster. Dabei ist die Idee, Methoden in Form einer Klasse (der Command-Klasse) zu kapseln. Eine Objektinstanz dieser Klasse entspricht dann sozusagen einer First-Class-Funktion. Das klassische Entwurfsmuster besteht dabei aus mehreren Komponenten, wie Sie im Klassendiagramm in Abbildung 8.17
sehen. Command definiert die Schnittstelle für den jeweiligen Befehl. Klassen, die einen konkreten Befehl darstellen, müssen diese Schnittstelle implementieren (im Beispiel ConcreteCommand). Um den entsprechend gekapselten Befehl auszuführen, ruft ConcreteCommand Methoden am entsprechenden Empfängerobjekt (Instanzen der Klasse Receiver) auf. Die Invoker-Komponente dagegen dient dazu, den Befehl überhaupt erst zu starten. Durch diese Anordnung sind Invoker und Receiver komplett voneinander entkoppelt. Die Client-Komponente schließlich ist dafür verantwortlich, Instanzen der Command-Klasse zu erstellen und dabei die jeweilige Receiver-Komponente zu übergeben.
Abbildung 8.17 Klassendiagramm für das Command-Entwurfsmuster
Relevanz in JavaScript
Folgt man beim Umsetzen des Entwurfsmusters in JavaScript stur den beschriebenen Vorgaben, wie in Listing 8.34 und Listing 8.35 anhand eines Videoplayers demonstriert, ergibt das ähnlich viel Quelltext, wie man es vom gesprächigen Java her gewohnt ist. SwitchOnCommand und SwitchOffCommand sind sozusagen Implementierungen von Command und definieren die »abstrakte« Funktion execute() jeweils neu. Button stellt die InvokerKomponente dar, VideoPlayer die Receiver-Komponente. Im Beispiel wird jeweils eine Instanz von SwitchOnCommand und SwitchOffCommand erzeugt und den entsprechenden Schaltflächenobjekten als Parameter übergeben. Button und VideoPlayer, das heißt Invoker-Komponente und ReceiverKomponente, sind also entkoppelt. // Aufrufer
class Button {
constructor(command) {
this.command = command;
this.click = function () {
command.execute();
}
}
}
// Empfänger
class VideoPlayer {
switchOn() {
console.log("einschalten");
}
switchOff() {
console.log("ausschalten");
}
}
// Commands
class Command {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
}
}
class SwitchOnCommand extends Command {
execute() {
this.receiver.switchOn();
}
}
class SwitchOffCommand extends Command {
execute() {
this.receiver.switchOff();
}
}
// Client
const videoPlayer = new VideoPlayer();
// Instanzen der Commands
const switchOnCommand = new SwitchOnCommand(videoPlayer);
const switchOffCommand = new SwitchOffCommand(videoPlayer);
const buttonOn = new Button(switchOnCommand);
const buttonOff = new Button(switchOffCommand);
buttonOn.click();
buttonOff.click();
Listing 8.34 Umsetzung des Command-Entwurfsmusters in JavaScript [ES2015] // Aufrufer
function Button(command) {
this.command = command;
this.click = function () {
command.execute();
}
}
// Empfänger
function VideoPlayer() {
this.switchOn = function () {
console.log("einschalten");
}
this.switchOff = function () {
console.log("ausschalten");
}
}
// Commands
const Command = function (receiver) {
this.receiver = receiver;
};
Command.prototype.execute = function () {
};
const SwitchOnCommand = function (receiver) {
Command.call(this, receiver);
};
SwitchOnCommand.prototype = Object.create(Command.prototype);
SwitchOnCommand.prototype.execute = function () {
this.receiver.switchOn();
}
const SwitchOffCommand = function (receiver) {
Command.call(this, receiver);
};
SwitchOffCommand.prototype = Object.create(Command.prototype);
SwitchOffCommand.prototype.execute = function () {
this.receiver.switchOff();
}
// Client
const videoPlayer = new VideoPlayer();
// Instanzen der Commands
const switchOnCommand = new SwitchOnCommand(videoPlayer);
const switchOffCommand = new SwitchOffCommand(videoPlayer);
const buttonOn = new Button(switchOnCommand);
const buttonOff = new Button(switchOffCommand);
buttonOn.click();
buttonOff.click();
Listing 8.35 Klassische Umsetzung des Command-Entwurfsmusters in JavaScript
Zugegebenermaßen ist solch eine Vorgehensweise in den meisten Fällen übertrieben, sie soll aber nochmals veranschaulichen, wie viel Code durch First-Class-Funktionen letztendlich gespart wird. Listing 8.36 und Listing 8.37 zeigen daher das Entwurfsmuster mit funktionaler Unterstützung: Button und VideoPlayer sind weiterhin entkoppelt, die Variablen switchOnCommand und switchOffCommand werden allerdings nicht mehr als Klassen modelliert, sondern direkt als Funktionen. Genauer gesagt handelt es sich hierbei um Funktionen, die jeweils eine Closure zurückgeben (siehe Abschnitt 2.5.3). Auf diese Weise lässt sich der Empfänger des
Commands übergeben und in der Closure kapseln. Der Konstruktor von Button ändert sich insofern, als das übergebene Objekt nun eine Funktion und nicht wie vorher ein normales Objekt ist. Diese Funktion kann direkt der Variablen click zugewiesen und mit click() aufgerufen werden. // Aufrufer
class Button {
constructor(command) {
this.click = command;
}
}
// Empfänger
class VideoPlayer {
switchOn() {
console.log("einschalten");
}
switchOff() {
console.log("ausschalten");
}
}
// Client
const videoPlayer = new VideoPlayer();
// Die Commands sind Funktionen
const switchOnCommand = (receiver) => () => receiver.switchOn();
const switchOffCommand = (receiver) => () => receiver.switchOff();
const buttonOn = new Button(switchOnCommand(videoPlayer));
const buttonOff = new Button(switchOffCommand(videoPlayer));
buttonOn.click();
buttonOff.click();
Listing 8.36 Umsetzung des Command-Entwurfsmusters mit funktionaler Unterstützung [ES2015] // Aufrufer
function Button(command) {
this.click = command;
}
// Empfänger
function VideoPlayer() {
this.switchOn = function () {
console.log("einschalten");
}
this.switchOff = function () {
console.log("ausschalten");
}
}
// Client
const videoPlayer = new VideoPlayer();
// Die Commands sind Funktionen
const switchOnCommand = function (receiver) {
return function () {
receiver.switchOn();
}
}
const switchOffCommand = function (receiver) {
return function () {
receiver.switchOff();
}
}
const buttonOn = new Button(switchOnCommand(videoPlayer));
const buttonOff = new Button(switchOffCommand(videoPlayer));
buttonOn.click();
buttonOff.click();
Listing 8.37 Umsetzung des Command-Entwurfsmusters mit funktionaler Unterstützung [ES5]
Wenn wie im Beispiel ein Command nur einen einzigen Funktionsaufruf kapselt, können Sie die Closure sogar weglassen und wie in Listing 8.38 die Methode des Empfängerobjekts direkt an den Aufrufer übergeben: const videoPlayer = new VideoPlayer();
const buttonOn = new Button(videoPlayer.switchOn.bind(videoPlayer));
const buttonOff = new Button(videoPlayer.switchOff.bind(videoPlayer));
buttonOn.click();
buttonOff.click();
Listing 8.38 Umsetzung des Command-Entwurfsmusters mit funktionaler Unterstützung [ES5] [ES2015]
Sobald allerdings mehrere Funktionsaufrufe involviert sind, sind diese in einer Closure zusammenzufassen: const switchOnOffCommand = (receiver) => () => {
receiver.switchOn();
receiver.switchOff();
}
Listing 8.39 Kapseln mehrerer Anweisungen in einer Closure [ES2015] const switchOnOffCommand = function (receiver) {
return function () {
receiver.switchOn();
receiver.switchOff();
}
}
Listing 8.40 Kapseln mehrerer Anweisungen in einer Closure [ES5]
Merke Das Command-Entwurfsmuster ist aufgrund von First-ClassFunktionen in JavaScript in seiner klassischen Form überflüssig. Lediglich bei komplexeren Commands, die beispielsweise über eine undo()-Methode verfügen, kann der Einsatz von Klassen hilfreich sein.
8.4.5 Algorithmen als Funktionen beschreiben (Strategy)
Das in Abschnitt 8.4.3, »Eine Vorlage für einen Algorithmus definieren (Template Method)«, beschriebene Entwurfsmuster Template Method dient dazu, einzelne Schritte eines Algorithmus in Unterklassen anzupassen. Doch manchmal möchte man nicht nur einzelne Schritte, sondern den gesamten Algorithmus als solchen zur Laufzeit austauschen. Beschreibung des Entwurfsmusters
Das Strategy-Entwurfsmuster adressiert genau diese Problemstellung, nämlich alternative Algorithmen (bzw. Strategien) für eine bestimmte Aufgabe bereitzustellen, die dynamisch zur
Laufzeit ausgewählt werden. Die Idee dieses Entwurfsmusters ist es, die verschiedenen Strategien in Form einzelner Klassen bereitzustellen, die jeweils das gleiche Interface implementieren. Auf diese Weise kann an allen Stellen in einer Anwendung, an denen das Interface verwendet wird, zur Laufzeit eine der Implementierungen verwendet werden. Die beteiligten Komponenten sind in Abbildung 8.18 dargestellt: Strategy als Interface für den austauschbaren Algorithmus, ConreteStrategy1 und ConcreteStrategy2 als exemplarische Varianten dieses Algorithmus.
Abbildung 8.18 Klassendiagramm für das Strategy-Entwurfsmuster
Relevanz in JavaScript
Prinzipiell ist der Einsatz des Strategy-Entwurfsmusters auch in JavaScript sinnvoll. Statt jedoch wie in der klassischen Variante die Strategien über einzelne Klassen zu repräsentieren, können Sie wie schon beim Command-Entwurfsmuster (siehe Abschnitt 8.4.4,
»Funktionen als Parameter übergeben (Command)«) direkt Funktionen verwenden. Folgende beiden Listings verdeutlichen dies. Während in Listing 8.41 und Listing 8.42 die Umsetzung des Entwurfsmusters unter Verwendung von Klassen bzw. Pseudoklassen zu sehen ist, zeigen Listing 8.43 und Listing 8.44 die Umsetzung unter Berücksichtigung der funktionalen Aspekte von JavaScript. Gezeigt ist jeweils die Sortierung von Arrays mit Hilfe der Methode Array.sort(). Diese erwartet als Parameter eine Funktion, die zwei Werte miteinander vergleicht und somit die Sortierreihenfolge der Werte des Arrays beeinflusst. Diese Vergleichsfunktionen stellen im Beispiel also die verschiedenen Strategien dar. Sie sehen, dass die funktionale Implementierung wesentlich schlanker ist und ohne Boilerplate-Code auskommt. class Comparator {
compare (value1, value2) {
}
}
class DescendingComparator extends Comparator {
compare(value1, value2) {
return value1 < value2;
}
}
class AscendingComparator extends Comparator {
compare(value1, value2) {
return value1 > value2;
}
}
const descendingComparator = new DescendingComparator();
const ascendingComparator = new AscendingComparator();
const array = [4, 5, 8, 3, 4, 2, 9, 4, 5];
array.sort(descendingComparator.compare);
console.log(array);
array.sort(ascendingComparator.compare);
console.log(array);
Listing 8.41 Implementierung des Strategy-Entwurfsmusters in JavaScript [ES2015]
const Comparator = function () {
};
Comparator.prototype.compare = function (value1, value2) {
};
const DescendingComparator = function () {
};
DescendingComparator.prototype = Object.create(Comparator.prototype);
DescendingComparator.prototype.compare = function (value1, value2) {
return value1 < value2;
}
const AscendingComparator = function () {
};
AscendingComparator.prototype = Object.create(Comparator.prototype);
AscendingComparator.prototype.compare = function (value1, value2) {
return value1 > value2;
}
const descendingComparator = new DescendingComparator();
const ascendingComparator = new AscendingComparator();
const array = [4, 5, 8, 3, 4, 2, 9, 4, 5];
array.sort(descendingComparator.compare);
console.log(array);
array.sort(ascendingComparator.compare);
console.log(array);
Listing 8.42 Klassische Implementierung des Strategy-Entwurfsmusters in JavaScript [ES5]
Hinweis Auch die Implementierungen in Listing 8.41 und Listing 8.42 nutzen bereits First-Class-Funktionen. Anstatt der Methode sort() die Instanzen descendingComparator und ascendingComparator zu übergeben, werden direkt deren compare()-Methoden übergeben. Im klassischen StrategyEntwurfsmuster wäre das anders – hier müssten Objektinstanzen übergeben werden. const array = [4, 5, 8, 3, 4, 2, 9, 4, 5];
const descendingSorting = (value1, value2) => value1 < value2;
const ascendingSorting = (value1, value2) => value1 > value2;
array.sort(descendingSorting);
console.log(array);
array.sort(ascendingSorting);
console.log(array);
Listing 8.43 Implementierung des Strategy-Entwurfsmusters mit funktionaler Unterstützung [ES2015] const array = [4, 5, 8, 3, 4, 2, 9, 4, 5];
const descendingSorting = function (value1, value2) {
return value1 < value2;
};
const ascendingSorting = function (value1, value2) {
return value1 > value2;
};
array.sort(descendingSorting);
console.log(array);
array.sort(ascendingSorting);
console.log(array);
Listing 8.44 Implementierung des Strategy-Entwurfsmusters mit funktionaler Unterstützung [ES5]
Hinweis Ein Beispiel für die Anwendung des Strategy-Entwurfsmusters ist die Bibliothek Passport.js für die Authentifizierung bei Webanwendungen (http://www.passportjs.org/). Die verschiedenen Authentifizierungsmethoden (OAuth, Facebook, Twitter etc.) sind dabei jeweils als Strategies implementiert.
Merke Wie auch das Command-Entwurfsmuster ist ebenso das StrategyEntwurfsmuster in JavaScript dank First-Class-Funktionen in klassischer Form in den meisten Fällen überflüssig.
8.4.6 Das Zusammenspiel mehrerer Objekte koordinieren (Mediator)
Häufig müssen in einer Anwendung verschiedene eigentlich voneinander unabhängige Objekte (bzw. allgemeiner:
Komponenten) miteinander kommunizieren. Um hierbei die Komponenten weiterhin unabhängig voneinander zu halten, kommt das folgende Entwurfsmuster zum Einsatz. Beschreibung des Entwurfsmusters
Das Mediator-Entwurfsmuster hilft dabei, die Kommunikation zwischen mehreren Objekten zu koordinieren, und ist immer dann sinnvoll, wenn Sie es in einer Anwendung mit mehreren Objekten zu tun haben, die alle jeweils über Zustandsänderungen aller anderen Objekte informiert werden sollen. Ein klassisches Beispiel ist ein Chatsystem, in dem innerhalb eines Chats alle Teilnehmer über Nachrichten der anderen informiert werden. Die Komponenten dieses Entwurfsmusters zeigt Abbildung 8.19. Colleague bzw. ConcreteColleague repräsentieren die einzelnen teilnehmenden Objekte. Sie benachrichtigen die Mediator-Komponente, die wiederum dafür sorgt, dass die entsprechenden ConcreteColleague-Komponenten aktualisiert werden.
Abbildung 8.19 Klassendiagramm für das Mediator-Entwurfsmuster
Relevanz in JavaScript
Das Konzept des Mediator-Entwurfsmusters wird in JavaScript relativ häufig verwendet und kommt meistens in Form der PublishSubscribe-Technik zum Einsatz. Beispiel hierfür ist die Bibliothek Mediator.js (http://thejacklawson.com/Mediator.js), die das Erzeugen und Verwalten von Mediatoren vereinfacht. Die Bibliothek kann sowohl im Browser als auch als Node.js-Modul genutzt werden. Installiert wird sie in letzterem Fall über den Befehl npm install mediator-js. Eine sehr vereinfachte Version des Chatraum-Beispiels zeigen Listing 8.45 (Klassensyntax) und Listing 8.46 (pseudoklassische Variante). Die beiden User-Instanzen user1 und user2 registrieren sich über die Methode subscribe() an dem Mediator. Übergeben werden dazu das Thema (vergleichbar mit einem Event), für das die Registrierung erfolgen soll, sowie die Funktion (bzw. in diesem Fall die Objektmethode), die aufgerufen werden soll, wenn der Mediator (über die Methode publish()) für das entsprechende Thema eine Benachrichtigung verschickt. const { Mediator } = require('mediator-js');
const chatroom = new Mediator();
class User {
constructor(name) {
this.name = name;
}
log(message) {
console.log(`${this.name} empfängt Nachricht: ${message}`);
}
}
const topic = 'Professionelles JavaScript';
const user1 = new User('Max');
const user2 = new User('Moritz');
chatroom.subscribe(topic, user1.log.bind(user1));
chatroom.subscribe(topic, user2.log.bind(user2));
chatroom.publish(topic, 'Herzlich willkommen');
Listing 8.45 Anwendung des Mediator-Entwurfsmusters in JavaScript [ES2015]
const Mediator = require("mediator-js").Mediator;
const chatroom = new Mediator();
function User(name) {
this.name = name;
}
User.prototype.log = function(message) {
console.log(this.name + ' empfängt Nachricht: ' + message);
}
const topic = 'Professionelles JavaScript';
const user1 = new User('Max');
const user2 = new User('Moritz');
chatroom.subscribe(topic, user1.log.bind(user1));
chatroom.subscribe(topic, user2.log.bind(user2));
chatroom.publish(topic, 'Herzlich willkommen');
Listing 8.46 Anwendung des Mediator-Entwurfsmusters in JavaScript [ES5]
Merke Das Mediator-Entwurfsmuster kommt in JavaScript in Form des Publish-Subscribe-Mechanismus zum Einsatz.
8.4.7 Den Zustand eines Objekts speichern (Memento)
Oft möchte man den Zustand einzelner Objekte speichern und zu einem späteren Zeitpunkt wiederherstellen können. Beschreibung des Entwurfsmusters
Das Memento-Entwurfsmuster dient genau dazu: den internen Zustand eines Objekts zu speichern und später wiederherstellen zu können. Folgende in Abbildung 8.20 gezeigten Komponenten kommen dabei zum Einsatz: Originator bezeichnet das Objekt, dessen Zustand gespeichert werden soll, Memento stellt den jeweiligen gespeicherten Zustand dar, und die CaretakerKomponente dient der Verwaltung verschiedener MementoInstanzen.
Abbildung 8.20 Klassendiagramm für das Memento-Entwurfsmuster
Relevanz in JavaScript
In JavaScript können Sie das Memento-Entwurfsmuster relativ einfach umsetzen. Die Sprache sieht mit Unterstützung des JSONFormats bereits vor, Objektinstanzen speichern und wiederherstellen zu können. Dazu bedienen Sie sich einfach der beiden Methoden des globalen JSON-Objekts: stringify() zum Speichern und parse() zum Laden jeweiliger Objektinstanzen. Listing 8.47 und Listing 8.48 zeigen dazu ein Beispiel. Artist stellt die Originator-Komponente dar, das Objekt, dessen Zustand gespeichert werden soll. Dazu bietet es die Methode saveToMemento() an, die aus der jeweiligen Artist-Instanz eine String-Variante im JSON-Format erzeugt. Diese Zeichenkette stellt die Memento-Komponente dar. ArtistStorage repräsentiert die Caretaker-Komponente und verwaltet in der Eigenschaft mementos ein oder mehrere MementoObjekte. Über die Methode restoreFromMemento() können Sie außerdem eine Artist-Instanz auf Basis solcher Memento-Objekte
wiederherstellen.
// Originator
class Artist {
constructor(name, genre) {
this.name = name;
this.genre = genre;
}
saveToMemento() {
// Memento
const memento = JSON.stringify(this);
return memento;
}
replaceFromMemento(memento) {
// Memento
const m = JSON.parse(memento);
this.name = m.name;
this.genre = m.genre;
}
}
// Caretaker
class ArtistStorage {
constructor() {
this.mementos = new Map();
}
add(key, memento) {
this.mementos.set(key, memento);
}
get(key) {
return this.mementos.get(key);
}
}
const kyuss = new Artist('Kyuss', 'Stonerrock');
const monsterMagnet = new Artist('Monster Magnet', 'Spacerock');
const artistStorage = new ArtistStorage();
// Artist { name: 'Kyuss', genre: 'Stonerrock' }
console.log(kyuss);
// Artist { name: 'Monster Magnet', genre: 'Spacerock' }
console.log(monsterMagnet);
artistStorage.add(1, kyuss.saveToMemento());
artistStorage.add(2, monsterMagnet.saveToMemento());
kyuss.genre = 'Klassik';
monsterMagnet.genre = 'Blues';
// Artist { name: 'Kyuss', genre: 'Klassik' }
console.log(kyuss);
// { name: 'Monster Magnet', genre: 'Blues' }
console.log(monsterMagnet);
kyuss.replaceFromMemento(artistStorage.get(1));
monsterMagnet.replaceFromMemento(artistStorage.get(2));
// Artist { name: 'Kyuss', genre: 'Stonerrock' }
console.log(kyuss);
// Artist { name: 'Monster Magnet', genre: 'Spacerock' }
console.log(monsterMagnet);
Listing 8.47 Anwendung des Memento-Entwurfsmusters in JavaScript [ES2015] // Originator
const Artist = function(name, genre) {
this.name = name;
this.genre = genre;
}
Artist.prototype = {
saveToMemento: function() {
// Memento
const memento = JSON.stringify(this);
return memento;
},
replaceFromMemento: function(memento) {
// Memento
const m = JSON.parse(memento);
this.name = m.name;
this.genre = m.genre;
}
}
// Caretaker
const ArtistStorage = function() {
this.mementos = {};
this.add = function(key, memento) {
this.mementos[key] = memento;
},
this.get = function(key) {
return this.mementos[key];
}
}
const kyuss = new Artist('Kyuss', 'Stonerrock');
const monsterMagnet = new Artist('Monster Magnet', 'Spacerock');
const artistStorage = new ArtistStorage();
// { name: 'Kyuss', genre: 'Stonerrock' }
console.log(kyuss);
// { name: 'Monster Magnet', genre: 'Spacerock' }
console.log(monsterMagnet);
artistStorage.add(1, kyuss.saveToMemento());
artistStorage.add(2, monsterMagnet.saveToMemento());
kyuss.genre = 'Klassik';
monsterMagnet.genre = 'Blues';
// { name: 'Kyuss', genre: 'Klassik' }
console.log(kyuss);
// { name: 'Monster Magnet', genre: 'Blues' }
console.log(monsterMagnet);
kyuss.replaceFromMemento(artistStorage.get(1));
monsterMagnet.replaceFromMemento(artistStorage.get(2));
// { name: 'Kyuss', genre: 'Stonerrock' }
console.log(kyuss);
// { name: 'Monster Magnet', genre: 'Spacerock' }
console.log(monsterMagnet);
Listing 8.48 Anwendung des Memento-Entwurfsmusters in JavaScript [ES5]
Merke Das Memento-Entwurfsmuster kann in JavaScript dank der nativen Unterstützung des JSON-Formats relativ einfach implementiert werden.
8.4.8 Operationen auf Objekten von Objekten entkoppeln (Visitor)
In klassenbasierten Sprachen möchte man häufig vermeiden, für eine Objektstruktur verschiedene, nicht miteinander verwandte Operationen in der entsprechenden Klasse zu definieren, da dies schnell zu unübersichtlichen und aufgeblähten Klassen führt. Beschreibung des Entwurfsmusters
Das Ziel beim Visitor-Entwurfsmuster ist es, für eine Objektstruktur neue Operationen definieren zu können, ohne dies in der entsprechenden Klasse zu tun. Die Operation wird stattdessen in einer eigenen Visitor-Implementierung definiert (siehe Abbildung 8.21). Die Elemente in der entsprechenden Objektstruktur stellen zudem eine Methode accept() zur Verfügung, die eine Instanz eines Visitors entgegennimmt, so dass dieser die Operation auf den Elementen ausführen kann. Relevanz in JavaScript
Sie werden es vielleicht schon ahnen – das Visitor-Entwurfsmuster kann in JavaScript oftmals einfacher umgesetzt werden, da die Sprache es bereits ermöglicht, dynamisch Methoden zur Laufzeit zu Objekten hinzuzufügen: zum einen über das Hinzufügen von Methoden am Prototyp, um eine Methode direkt allen ableitenden Objektinstanzen zur Verfügung zu stellen (siehe Listing 8.51), zum anderen über die kopierende Vererbung, um gezielt einzelne Objekte mit der neuen Funktionalität auszustatten (siehe Listing 8.52).
Abbildung 8.21 Klassendiagramm für das Visitor-Entwurfsmuster
Werfen wir aber zunächst einen Blick auf die klassische Variante des Entwurfsmusters (siehe Listing 8.49 und Listing 8.50). Die Variable albums stellt die Objektstruktur dar, die einzelnen Instanzen von Album die Elemente der Objektstruktur. Über die Methode accept() nehmen diese ein Visitor-Objekt entgegen. Das Objekt Discount repräsentiert einen solchen Visitor, der dafür sorgt, dass der Preis im jeweiligen Album-Objekt entsprechend herabgesetzt wird.
class Album {
constructor(artist, title, year, price) {
this.artist = artist;
this.title = title;
this.year = year;
this.price = price;
}
accept(visitor) {
visitor.visit(this);
}
getPrice() {
return this.price;
}
setPrice(newPrice) {
this.price = newPrice;
}
toString() {
return `${this.artist}: ${this.title} (${this.year}) ${this.price}€`;
}
}
class Discount {
constructor(discount) {
this.discount = discount;
}
visit(album) {
album.setPrice(Math.round(album.getPrice() * this.discount));
}
}
const albums = [
new Album('The Doors', 'The Doors', 1967, 10),
new Album('The Doors', 'Strange Days', 1967, 10),
new Album('The Doors', 'Waiting for the Sun', 1968, 10),
new Album('The Doors', 'The Soft Parade', 1969, 10)
];
const visitorDiscount = new Discount(0.8);
albums.forEach((album) => {
album.accept(visitorDiscount);
console.log(album.toString());
});
Listing 8.49 Anwendung des Visitor-Entwurfsmusters in JavaScript [ES2015] const Album = function (artist, title, year, price) {
this.accept = function (visitor) {
visitor.visit(this);
};
this.getPrice = function () {
return price;
};
this.setPrice = function (newPrice) {
price = newPrice;
};
this.toString = function() {
return artist + ': ' + title + ' (' + year + ') ' + price + '€';
}
};
const Discount = function(discount) {
this.visit = function(album) {
album.setPrice(Math.round(album.getPrice() * discount));
};
};
const albums = [
new Album('The Doors', 'The Doors', 1967, 10),
new Album('The Doors', 'Strange Days', 1967, 10),
new Album('The Doors', 'Waiting for the Sun', 1968, 10),
new Album('The Doors', 'The Soft Parade', 1969, 10)
];
const visitorDiscount = new Discount(0.8); // 20% Rabatt
albums.forEach(function(album) {
album.accept(visitorDiscount);
console.log(album.toString());
});
Listing 8.50 Anwendung des Visitor-Entwurfsmusters in JavaScript [ES5]
Anstatt das klassische Visitor-Entwurfsmuster zu nutzen, könnten Sie man in JavaScript auch so vorgehen wie in Listing 8.51 gezeigt. Hier entfallen sowohl die Visitor-Komponente als auch die Methode accept() in der Pseudoklasse Album. Stattdessen wird die Methode setDiscount() dynamisch am Prototyp von Album definiert. Dies widerspricht zwar dem ursprünglichen Ziel des Visitor-Entwurfsmusters insofern, als die Operation nicht mehr losgelöst von der Klasse ist und daher den Prototyp aufbläht, allerdings ist das in JavaScript nicht so tragisch wie beispielsweise in Java, da die Anpassungen des Prototyps an nahezu beliebiger Stelle
im Code vorkommen können und nicht (wie in Java) in einer einzelnen Quelltextdatei stehen müssen. class Album {
constructor(artist, title, year, price) {
this.artist = artist;
this.title = title;
this.year = year;
this.price = price;
}
getPrice() {
return this.price;
}
setPrice(newPrice) {
this.price = newPrice;
}
toString() {
return `${this.artist}: ${this.title} (${this.year}) ${this.price}€`;
}
}
const albums = [
new Album('The Doors', 'The Doors', 1967, 10),
new Album('The Doors', 'Strange Days', 1967, 10),
new Album('The Doors', 'Waiting for the Sun', 1968, 10),
new Album('The Doors', 'The Soft Parade', 1969, 10)
];
Album.prototype.setDiscount = function(discount) {
this.setPrice(Math.round(this.getPrice() * discount));
};
albums.forEach((album) => {
album.setDiscount(0.8);
console.log(album.toString());
});
Listing 8.51 Alternative zum Visitor-Entwurfsmuster über dynamische Methoden am Prototyp [ES2015]
Alternativ zur Definition neuer Operationen am Prototyp lassen sich mit Hilfe kopierender Vererbung (siehe Abschnitt 3.3.4) auch einzelnen Objekten neue Operationen bzw. Methoden zur Laufzeit hinzufügen. Listing 8.52 zeigt das entsprechend angepasste Beispiel.
Sie sehen hier, dass die Methode setDiscount() des DiscountObjekts über die Funktion extend() in die einzelnen Objektinstanzen von Album hineinkopiert wird. Im Gegensatz zu Listing 8.51 bläht dies den Prototyp nicht auf, führt auf der anderen Seite aber zu mehrfachen Kopien der setDiscount()-Methode. Hier müssen Sie abwägen, welche der vorgestellten Techniken Sie wählen. class Album {
constructor(artist, title, year, price) {
this.artist = artist;
this.title = title;
this.year = year;
this.price = price;
}
getPrice() {
return this.price;
}
setPrice(newPrice) {
this.price = newPrice;
}
toString() {
return `${this.artist}: ${this.title} (${this.year}) ${this.price}€`;
}
}
const albums = [
new Album('The Doors', 'The Doors', 1967, 10),
new Album('The Doors', 'Strange Days', 1967, 10),
new Album('The Doors', 'Waiting for the Sun', 1968, 10),
new Album('The Doors', 'The Soft Parade', 1969, 10)
];
const extend = (target, source) => {
target = target || {};
for(let property in source) {
if(source.hasOwnProperty(property)) {
target[property] = source[property];
}
}
return target;
}
const Discount = {
setDiscount(discount) {
this.setPrice(Math.round(this.getPrice() * discount));
}
}
albums.forEach((album) => {
extend(album, Discount);
album.setDiscount(0.8);
console.log(album.toString());
});
Listing 8.52 Alternative zum Visitor-Entwurfsmuster über kopierende Vererbung [ES2015]
Merke Das Visitor-Entwurfsmuster hilft dabei, Operationen losgelöst von der Objektstruktur zu definieren, auf der sie operieren. In JavaScript können Sie dieses Entwurfsmuster entweder in der klassischen Form umsetzen oder alternativ Operationen dynamisch am Prototyp oder an einzelnen Objektinstanzen hinzufügen.
8.4.9 Das Verhalten eines Objekts abhängig vom Zustand ändern (State)
Wenn das Verhalten eines Objekts abhängig von seinem aktuellen Zustand sein soll, bietet sich das folgende Entwurfsmuster an. Beschreibung des Entwurfsmusters
Das State-Entwurfsmuster dient dazu, das Verhalten eines Objekts abhängig von seinem Zustand anzupassen. Statt dies über mehrere if-Abfragen innerhalb der Methoden des Objekts zu machen, lagert man das zustandsabhängige Verhalten in einzelne State-Klassen aus (ConcreteState1, ConcreteState2 in Abbildung 8.22). Für
jeden Zustand eines Objekts erstellt man also eine Klasse, die das entsprechende Verhalten des Objekts kapselt.
Abbildung 8.22 Klassendiagramm für das State-Entwurfsmuster
Relevanz in JavaScript
In JavaScript ist das State-Entwurfsmuster durchaus berechtigt, wie die folgenden Listings verdeutlichen. Gezeigt ist jeweils das Objekt videoPlayer, das (der Einfachheit halber) über zwei verschiedene Zustände verfügt: Entweder spielt der Videoplayer gerade einen Film ab, oder er ist gestoppt. Analog zu den zwei Zuständen verfügt das Objekt über die zwei Methoden play() und stop(), die jeweils unter Berücksichtigung des aktuellen Zustands eine andere Ausgabe liefern: Ein Aufruf der Methode play() im Zustand stopped erzeugt die Ausgabe Video abspielen, im Zustand playing dagegen die Ausgabe Spielt schon. Umgekehrt erzeugt ein Aufruf der Methode stop() im Zustand playing die Ausgabe Video stoppen im Zustand stopped dagegen die Ausgabe Schon gestoppt.
Listing 8.53 zeigt die Variante ohne State-Entwurfsmuster. Sie sehen, dass die zustandsabhängige Ausgabe innerhalb der Methoden play() und stop() über if-else-Anweisungen gehandhabt wird. Sie können sich hierbei leicht ausmalen, dass die Komplexität dieser Anweisungen mit steigender Anzahl an Zuständen entsprechend zunimmt und vor allem dem Open/Closed-Prinzip widerspricht. class VideoPlayer {
constructor() {
this.status = 'stopped';
}
play() {
if (this.status === 'playing') {
console.log('Spielt schon');
return;
} else if (this.status === 'stopped') {
console.log('Video abspielen');
this.status = 'playing';
}
}
stop() {
if (this.status === 'playing') {
console.log('Video stoppen');
this.status = 'stopped';
} else if (this.status === 'stopped') {
console.log('Schon gestoppt');
return;
}
}
}
const videoPlayer = new VideoPlayer();
videoPlayer.play();
videoPlayer.play(); // Spielt schon
videoPlayer.stop();
videoPlayer.stop(); // Schon gestoppt
videoPlayer.play();
videoPlayer.stop();
Listing 8.53 Beispiel ohne Verwendung des State-Entwurfsmusters [ES2015]
Um dies zu vermeiden, geht man beim State-Entwurfsmuster wie in Listing 8.54 vor. Das Objekt videoPlayer hat hier zwei
Eigenschaften: Die Eigenschaft states bezeichnet die Menge aller Zustände, die das Objekt einnehmen kann, die Eigenschaft state repräsentiert den aktuellen Zustand. Das Wissen darüber, was im jeweiligen Zustand bei Aufruf der Methoden play() und stop() von videoPlayer geschehen soll, kapseln die Zustandsobjekte in eigenen Methoden play() und stop(). Rufen Sie nun eine der beiden gleichbenannten Methoden am videoPlayer-Objekt auf, delegieren diese Methoden den Aufruf entsprechend an das aktuelle Zustandsobjekt. class VideoPlayerState {
constructor(videoPlayer) {
this.videoPlayer = videoPlayer;
}
}
class PlayingState extends VideoPlayerState {
play() {
console.log('Spielt schon');
}
stop() {
console.log('Video stoppen');
this.videoPlayer.changeState(this.videoPlayer.states.stopped);
}
}
class StoppedState extends VideoPlayerState {
play() {
console.log('Video abspielen');
this.videoPlayer.changeState(this.videoPlayer.states.playing);
}
stop() {
console.log('Schon gestoppt');
}
}
class VideoPlayer {
constructor() {
this.status = 'stopped';
this.states = {
playing: new PlayingState(this),
stopped: new StoppedState(this)
};
this.initialize();
}
changeState(state) {
if (this.state !== state) {
this.state = state;
}
}
play() {
this.state.play();
}
stop() {
this.state.stop();
}
initialize() {
this.state = this.states.stopped;
}
}
const videoPlayer = new VideoPlayer();
videoPlayer.play();
videoPlayer.play(); // Spielt schon
videoPlayer.stop();
videoPlayer.stop(); // Schon gestoppt
videoPlayer.play();
videoPlayer.stop();
Listing 8.54 Beispiel für die Anwendung des State-Entwurfsmusters in JavaScript [ES2015]
Abbildung 8.23 gibt Ihnen noch kurz einen Überblick darüber, welche Objekte aus Listing 8.54 den Komponenten in Abbildung 8.22 entsprechen. Merke Das State-Entwurfsmuster kann auch in JavaScript dabei helfen, den Quelltext sauberer zu gestalten. Und zwar immer dann, wenn
Sie es mit Objekten zu tun haben, die je nach Zustand ein anderes Verhalten aufweisen.
Abbildung 8.23 Klassendiagramm für das Beispiel
8.4.10 Eine Repräsentation für die Grammatik einer Sprache definieren (Interpreter)
Es folgt die Beschreibung eines Entwurfsmusters, das in der Praxis extrem selten eingesetzt wird, das sogenannte InterpreterEntwurfsmuster. Beschreibung des Entwurfsmusters
Das Interpreter-Entwurfsmuster kommt dann zum Einsatz, wenn man innerhalb einer Anwendung dem Nutzer/der Nutzerin eine eigene (Skript-)Sprache zur Verfügung stellen möchte, die dann programmintern geparst und ausgewertet wird. Das
Klassendiagramm dazu zeigt Abbildung 8.24. Das Entwurfsmuster wird in der Praxis jedoch relativ selten verwendet (man spricht hin und wieder auch scherzhaft von »22 GoF-Entwurfsmustern plus einem, das als Witz gemeint war«).
Abbildung 8.24 Klassendiagramm für das Interpreter-Entwurfsmuster
Relevanz in JavaScript
In JavaScript wird das Entwurfsmuster so gut wie gar nicht angewandt. Eine Implementierung wäre höchstens zu Demonstrationszwecken sinnvoll, soll aber an dieser Stelle nicht weiter vertieft werden. Merke Das Interpreter-Entwurfsmuster nimmt auch außerhalb von JavaScript eine Sonderstellung ein, da es in der Praxis extrem selten verwendet wird.
8.4.11 Anfragen nach Zuständigkeit bearbeiten (Chain of Responsibility)
Die Idee beim Entwurfsmuster Chain of Responsibility (auch Zuständigkeitskette genannt) ist es, Anfragen innerhalb einer Anwendung durch mehrere hintereinander verkettete HandlerObjekte zu verarbeiten. Beschreibung des Entwurfsmusters
Das Anfrageobjekt wird so lange in dieser Handler-Kette von Handler zu Handler weitergereicht, bis ein Handler erreicht wurde, der die Anfrage bearbeiten kann, oder bis das Ende der HandlerKette erreicht wurde. Das Entwurfsmuster Chain of Responsibility enthält die in Abbildung 8.25 dargestellten Komponenten: Handler stellt das Interface zur Bearbeitung von Anfragen dar und wird von einer oder mehreren Klassen implementiert (im Diagramm ConcreteHandler1 und ConcreteHandler2). Jeder Handler enthält zudem (optional) eine Referenz auf ein weiteres HandlerObjekt, an das es die Anfrage weiterleitet, sollte es diese nicht selbst bearbeiten können.
Abbildung 8.25 Klassendiagramm für das Chain-of-Responsibility-Entwurfsmuster
Hinweis Klingt bekannt? Das Prinzip der Prototypkette in JavaScript ist letztendlich nichts anderes als eine Zuständigkeitskette: Beim Aufruf einer Methode an einem Objekt wird erst ermittelt, ob dieses Objekt die Methode enthält, und – falls nicht – eine entsprechende Anfrage an den Prototyp des Objekts weitergegeben.
Relevanz in JavaScript
Das Entwurfsmuster Chain of Responsibility können Sie durchaus auch in JavaScript-Anwendungen verwenden. Listing 8.55 zeigt ein einfaches Beispiel. Dargestellt ist hier das Szenario einer Suche nach einer bestimmten Schallplatte, bei der von Plattenhändler zu Plattenhändler gegangen wird. Der Einfachheit halber wird die Schallplatte nur anhand ihres Namens identifiziert. Die Pseudoklasse RecordStore bzw. ihre einzelne Objektinstanzen stellen die einzelnen Handler in der Zuständigkeitskette dar. Sie referenzieren jeweils über die Eigenschaft next den nächsten Handler (im Beispiel sogar tatsächlich ein »Händler«, nämlich ein Plattenhändler). Innerhalb der Methode requestAlbum() entscheidet jeder Handler, ob er die Anfrage selbst bearbeiten kann, sprich die entsprechende Schallplatte zum Verkauf anbietet, oder ob der Interessent zum nächsten Plattenladen weitergeschickt werden soll. class RecordStore {
constructor(name) {
this.name = name;
this.albums = [];
this.next = null;
}
setNext(recordStore) {
this.next = recordStore;
}
requestAlbum(albumTitle) {
if (this.albums.indexOf(albumTitle) >= 0) {
console.log(`${this.name}: Album habe ich hier`);
} else {
console.log(`${this.name}: Album habe ich nicht hier`);
if (this.next) {
this.next.requestAlbum(albumTitle);
}
}
}
}
// Aufbau der Zuständigkeitskette
const recordStore1 = new RecordStore('Johnnys Vinyl Kiste');
const recordStore2 = new RecordStore('Vinyl for all');
const recordStore3 = new RecordStore('Schallplatten Tauschbörse');
const recordStore4 = new RecordStore('Olaf der Plattensammler');
const recordStore5 = new RecordStore('LP und CD Ankauf');
recordStore1.setNext(recordStore2);
recordStore2.setNext(recordStore3);
recordStore3.setNext(recordStore4);
recordStore4.setNext(recordStore5);
recordStore5.albums.push('Blues for the red sun');
recordStore1.requestAlbum('Blues for the red sun');
Listing 8.55 Anwendung des Chain-of-Responsibility-Entwurfsmusters in JavaScript [ES2015] const RecordStore = function(name) {
this.name = name;
this.albums = [];
this.next = null;
this.setNext = function(recordStore) {
this.next = recordStore;
};
this.requestAlbum = function(albumTitle) {
if (this.albums.indexOf(albumTitle) >= 0) {
console.log(this.name + ': Album habe ich hier');
} else {
console.log(this.name + ': Album habe ich nicht hier');
if (this.next) {
this.next.requestAlbum(albumTitle);
}
}
};
}
// Aufbau der Zuständigkeitskette
const recordStore1 = new RecordStore('Johnnys Vinyl Kiste');
const recordStore2 = new RecordStore('Vinyl for all');
const recordStore3 = new RecordStore('Schallplatten Tauschbörse');
const recordStore4 = new RecordStore('Olaf der Plattensammler');
const recordStore5 = new RecordStore('LP und CD Ankauf');
recordStore1.setNext(recordStore2);
recordStore2.setNext(recordStore3);
recordStore3.setNext(recordStore4);
recordStore4.setNext(recordStore5);
recordStore5.albums.push('Blues for the red sun');
recordStore1.requestAlbum('Blues for the red sun');
Listing 8.56 Anwendung des Chain-of-Responsibility-Entwurfsmusters in JavaScript [ES5]
In den Beispielprogrammen wird eine Zuständigkeitskette von fünf Plattenläden aufgebaut, wobei nur der letzte die gesuchte Schallplatte anbietet. Die Ausgabe der Programme lautet daher wie folgt: Johnnys Vinyl Kiste: Album habe ich nicht hier
Vinyl for all: Album habe ich nicht hier
Schallplatten Tauschbörse: Album habe ich nicht hier
Olaf der Plattensammler: Album habe ich nicht hier
LP und CD Ankauf: Album habe ich hier
Listing 8.57 Ausgabe der Beispielprogramme
Hinweis Ein bekanntes Beispiel in der Webentwicklung, bei der das Konzept einer Chain of Responsibility verwendet wird, ist das sogenannte Event-Bubbling. Hierbei wird ein Event, das auf einem DOM-Element im Browser ausgelöst wird, vom jeweiligen Element zum Elternelement weitergegeben. Eine Variante dieses Entwurfsmusters findet sich in der sogenannten Middleware des Express.js-Frameworks (http://expressjs.com/de/guide/using-middleware.html). Hierbei können eingehende HTTP-Anfragen über eine Kette von Middleware-Funktionen abgearbeitet werden.
Merke Das Entwurfsmuster Chain of Responsibility kann je nach Anwendungsgebiet auch in JavaScript sinnvoll sein. Bekanntestes Beispiel ist das Event-Bubbling.
8.5 Zusammenfassung und Ausblick In diesem Kapitel haben Sie gesehen, welche Relevanz die einzelnen GoF-Entwurfsmuster in der JavaScript-Entwicklung haben. Sie sollten nun eine Vorstellung davon haben, welche der Entwurfsmuster in welcher Form für die JavaScript-Entwicklung eine Rolle spielen. Wenn Sie nach dem Stellenwert von GoF-Entwurfsmustern in JavaScript googeln, werden Sie feststellen, dass sich die Ergebnisse zwischen zwei Standpunkten bewegen: Auf der einen Seite finden Sie viele Seiten und Beispiele, die die Entwurfsmuster in Kombination mit pseudoklassischer Objektorientierung umsetzen und die funktionalen Aspekte der Sprache nicht einbeziehen. Auf der anderen Seite finden Sie Aussagen wie: »In JavaScript braucht man überhaupt keine GoF-Entwurfsmuster.« Leider bleiben die Erklärungen hierbei meistens nebulös und theoretisch. Ich halte daher beide Sichtweisen für zu engstirnig: Die rein objektorientierte Herangehensweise macht sich nicht die funktionalen Stärken von JavaScript zunutze, die funktionale Herangehensweise umgekehrt nicht die objektorientierten Stärken von JavaScript. JavaScript kann aber beides! Sie sollten also jeweils selbst entscheiden, welche Herangehensweise Sie wählen. Tabelle 8.1 gibt Ihnen zusammenfassend einen kurzen Überblick über die GoF-Entwurfsmuster und ihre jeweilige Relevanz in JavaScript. Sofern in der Literatur eine entsprechende deutsche Übersetzung zu finden ist, ist diese in der Tabelle mit aufgeführt. Entwurfsmuster Erzeugungsmuster
Beschreibung
Relevanz in JavaScript
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Factory Method (Fabrikmethode)
Erzeugt Objekte an zentraler Stelle.
Abstract Factory (abstrakte Fabrik)
Erzeugt Objekte einer ganzen Produktfamilie an zentraler Stelle.
Ist auch in JavaScript sinnvoll, insbesondere um den Aufruf von Konstruktorfunktionen an zentraler Stelle zu kapseln.
Builder (Erbauer)
Vereinfacht komplexe Prozesse beim Erzeugen von Objekten.
Ist auch in JavaScript sinnvoll, um komplexe Erzeugungsprozesse zu vereinfachen, insbesondere um DOM-Operationen zu kapseln.
Prototype (Prototyp)
Erzeugt neue Objekte auf Basis eines Prototyps.
in seiner klassischen Form irrelevant, da JavaScript auf Prototypen basiert
Singleton
Stellt sicher, dass es zu einer Klasse genau eine Objektinstanz gibt.
In JavaScript ist jedes Objekt implizit ein Singleton. Ein Zugriff auf die SingletonInstanz per Methode kann über das ModuleEntwurfsmuster nachgebildet werden.
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Adapter
Passt die Schnittstelle einer Klasse an eine andere Schnittstelle an.
Ist auch in JavaScript sinnvoll, insbesondere wenn Sie eine Bibliothek gegen eine andere Bibliothek austauschen möchten.
Bridge (Brücke)
Trennt die Abstraktion von der Implementierung.
Ist in JavaScript kaum sinnvoll, da es hier keine Unterscheidung zwischen Abstraktion und Implementierung gibt.
Composite (Kompositum)
Fügt Objekte zu Baumstrukturen zusammen und behandelt Teile sowie Container einheitlich.
Ist auch in JavaScript sinnvoll, wobei Sie die Unterscheidung zwischen einzelnen Teilen und ContainerKomponenten auch über Duck-Typing realisieren können.
Strukturmuster
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Decorator (Dekorierer)
Erweitert ein Objekt dynamisch um Verhalten.
Objekte können in JavaScript zwar dynamisch zur Laufzeit angepasst werden, so dass dieses Entwurfsmuster prinzipiell überflüssig erscheint. Dennoch ist es sinnvoll, um den Code sauberer zu halten.
Facade (Fassade)
Bietet eine einheitliche Schnittstelle für eine Menge von Schnittstellen eines Subsystems an.
Ist auch in JavaScript sinnvoll, insbesondere bei der Implementierung von Cross-BrowserFunktionalitäten.
Flyweight (Fliegengewicht)
Nutzt Objekte mit gleichen Eigenschaften gemeinsam, um große Mengen effizient verwenden zu können.
Ist auch in JavaScript sinnvoll, wenn Sie es mit einer Vielzahl von Objekten mit relativ vielen gleichen Eigenschaften zu tun haben.
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Proxy
Erlaubt es, Zugriffe auf ein Objekt abzufangen.
Ist in seiner klassischen Form seit ES2015 irrelevant, da Proxies nativ in JavaScript unterstützt werden. In ES5 können Proxies emuliert werden.
Chain of Responsibility (Zuständigkeitskette)
Ermöglicht es, Anfragen durch hintereinander verkettete Handler zu bearbeiten.
Auch in JavaScript sinnvoll, wenn eine Anfrage von verschiedenen Handlern bearbeitet werden kann.
Command (Befehl)
Kapselt einen Befehl in einem Objekt.
Ist aufgrund von funktionalen Aspekten in JavaScript in seiner klassischen Form nicht relevant.
Interpreter
Definiert eine Repräsentation für die Grammatik einer Sprache.
In der Praxis kommt dieses Entwurfsmuster sehr selten zum Einsatz, in JavaScript so gut wie gar nicht.
Verhaltensmuster
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Iterator
Ermöglicht den sequenziellen Zugriff auf eine Datenstruktur.
Ist in seiner klassischen Form irrelevant, da seit ES6 Iteratoren nativ in JavaScript unterstützt werden. In ES5 können Iteratoren relativ einfach implementiert werden.
Mediator (Vermittler)
Koordiniert das Zusammenspiel mehrerer Objekte.
Ist auch in JavaScript sinnvoll, wobei es meistens in Form von Publish-Subscribe zum Einsatz kommt.
Memento
Speichert den Zustand eines Objekts, so dass das Objekt später in diesen Zustand zurückversetzt werden kann.
Ist auch in JavaScript sinnvoll und durch das JSON-Format relativ einfach umzusetzen.
Observer (Beobachter)
Ermöglicht es, Objekte bei Zustandsänderungen anderer Objekte zu benachrichtigen.
Ist ebenfalls in JavaScript sinnvoll. Alternativ bietet sich das Konzept der ereignisgesteuerten Programmierung an.
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
State (Zustand)
Ermöglicht es, das Verhalten eines Objekts abhängig vom Zustand des Objekts anzupassen.
Ist auch in JavaScript sinnvoll, wenn ein Objekt abhängig vom Zustand sein Verhalten anpassen soll und dies nicht über if-elseAnweisungen implementiert werden soll.
Strategy (Strategie)
Kapselt einen gesamten Algorithmus in einem Objekt, so dass er zur Laufzeit ausgetauscht werden kann.
Ist aufgrund von funktionalen Aspekten in JavaScript in seiner klassischen Form nicht relevant.
Template Method Definiert die Vorlage (Schablonenmethode) eines Algorithmus, wobei die einzelnen Schritte durch Unterklassen definiert werden.
Ist prinzipiell auch in JavaScript sinnvoll, kann aber dank FirstClass-Funktionen auch über Komposition statt über Vererbung realisiert werden.
Entwurfsmuster
Beschreibung
Relevanz in JavaScript
Visitor (Besucher)
Kapselt eine auf den Elementen einer Objektstruktur auszuführende Operation in einem Objekt.
Ist prinzipiell auch in JavaScript sinnvoll, wobei Sie abwägen müssen, ob Sie sich nicht die dynamischen Eigenschaften von JavaScript zunutze machen und die entsprechenden Operationen am Prototyp oder über kopierende Vererbung an der entsprechenden Objektinstanz hinzufügen.
Tabelle 8.1 Die Bedeutung der GoF-Entwurfsmuster in JavaScript
9 Architekturmuster und Konzepte moderner JavaScript-Webframeworks JavaScript-Webframeworks gibt es mittlerweile wie Sand am Meer. Bezüglich der Konzepte und Architekturmuster ähneln sich die einzelnen Frameworks jedoch häufig. Nachdem der Fokus im vorigen Kapitel auf den klassischen GoFEntwurfsmustern lag, stelle ich Ihnen in diesem Kapitel verschiedene Architekturmuster sowie einige grundlegende Konzepte moderner Webframeworks vor, so dass Sie anschließend – unabhängig davon, welches konkrete Framework Sie in einem Projekt einsetzen – einen guten Überblick haben. Zur Veranschaulichung zeige ich Ihnen dabei zwar auch konkrete Codebeispiele einiger der Frameworks. Hierbei liegt der Fokus aber nicht darauf, Ihnen das jeweilige Framework als Ganzes vorzustellen (das wäre in einem Kapitel gar nicht machbar), sondern Ihnen eine Vorstellung davon zu geben, wie das entsprechende Architekturmuster bzw. Konzept in der Praxis umgesetzt wird. Bei der Fülle an Frameworks, die es mittlerweile gibt, ist es meines Erachtens nämlich sinnvoller, sich vor der Auswahl eines Frameworks zunächst der Architektur und der verwendeten Konzepte bewusst zu sein. Als Architekturmuster möchte ich Ihnen im Folgenden zum einen Model View Controller, Model View Presenter und Model View ViewModel, sowie zum anderen später in diesem Kapitel die sogenannte komponentenbasierte Architektur vorstellen. Alle diese Muster dienen den gleichen Zielen wie loser Kopplung, Modularität,
Testbarkeit, Flexibilität und Wartbarkeit des Quelltextes, unterscheiden sich im Detail aber in der Umsetzung. Des Weiteren stelle ich Ihnen am Rande Konzepte wie Data-Binding, Templating und Routing vor.
9.1 Model View Controller Das Prinzip Model View Controller (MVC) gibt es schon bedeutend länger als JavaScript: Erstmals wurde dieses Architekturmuster 1979 für die Programmiersprache Smalltalk vorgestellt. Die Idee dabei ist, die Datenhaltung und Geschäftslogik einer Anwendung von der Präsentation der Daten zu entkoppeln. Die Komponente der Datenhaltung und Geschäftslogik wird dabei als Model, die Komponente der Präsentation als View bezeichnet. Die Entkopplung beider Komponenten, also von Model und View, geschieht über eine dritte Komponente: den Controller (siehe Abbildung 9.1).
Abbildung 9.1 Model View Controller
Die Controller-Komponente ist verantwortlich für die Anwendungslogik und koordiniert das Zusammenspiel zwischen Model und View: Der Nutzer interagiert mit der View-Komponente (oftmals eine grafische Oberfläche, kurz GUI für Graphical User Interface), der Controller prüft die Nutzereingaben auf Gültigkeit, verarbeitet diese im Rahmen der Anwendungslogik und aktualisiert die Daten im Model. Der Vorteil dieser Herangehensweise ist, dass das Datenmodell an sich unabhängig von der Darstellung (bzw. der Präsentation) der Daten ist. Dadurch erreicht man, dass relativ einfach verschiedene Ansichten bzw. Views für die gleichen Daten implementiert werden können, ohne dass das Datenmodell angepasst werden muss. Hinzu kommt, dass das Datenmodell viel einfacher isoliert getestet werden kann.
Neben den Kommunikationswegen zwischen Controller und View sowie Controller und Model gibt es bei MVC einen weiteren Kommunikationsweg zwischen der View-Komponente und dem Model, der dafür sorgt, dass die View auf Änderungen am Datenmodell reagiert und die Darstellung der Daten entsprechend anpasst. Mit anderen Worten: Die View-Komponente ist bei MVC abhängig von der Model-Komponente.
9.2 Model View Presenter Model View Presenter (MVP) basiert auf dem MVC-Entwurfsmuster, geht aber in der Entkopplung von View und Model noch einen Schritt weiter: Die View hat keinen Zugriff auf das Model wie bei MVC, sondern nun sind beide Komponenten komplett voneinander entkoppelt. Abbildung 9.2 stellt diesen Zusammenhang grafisch dar. Die Kommunikation erfolgt ausschließlich über die dritte Komponente: den Presenter. Dieser nimmt im Wesentlichen die Aufgaben der Controller-Komponente aus MVC wahr, sorgt aber zusätzlich dafür, dass die View bei Änderungen am Datenmodell entsprechend aktualisiert wird. Die Presenter-Komponente dient somit als direktes (und einziges) Bindeglied zwischen View und Model.
Abbildung 9.2 Model View Presenter
So weit die Grundlagen zu MVC und MVP. Bevor ich Ihnen in Abschnitt 9.4 mit MVVM ein weiteres verwandtes Architekturmuster vorstellen werde, lassen Sie mich Ihnen im nächsten Schritt zunächst zeigen, welche Bedeutung MVC und MVP für Webanwendungen haben.
9.3 MVC und MVP in Webanwendungen Betrachtet man MVC/MVP für Webanwendungen, muss man zwischen klassischen und modernen Webanwendungen unterscheiden. In einer klassischen Webanwendung geschieht der Großteil der Anwendungslogik auf Serverseite. Nur wenig, wenn überhaupt etwas ist auf Clientseite implementiert (daher bezeichnet man in diesem Fall den Client auch als Thin Client). Bei modernen Webanwendungen, wie beispielsweise Single-Page Applications, ist dagegen vieles, wenn nicht das meiste der Anwendungslogik auf Clientseite implementiert. Analog zu einem Thin Client spricht man in diesem Fall von einem Thick Client. 9.3.1 Klassische Webanwendungen
Bei klassischen Webanwendungen sind MVC/MVP meist so umgesetzt wie in Abbildung 9.3. Sie sehen sofort: Die drei wesentlichen Komponenten, die diese Muster ausmachen, befinden sich auf Serverseite. Doch schauen wir Schritt für Schritt, wie der Ablauf in einer solchen Anwendung aussieht.
Abbildung 9.3 Model View Controller in klassischen Webanwendungen
Zunächst stellt der Client über den Browser eine HTTP-Anfrage (HTTP-Request), die serverseitig von einer (mehr oder weniger komplexen) Routing-Engine verarbeitet wird. Die Aufgabe einer Routing-Engine ist es im Wesentlichen, anhand der HTTP-Anfrage den Controller auf Serverseite auszuwählen, der die Anfrage bearbeiten soll. Bei einer klassischen Webanwendung greift die Routing-Engine dabei auf Informationen wie die URL, RequestHeader, Cookie-Werte und Request-Parameter aus der HTTP-Anfrage zurück. Zu den Aufgaben eines Controllers zählen beispielsweise die Validierung der empfangenen Daten, das Aktualisieren des Models und das Erstellen der HTTP-Antwort, die an den Client zurückgesendet wird. In der Regel kommt dabei eine sogenannte Template-Engine zum Einsatz, die auf Basis eines entsprechenden View-Templates und der Daten aus dem Model das HTML generiert, das als Teil der HTTP-Antwort (HTTP-Response) an den Client zurückgesendet wird. Das Model wird dabei durch entsprechende Objektinstanzen und/oder Datensätze einer Datenbank repräsentiert. Oft nutzt man in diesem Zusammenhang sogenannte ORM-Frameworks (ORM für Object Relational Mapping), die es ermöglichen, Objektinstanzen automatisch in relationalen Datenbanken zu speichern sowie daraus zu lesen. Die View-Komponente wird in der Regel über HTML-Templates repräsentiert. Bis auf die HTTP-Antwort, die die View an den Client sendet, spielt sich also bei klassischen Webanwendungen die gesamte Kommunikation zwischen den Komponenten auf der Serverseite ab. Bei modernen Webanwendungen, bzw. genauer bei Single-Page Applications, ist dies nicht der Fall.
9.3.2 Moderne Webanwendungen
Der Begriff moderne Webanwendungen ist zunächst etwas abstrakt und bedarf einer kurzen Erklärung. An dieser Stelle meine ich damit solche Anwendungen, bei denen sich der Großteil auf Clientseite abspielt und somit ein großer Teil der Anwendungslogik in JavaScript implementiert ist. In den meisten Fällen sind solche Anwendungen als Single-Page Applications implementiert, also als Anwendungen, die aus einer Basiswebseite bestehen, die dynamisch durch JavaScript geändert wird. Historischer Rückblick Der Startschuss für moderne Webanwendungen fiel mit dem XMLHttpRequest-Objekt, das Microsoft erstmals im Internet Explorer 5 zur Verfügung stellte. Dieses ermöglichte es, programmatisch (über JavaScript) HTTP-Anfragen an einen Server zu stellen und zu verarbeiten, ohne die komplette Webseite neu zu laden. Das war vorher in JavaScript nicht möglich. Zunächst wurde das XMLHttpRequest-Objekt hauptsächlich dazu eingesetzt, vorgefertigte HTML-Schnipsel (sozusagen einzelne Teile der View) je nach Bedarf vom Server zu laden und dynamisch in das DOM einzubauen. Später wurde mehr und mehr der Fokus darauf gelegt, statt fertiger View-Teile die Daten selbst (sprich das Model) vom Server zu holen und das Aufbauen der View komplett auf Clientseite durchzuführen. Als Austauschformat wurde dazu zunächst XML eingesetzt, das in Kombination mit asynchronem JavaScript den Begriff Ajax (Asynchronous JavaScript and XML) formte. Allerdings lässt sich XML vergleichsweise eher schlecht mit JavaScript verarbeiten, so
dass in heutigen Webanwendungen vor allem JSON als Austauschformat verwendet wird.
Wenn der Großteil der Anwendungslogik auf Clientseite in JavaScript implementiert ist, macht es Sinn, Architekturmuster und Konzepte wie Routing und Templating auch auf Clientseite einzusetzen. Der prinzipielle Zusammenhang von MVC/MVP und diesen Konzepten ist wie in Abbildung 9.4 zu sehen also gleich dem auf Serverseite.
Abbildung 9.4 Model View Controller in modernen Webanwendungen
Trotzdem gibt es wichtige Unterschiede: So handelt es sich bei den Anfragen des Browsers an die Routing-Engine nicht mehr um HTTPAnfragen. In Abschnitt 9.5, »Komponentenbasierte Architektur«, werde ich das Thema Routing noch einmal aufgreifen. Dort werde ich Ihnen zeigen, wie Routing in JavaScript und auf Clientseite umgesetzt wird. Hinweis
Die meisten JavaScript-Frameworks für Single-Page Applications verwenden das MVC-Architekturmuster oder Varianten davon wie MVP oder MVVM, das ich Ihnen in Abschnitt 9.4 vorstellen werde. Da die Implementierungen der Controller-Komponente dabei teilweise sehr individuell sind, nicht strikt eines der Muster umsetzen und zudem sehr unterschiedliche Herangehensweisen verwenden, fasst man die Architekturmuster auch unter dem Begriff MV* zusammen.
MVC am Beispiel von Backbone.js
Backbone.js (http://backbonejs.org) ist eine der ersten JavaScriptBibliotheken, die das MVC-Entwurfsmuster auf die Clientseite brachten. Um Backbone.js zu verwenden, laden Sie es entweder von der Webseite herunter oder installieren es per NPM über den Befehl npm install backbone und binden anschließend in die entsprechende HTML-Datei ein (Listing 9.1). Da Backbone.js auf der Bibliothek Underscore.js (http://underscorejs.org) aufsetzt, benötigen Sie diese ebenfalls und für das folgende Beispiel außerdem die Bibliothek jQuery (http://jquery.com). Beide lassen sich wie Backbone.js über NPM installieren oder über die entsprechende Webseite herunterladen.
Backbone.js Beispiel
Listing 9.1 Einbinden von Backbone.js und weiteren Bibliotheken
Die HTML-Datei aus Listing 9.1 bildet lediglich das Grundgerüst für die Backbone.js-Anwendung. Der Hauptteil der Backbone.jsAnwendung ist im Beispiel in der Datei app.js zu finden. Für die Erstellung des Objektmodells und für die pseudoklassische Vererbung bietet Backbone.js eigene Objekte an. Dies sind zum einen das Objekt Backbone.Model für normale Objekte (bzw. »Klassen«) und Backbone.Collection für Listen. Listing 9.2 zeigt dazu ein einfaches Beispiel: Album leitet von der »Basisklasse« Backbone.Model ab und erweitert sie um zwei Eigenschaften, AlbumList leitet von der »Basisklasse« Backbone.Collection ab, wobei Elemente in dieser Liste vom Typ Album sein müssen. const Album = Backbone.Model.extend({
defaults: {
title: '',
artist: ''
}
});
const AlbumList = Backbone.Collection.extend({
model: Album
});
Listing 9.2 Model einer Backbone.js-Anwendung
View-Komponente und Controller-Komponente sind in Backbone.js miteinander verwoben. Ausgangspunkt bildet das Objekt
Backbone.View. Um eine eigene View zu erstellen, leiten Sie, wie
in Listing 9.3 zu sehen, von diesem Objekt ab und erzeugen eine Instanz, wobei Sie das entsprechende Model als Parameter übergeben. Im Beispiel ist dies eine Liste (AlbumList) von Alben. Innerhalb des View-Objekts bezeichnet die Eigenschaft el das Element im DOM, das als Container für die entsprechende View dienen soll (im Beispiel das body-Element).
Das Aussehen der View wird über die Methode render(). Diese müssen Sie überschreiben, damit überhaupt irgendeine Ausgabe generiert wird. Über this.el haben Sie dabei Zugriff auf das eingangs definierte Element. Im Beispiel fügen wir diesem Element eine HTML-Liste hinzu, die nach und nach um Listeneinträge für die im Model enthaltenen Alben erweitert wird (appendItem()). Hinweis Anstatt wie im Beispiel das HTML innerhalb der View-Komponente manuell über JavaScript-Methoden am DOM zu erzeugen, ist es für den Praxiseinsatz empfehlenswert, ein Templating-Framework wie Handlebars.js (http://handlebarsjs.com), EJS (http://ejs.co/) oder Mustache.js (http://mustache.github.io/) zu verwenden.
Hinweis zu jQuery und Underscore Listing 9.3 macht Gebrauch von Funktionen aus den Bibliotheken jQuery und Underscore.js. Erstere wird standardmäßig über die Variable $ angesprochen, letztere über die Variable _. jQuery bietet vor allem Funktionen an, die den Zugriff auf das DOM vereinfachen, Underscore.js dagegen vor allem Funktionen, die das funktionale Programmieren erleichtern.
In Backbone.js gibt es keine explizite Controller-Komponente. Stattdessen wird die entsprechende Logik zum Steuern der View und zum Aktualisieren des Models innerhalb der View-Komponente definiert. Dazu steht die Eigenschaft events zur Verfügung, über die einzelnen HTML-Elementen für bestimmte Events verschiedene Event-Handler hinzugefügt werden können. Als Events stehen z.B. das Klicken, Doppelklicken und das Ändern von Feldwerten zur Verfügung. Über die Angabe eines zusätzlichen Selektors können Sie steuern, für welche Elemente der EventHandler registriert werden soll. Im Beispiel werden auf diese Weise die beiden Event-Handler handleChangedTitle und handleChangedArtist für das change-Event für alle Elemente mit den CSS-Klassen title und artist registriert. const AlbumView = Backbone.View.extend({
el: $('body'),
initialize: function(){
this.render();
},
render: function(){
const self = this;
$(this.el).append('
');
_(this.collection.models).each(function(item) {
self.appendItem(item);
}, this);
},
appendItem: function(item){
const list = $('ul', this.el);
const listItem = list.append(
`
`
);
},
events: {
'change .title' : 'handleChangedTitle',
'change .artist' : 'handleChangedArtist'
},
handleChangedTitle: function(t) {
alert('Changed title');
},
handleChangedArtist: function(t) {
alert('Changed artist');
}
});
const albumList = new AlbumList([
new Album({
title: 'Wretch',
artist: 'Kyuss'
}),
new Album({
title: 'Sky Valley',
artist: 'Kyuss'
}),
new Album({
title: 'The Will To Live',
artist: 'Ben Harper'
}),
new Album({
title: 'Fight for Your Mind',
artist: 'Ben Harper'
})
]);
const listView = new AlbumView({
collection: albumList
});
Listing 9.3 View einer Backbone.js-Anwendung
Das Ergebnis des obigen Quelltextes sehen Sie in Abbildung 9.5.
Abbildung 9.5 Eine einfache Backbone.js-Anwendung
9.4 Model View ViewModel Model View ViewModel (MVVM) ist eine Variante der MVC- und MVPArchitekturmusters und wurde ursprünglich 2005 für auf Microsoft Silverlight und Windows Presentation Foundation (WPF) basierenden Anwendungen entwickelt. Prinzipiell kann das Muster aber auch auf andere UI-Technologien wie HTML5 oder JavaFX 8 angewandt werden. Wie beim klassischen MVC und MVP gibt es bei MVVM sowohl eine View-Komponente als auch eine Model-Komponente, wobei hier das Ziel ist, beide Komponenten voneinander zu entkoppeln. Diese Aufgabe übernimmt das sogenannte ViewModel, das in gewisser Weise als Sonderform eines Controllers angesehen werden kann, oft aber auch als »Abstraktion der View« bezeichnet wird. Die Idee dabei ist, im ViewModel für jedes dynamische UI-Element entsprechende Datenfelder bereitzustellen, die über bidirektionales Data-Binding an die View gebunden werden (siehe Abbildung 9.6). Ändert der Nutzer den Wert eines UI-Elements (beispielsweise den Text innerhalb eines Textfeldes), wird automatisch das entsprechende Datenfeld im ViewModel geändert. Umgekehrt passt sich der Wert des UI-Elements an, wenn (programmatisch) der Wert des Datenfeldes im ViewModel geändert wird. Zusätzlich können UI-Elemente der View (beispielsweise Schaltflächen) Methoden im ViewModel aufrufen (unidirektionales Command-Binding). Hinweis Bidirektionales Binding ist allerdings mit Vorsicht zu genießen, weil bei vielen Bindings die Performance negativ beeinflusst werden
kann. Alle großen Frameworks haben es daher zugunsten eines einseitig gerichteten Datenflusses ausgetauscht.
Abbildung 9.6 Model View ViewModel
Da jedes UI-Element aus der View sein Äquivalent im ViewModel hat, ist es möglich, jegliche UI-relevante Logik nicht in der ViewKomponente, sondern im ViewModel – oder noch besser: in separaten Services – zu implementieren. Der Vorteil: Diese Logik können Sie unabhängig von der View testen. Anstatt spezielle, teils aufwendige Unit-Tests zu schreiben, die bestimmte UI-Aspekte testen (beispielsweise, wie in Abschnitt 6.5.2 gesehen, anhand von CasperJS), können normale Unit-Tests direkt die UI-Logik im ViewModel testen. Doch eine View ohne Logik hat einen weiteren Vorteil: Derjenige, der die View entwickelt, muss über keine JavaScript-Kenntnisse verfügen und sich nicht um UI-Logik kümmern. So kann ein UIDesigner die View-Komponente umsetzen, während ein JavaScriptEntwickler unabhängig davon die UI-Logik im ViewModel implementiert. Insbesondere in größeren Projekten kann dies den Workflow extrem beschleunigen.
9.4.1 MVVM am Beispiel von Knockout.js
Eines der JavaScript-Frameworks, das das MVVM-Architekturmuster umsetzt, ist Knockout.js (http://knockoutjs.com). Sie können die Bibliothek entweder direkt von der Webseite herunterladen oder über NPM mit dem Befehl npm install knockout lokal in Ihrem jeweiligen Projekt installieren. Um Knockout.js verwenden zu können, müssen Sie die JavaScript-Bibliotheksdatei lediglich in das entsprechende HTML-Dokument einbinden. Anschließend steht die Bibliothek über das globale Objekt ko zur Verfügung. Hinweis Knockout.js ist mittlerweile etwas in die Jahre gekommen und ist unter heutigen Gesichtspunkten wahrscheinlich nicht das Framework der Wahl, wenn man ein neues Projekt aufsetzt. Trotzdem ist es ein gutes Beispiel für die Anwendung des MVVMEntwurfsmusters, so dass ich es aus didaktischen Gründen weiterhin als Beispiel heranziehe.
Das Model einer Knockout.js-Anwendung wird prinzipiell über normale JavaScript-Objekte repräsentiert. Anstatt jedoch normale Datentypen wie Strings innerhalb der Model-Objekte zu verwenden, kommen spezielle Objekte zum Einsatz, die über den Aufruf der Methoden ko.observable() und ko.observableArray() erzeugt werden. Diese speziellen Objekte sind »observable«, sprich, Änderungen am unterliegenden Wert werden von Knockout.js registriert. Listing 9.4 zeigt ein Beispiel für ein einfaches Model. const Album = function (title, artist) {
this.title = ko.observable(title);
this.artist = ko.observable(artist);
};
Listing 9.4 Model einer Knockout.js-Anwendung
Die View einer Knockout.js-Anwendung wird, wie in Listing 9.6 zu sehen, über HTML-Dokumente repräsentiert, das ViewModel wie in Listing 9.5 als JavaScript-Objekt. Das ViewModel hat dabei direkten Zugriff auf das Model und wird über den Aufruf von ko.applyBindings() an die View gebunden. function AlbumViewModel() {
// Daten würden normalerweise vom Server abgefragt
this.availableArtists = [
{ name: 'Kyuss' },
{ name: 'Ben Harper' }
];
// Initiale Daten
this.albums = ko.observableArray([
new Album('Wretch', this.availableArtists[0]),
new Album('Sky Valley', this.availableArtists[0]),
new Album('The Will To Live', this.availableArtists[1]),
new Album('Fight for Your Mind', this.availableArtists[1])
]);
this.addAlbum = () => {
this.albums.push(new Album('', this.availableArtists[0]));
};
this.removeAlbum = album => {
this.albums.remove(album);
};
}
ko.applyBindings(new AlbumViewModel());
Listing 9.5 ViewModel einer Knockout.js-Anwendung
Die konkreten Bindings werden dabei innerhalb des HTMLDokuments über das HTML-Attribut data-bind definiert. Der Inhalt dieses Attributs wird von Knockout.js interpretiert und kann für verschiedene Arten von Bindings verwendet werden. Listing 9.6 nutzt hierzu direkt mehrere Beispiele. Der Kennzeichner text sorgt beispielsweise dafür, dass die referenzierten Daten aus dem Model als simpler Text ausgegeben werden (im Listing beispielsweise die Anzahl der Alben über albums().length).
Der Kennzeichner value bewirkt, dass der Wert eines DOMElements bidirektional an eine Eigenschaft innerhalb des Models gebunden wird. Im Beispiel wird das Eingabefeld für den Titel eines Albums an die entsprechende Objekteigenschaft gebunden (value: title). Über click wiederum lässt sich ein Command-Binding erstellen, das dafür sorgt, dass bei Klick auf das entsprechende DOM-Element eine Methode im Model aufgerufen wird. Im Beispiel kommt diese Art von Binding zum Einsatz, um Alben zu löschen (click: $root.removeAlbum) bzw. neue Alben anzulegen (click: $root.addAlbum). Zudem lässt sich über spezielle Kennzeichner der Kontrollfluss der View beeinflussen. Der Kennzeichner foreach beispielsweise ermöglicht die Iteration über ein Array im Datenmodell. Im Beispiel wird dies genutzt, um über alle Alben zu iterieren und die einzelnen Zellen der Tabelle zu generieren. Das Ergebnis der Anwendung sehen Sie in Abbildung 9.7.
Knockout Beispiel
Albums (
)
Add another album
Listing 9.6 View einer Knockout.js-Anwendung
Abbildung 9.7 Eine einfache Knockout.js-Anwendung
Hinweis Das im vorigen Abschnitt kurz vorgestellte Backbone.js verfügt standardmäßig über kein Data-Binding. Allerdings gibt es verschiedene Data-Binding-Bibliotheken wie Backbone.stickit (http://nytimes.github.io/backbone.stickit), Rivets.js (http://rivetsjs.
com) und Epoxy.js (http://github.com/gmac/backbone.epoxy), die das Konzept auch in Backbone.js-Anwendungen ermöglichen.
9.4.2 Kombination von MVC und MVVM am Beispiel von AngularJS
Ein Framework, das das MVC-Architekturmuster mit dem MVVMArchitekturmuster kombiniert, ist die erste Version von AngularJS (https://angularjs.org). Um das Framework zu verwenden, benötigen Sie die entsprechende Bibliotheksdatei, die Sie entweder von der Projektwebseite herunterladen oder über NPM (npm install angularjs) installieren. Hinweis Wie schon bei Knockout.js gilt auch für die erste Version von Angular: Für neue Projekte fällt die Wahl sehr wahrscheinlich auf ein anderes Framework bzw. auf die zweite Version von Angular, das sich von der Architektur her grundsätzlich unterscheidet und später in diesem Kapitel kurz vorgestellt wird.
Wie in Knockout.js bildet auch in AngularJS ein HTML-Dokument die View-Komponente (siehe Listing 9.7). Innerhalb dieses Dokuments definieren Sie die AngularJS-Anwendung über das Attribut ng-app. Der Teil unterhalb eines Elements mit diesem Attribut bildet den Inhalt der Anwendung. Prinzipiell lassen sich innerhalb eines HTML-Dokuments mehrere Elemente mit dem Attribut ng-app auszeichnen und auf diese Weise mehrere unabhängige AngularJS-Anwendungen innerhalb eines HTMLDokuments betreiben. Im Folgenden soll aber der Standardfall, die Verwendung einer einzelnen Anwendung, betrachtet werden.
AngularJS Beispiel
Print Albums
Listing 9.7 View einer AngularJS-Anwendung
Die Verknüpfung mit einer Controller-Komponente geschieht über das Attribut ng-controller (zum genauen Aufbau eines Controllers in wenigen Momenten mehr). Wie auch Knockout.js stellt AngularJS zudem spezielle HTML-Attribute bereit, über die sich der Kontrollfluss steuern lässt (im Beispiel ng-repeat) und über die sich Command-Bindings definieren lassen (im Beispiel ngclick). Das ViewModel wird in AngularJS mit Hilfe sogenannter Scopes realisiert. Innerhalb von Controllern (die übrigens wie in Listing 9.8 über den Aufruf von controller() definiert werden) steht der entsprechende Scope über die Variable $scope zur Verfügung. Der
Controller kann dann auf diesem Objekt Variablen und Funktionen initialisieren, die anschließend innerhalb der View unterhalb des mit dem Controller ausgezeichneten HTML-Elements zur Verfügung stehen. Das Model dagegen wird im einfachsten Fall über einfache JavaScript-Objekte repräsentiert. Im Beispiel sind diese hart codiert im Controller, in der Praxis wird das Objektmodell jedoch in der Regel über sogenannte Services bereitgestellt. const app = angular.module('albums', []);
// Controller
app.controller('AlbumController', ['$scope', function($scope) {
// Model
const albums = [{
title: 'Wretch',
artist: 'Kyuss'
},
{
title: 'Sky Valley',
artist: 'Kyuss'
},
{
title: 'The Will To Live',
artist: 'Ben Harper'
},
{
title: 'Fight for Your Mind',
artist: 'Ben Harper'
}];
// ViewModel
$scope.albums = albums;
$scope.printAlbums = function() {
$scope.albums.forEach(function(album) {
console.log(album);
});
}
}]);
Listing 9.8 View/Controller und ViewModel einer AngularJS-Anwendung
Abbildung 9.8 zeigt einen Screenshot der Beispielanwendung.
Abbildung 9.8 Eine einfache AngularJS-Anwendung
Hinweis Eine gelungene Übersicht darüber, wie die MV*-Architekturmuster in verschiedenen JavaScript-Frameworks angewandt werden bzw. umgesetzt sind, bietet die Website TodoMVC (http://todomvc.com). Die immer gleiche Beispielanwendung (eine Minianwendung zur Verwaltung von Aufgaben) ist dort in Frameworks wie AngularJS, Ember.js, Backbone.js, KnockoutJS, DOJO und vielen mehr realisiert. Den Quellcode dazu finden Sie unter https://github.com/tastejs/todomvc/tree/gh-pages/examples.
9.5 Komponentenbasierte Architektur Die Idee bei der komponentenbasierten Architektur (ComponentBased Architecture, kurz CBA) ist es, eine (Web-)Anwendung in einzelne Komponenten zu unterteilen. Anders als bei MVC und Konsorten (MV*) erfolgt keine Trennung der gesamten Anwendung in Model, View und Controller. Während bei MV* eine Anwendung horizontal unterteilt, wird die Anwendung bei der komponentenbasierten Architektur vertikal unterteilt (Abbildung 9.9). Jede Komponente ist für sich eigenständig und enthält sowohl Anwendungscode (zum Beispiel JavaScript) als auch UI-Code (zum Beispiel HTML und CSS). Im besten Fall sind einzelne Komponenten dabei so aufgebaut, dass sie in verschiedenen Anwendungen wiederverwendet werden können (Abbildung 9.10). Diese Wiederverwendbarkeit ist gleichzeitig einer der wesentlichen Vorteile der komponentenbasierten Architektur gegenüber den MV*-Architekturen. Hinweis Innerhalb einzelner Komponenten können Sie natürlich wieder MV* verwenden.
Abbildung 9.9 Gegenüber MVC ist die komponentenbasierte Architektur horizontal.
Abbildung 9.10 Einzelne Komponenten lassen sich in verschiedenen Anwendungen wiederverwenden.
Im Folgenden möchte ich Ihnen einen kurzen Überblick über drei repräsentative Frameworks geben, die alle dem komponentenbasierten Gedanken folgen: Angular (seit Version 2), React sowie Vue.js. 9.5.1 Komponentenbasierte Architektur am Beispiel von Angular
Die erste Version von Angular (auch als AngularJS bezeichnet) haben Sie bereits zu Anfang dieses Kapitels kennengelernt und gesehen, dass hier das MVVM-Architekturmuster zum Einsatz kommt. Im Zuge des Versionssprungs von 1.4.x zu 2.0 erfolgte bei Angular eine grundlegende Änderung der Architektur hin zu einem komponentenbasierten Aufbau. Seit dieser Version hört das Framework zudem schlicht auf den Namen Angular (ohne das »JS«) und ist unter https://angular.io zu finden. Generierung einer Angular-Anwendung
Um ein neues Angular-Projekt anzulegen, empfiehlt sich der Einsatz der Angular-Kommandozeilentools (Angular CLI), die sich über npm install -g @angular/cli installieren lassen und anschließend über den Befehl ng zur Verfügung stehen. Über ng new example-angular beispielsweise lässt sich ein Angular-Projektgerüst generieren und
über ng serve --open im generierten Verzeichnis die jeweilige Anwendung starten (der Parameter --open sorgt dafür, dass die Anwendung direkt im Browser geöffnet wird, standardmäßig unter http://localhost:4200/). $ ng new example-angular
create example-angular/README.md (1030 bytes)
create example-angular/.angular-cli.json (1250 bytes)
create example-angular/.editorconfig (245 bytes)
create example-angular/.gitignore (529 bytes)
create example-angular/src/assets/.gitkeep (0 bytes)
create example-angular/src/environments/environment.prod.ts (51 bytes)
create example-angular/src/environments/environment.ts (387 bytes)
...
create example-angular/src/app/app.module.ts (316 bytes)
create example-angular/src/app/app.component.css (0 bytes)
create example-angular/src/app/app.component.html (1141 bytes)
create example-angular/src/app/app.component.spec.ts (986 bytes)
create example-angular/src/app/app.component.ts (207 bytes)
Installing packages for tooling via npm.
...
added 1596 packages in 157.365s
Installed packages for tooling via npm.
Directory is already under version control. Skipping initialization of git.
Project 'example-angular' successfully created.
Listing 9.9 Generierung einer Angular-Anwendung
Komponenten in Angular
Da Angular TypeScript als Programmiersprache bevorzugt, haben die generierten Codedateien die Endung *.ts und enthalten Sprachfeatures, die teilweise (noch) nicht im aktuellen Standard zu JavaScript enthalten sind. Ein Beispiel hierfür sind sogenannte Decorators bzw. Annotationen, über die sich einzelne Klassen markieren lassen (siehe auch das Decorator-Entwurfsmuster in Kapitel 8, »Die Entwurfsmuster der Gang of Four«). Die generierte Datei app.component.ts beispielsweise, die die Hauptkomponente der generierten Anwendung darstellt (und im folgenden Listing um einige Zeilen ergänzt wurde), enthält den Decorator @Component und wird damit als Komponente ausgezeichnet:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
users = [
{
firstName: 'Max',
lastName: 'Mustermann'
},
{
firstName: 'Moritz',
lastName: 'Mustermann'
},
{
firstName: 'Peter',
lastName: 'Mustermann'
},
{
firstName: 'Petra',
lastName: 'Mustermann'
}
];
}
Listing 9.10 Beispielkomponente einer Angular-Anwendung
Über die Eigenschaft selector wird dabei definiert, wie sich die Komponente im HTML einbinden lässt (im Beispiel wäre das ). Die Angabe der CSS-Dateien erfolgt über die Eigenschaft styleUrls, wobei Sie hier mehrere Dateipfade übergeben können (alternativ können Sie das CSS auch direkt definieren, indem Sie die Eigenschaft styles verwenden). Über die Eigenschaft templateUrl lässt sich zudem eine HTMLDatei angeben, die als Vorlage für die Komponente verwendet wird und den Template-Code enthält (alternativ könnten Sie den entsprechenden Code auch direkt über die Eigenschaft template angeben, was aber wenn überhaupt nur für kleinere Snippets sinnvoll ist).
Innerhalb des Template-Codes haben Sie beispielsweise Zugriff auf Eigenschaften der Komponente (im folgenden Listing die Eigenschaft title), können über sogenannte Direktiven bedingte Anweisungen, Schleifen und andere Kontrollstrukturen definieren und vieles mehr, um den zu generierenden HTML-Code zu beeinflussen:
Welcome to {{ title }}!
Users:
{{ user.firstName }} {{ user.lastName }}
Listing 9.11 Template-Code einer Komponente
Hinweis Prinzipiell lassen sich Angular-Anwendungen auch mit reinem JavaScript entwickeln. Dann allerdings fallen auch die Annotationen als Sprachmittel weg, wodurch das Definieren von Komponenten nicht mehr ganz so komfortabel ist.
9.5.2 Komponentenbasierte Architektur am Beispiel von React
React (https://reactjs.org/) wurde von Facebook entwickelt und 2013 unter einer Open-Source-Lizenz veröffentlicht. Wie Angular folgt React einer komponentenbasierten Architektur. Generieren einer React-Anwenung
Um eine React-Anwendung zu erstellen, verwenden Sie am besten das offizielle Starterkit create-react-app (https://github.com/facebook/create-react-app), das Sie über den Befehl npm install -g create-react-app installieren. Ein neues Projekt erzeugen Sie dann über create-react-app example-react und starten es – nach Wechsel in das generierte Verzeichnis – über npm start (standardmäßig unter http://localhost:3000/). $ create-react-app example-react
Creating a new React app in /Users/philipackermann/workspace/example-react.
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
...
Success! Created example-react at /Users/philipackermann/workspace/example-react
Inside that directory, you can run several commands:
npm start
Starts the development server.
npm run build
Bundles the app into static files for production.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can't go back!
We suggest that you begin by typing:
cd example-react
npm start
Listing 9.12 Generierung einer React-Anwendung
Komponenten in React
Eine Beispielkomponente zeigt Listing 9.13. Sie sehen: Komponenten in React leiten entweder von der Klasse Component ab oder können über Funktionen generiert werden. In ersterem Fall
definieren Sie über die Methode render(), wie die Komponente dargestellt («gerendert«) werden soll, in letzterem Fall definiert der Rückgabewert der Funktion den darzustellenden Inhalt. Bei genauem Hinsehen fällt auf, dass der Code, der von der Methode render() bzw. von der Funktion UserList zurückgegeben wird, nicht nur aus JavaScript-Code besteht. Vielmehr setzt React eine eigens entwickelte Erweiterung von JavaScript mit dem Namen JSX (https://facebook.github.io/jsx/) ein, über die sich eine XML-artige Syntax innerhalb des JavaScript-Codes verwenden und mit diesem kombinieren lässt: import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
function UserList(props) {
const users = props.users;
const listItems = users.map((user) =>
{user.firstName} {user.lastName}
);
return (
);
}
const users = [
{
firstName: 'Max',
lastName: 'Mustermann'
},
{
firstName: 'Moritz',
lastName: 'Mustermann'
},
{
firstName: 'Peter',
lastName: 'Mustermann'
},
{
firstName: 'Petra',
lastName: 'Mustermann'
}
];
class App extends Component {
render() {
return (
Welcome to React
);
}
}
export default App;
Listing 9.13 Beispielkomponente einer React-Anwendung
Hinweis Damit der JSX-Code überhaupt im Browser funktioniert, muss er natürlich zuvor über einen entsprechenden Compiler in reinen JavaScript-Code übersetzt werden (beispielsweise mit dem BabelPlugin https://babeljs.io/docs/plugins/transform-react-jsx/). Wenn Sie das offizielle Starterkit verwenden, müssen Sie allerdings diesbezüglich nichts weiter tun, weil hierbei alles im Hintergrund für Sie erledigt wird.
9.5.3 Komponentenbasierte Architektur am Beispiel von Vue.js
Ein weiteres Framework, das die komponentenbasierte Architektur verwendet und in letzter Zeit immer mehr an Beliebtheit gewonnen hat, ist Vue.js (https://vuejs.org/).
Generierung einer Vue.js-Anwendung
Installieren lässt sich Vue.js über den Befehl npm install vue. Außerdem empfiehlt sich die Installation des Kommandozeilentools vue-cli über npm install -g vue-cli, wodurch der Befehl vue verfügbar wird, über den sich das Scaffolding einer Vue.js-Anwendung anstoßen lässt: $ vue init webpack example-vue
? Project name example-vue
? Project description A Vue.js project
? Author Philip Ackermann
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) npm
vue-cli · Generated "example-vue".
# Installing project dependencies ...
# ========================
...
Listing 9.14 Generierung einer Vue.js-Anwendung
Starten lässt sich die Anwendung anschließend über npm
start.
Komponenten in Vue.js
Komponenten in Vue.js haben die Dateiendung *.vue und bestehen, wie in Listing 9.15 zu sehen, im Wesentlichen aus drei Bereichen: Der Bereich definiert den Template-Code der Komponente. Über spezielle Attribute für die Elemente innerhalb des Template-Codes nehmen Sie wie bei Angular auf den Programmablauf und die Darstellung Einfluss (siehe https://vuejs.org/v2/api/#Directives), beispielsweise zur Definition von bedingten Anweisungen oder Schleifen.
Der Bereich
Listing 9.15 Beispielkomponente einer Vue.js-Anwendung
Damit die *.vue-Dateien geladen werden können, benötigen Sie außerdem einen Einstiegspunkt in die Anwendung, den standardmäßig die Datei main.js bildet. In dieser Datei wird unter anderem die Hauptkomponente der Anwendung eingebunden und in Form eines Konfigurationsobjekts dem Aufruf new Vue() als Parameter übergeben: import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: ''
})
Listing 9.16 Startdatei der generierten Vue.js-Anwendung
Hinweis WebStorm erkennt bereits von Haus aus das Format von *.vueDateien und hebt die Syntax der einzelnen Abschnitte entsprechend hervor. Für Visual Studio Code empfiehlt sich dagegen der Einsatz der Erweiterung Vetur (https://github.com/vuejs/vetur).
9.6 Routing Unter dem Begriff Routing versteht man im Zusammenhang mit Webframeworks das Abbilden (Mapping) von URLs auf bestimmte Aktionen bzw. Controller. Bei klassischen Webframeworks wie Spring MVC, Struts oder Ruby on Rails sind dies serverseitige Aktionen. Über spezielle Konfigurationen wird dort definiert, welches Servlet, welcher Webservice etc. durch welches URL-Muster aufgerufen werden soll (siehe Abbildung 9.11). Routing im Falle von modernen JavaScript-Webframeworks funktioniert ähnlich, allerdings mit dem Unterschied, dass nicht serverseitige Komponenten über das Mapping aufgerufen werden, sondern clientseitige Komponenten, sprich JavaScript-Objekte bzw. deren Methoden. Die Herausforderung in diesem Zusammenhang ist, den Zustand der Anwendung in dem Aufbau der URL widerzuspiegeln, weil sich diese bei Single-Page Applications zunächst einmal ja nicht ändert (schließlich wird eine SPA ja über eine einzelne Webseite repräsentiert, beispielsweise http://www.example.com/index.html).
Abbildung 9.11 Beim serverseitigen Routing wird auf Serverseite entschieden, welche Aktion bzw. welcher Controller aufgerufen wird.
Um dennoch innerhalb einer SPA über die URL den Zustand der Anwendung darstellen zu können, wurde lange Zeit von HashFragmenten Gebrauch gemacht. Ursprünglich dienen diese dazu, Links auf bestimmte Elemente (bzw. Anker) innerhalb einer Webseite zu definieren. Die Idee hinter solchen Hashbang-URLs ist es, verschiedene Zustände über verschiedene Hash-Fragmente zu repräsentieren. Zwei verschiedene Zustände einer SPA würden also unter Verwendung von Hashbang-URLs beispielsweise durch URLs wie http://www.example.com/index.html#login und http://www.example.com/index.html#settings dargestellt. Per JavaScript werden Aufrufe an diese URLs dann abgefangen und die Anwendung entsprechend angepasst. Das Abfangen der URLs ist dabei möglich, weil die HTTP-Anfrage über Ajax gestellt wird (siehe Abbildung 9.12) und Sie dementsprechend im JavaScript-Code Einfluss darauf haben. Auch wenn dieses Vorgehen prinzipiell funktioniert, stellt es eher eine Notlösung dar, weil die Hash-Fragmente ursprünglich einem anderen Zweck dienen. Daher wird bei neueren, auf HTML 5 basierenden Lösungen die sogenannte History-API verwendet, um den Zustand von SPAs zu repräsentieren. Mit dieser API ist es möglich, »normale« URLs zu verwenden und aufzurufen, ohne dass durch den Browser direkt eine neue HTTP-Anfrage gestartet wird (was ja zur Folge hätte, dass sich die Webseite komplett neu aufbaut).
Abbildung 9.12 Beim clientseitigen Routing wird auf Clientseite entschieden, welche Aktion bzw. welcher Controller aufgerufen wird.
9.7 Zusammenfassung und Ausblick In diesem Kapitel habe ich Ihnen verschiedene Architekturmuster und Konzepte vorgestellt, die in gängigen JavaScriptWebframeworks zum Einsatz kommen. Folgende Liste gibt Ihnen in komprimierter Form einen Überblick über die wesentlichen Aspekte: Moderne JavaScript-Frameworks zur Erstellung von Single-Page Applications verwenden verschiedene Architekturmuster: Model View Controller (MVC), Model View Presenter (MVP) und Model View ViewModel (MVVM) sowie die komponentenbasierte Architektur. Beim MVC-Architekturmuster wird eine Anwendung in die Komponenten Model, View und Controller eingeteilt. Das MVP-Architekturmuster basiert auf MVC, entkoppelt Model und View aber durch die Presenter-Komponente vollständig voneinander. MVVM ist eine weitere Variante von MVC, bei der das sogenannte ViewModel die Aufgaben der Controller-Komponente übernimmt. Data-Binding sorgt dafür, dass View und ViewModel synchron bleiben, über Command-Binding können Funktionen aus dem ViewModel aus der View heraus aufgerufen werden. Backbone.js ist ein Framework, das MVC umsetzt, wobei die Controller-Funktionalität in der View-Komponente integriert ist; Knockout.js ist ein Beispiel für die Verwendung von MVVM und AngularJS ein Beispiel für die Kombination von MVC und MVVM. Angular und React dagegen sind zwei Beispiele für komponentenbasierte Frameworks.
MVC, MVP und MVVM verfolgen primär alle das gleiche Ziel: die Entkopplung von Model und View. Aus diesem Grund und insbesondere aufgrund der Tatsache, dass die Implementierung der verschiedenen Architekturmuster in modernen JavaScriptFrameworks sehr unterschiedlich und teilweise nicht konsequent implementiert ist, spricht man auch von MV*-Architekturmustern. Bei komponentenbasierten Frameworks wird eine (Web-)Anwendung in verschiedene Komponenten unterteilt. Beispiele hierfür sind Angular, React und Vue.js. Unter Routing versteht man im Zusammenhang mit Webframeworks das Abbilden von URLs auf bestimmte Aktionen. In klassischen Webanwendungen wird das Routing auf Serverseite durchgeführt, in modernen Webanwendungen auf Clientseite. Das Generieren von einzelnen Views erfolgt auf Basis von Templates durch sogenannte Templating-Engines.
10 Messaging Messaging-Systeme helfen dabei, einzelne Softwarekomponenten voneinander zu entkoppeln. Wegen ihrer Programmiersprachenunabhängigkeit können sie auch in JavaScript eingesetzt werden. Bei der Implementierung komplexer Softwaresysteme, bei denen verschiedenen Komponenten bzw. Anwendungen miteinander interagieren müssen, können Messaging-Systeme bzw. MessageBroker dabei helfen, eine Kopplung zwischen den einzelnen Komponenten zu vermeiden.
10.1 Einführung Anstatt dass einzelne Komponenten direkt miteinander kommunizieren (wie in Abbildung 10.1), findet die gesamte Kommunikation über den Message-Broker statt (wie in Abbildung 10.2). Einzelne Komponenten schicken dazu Nachrichten an den Message-Broker oder rufen Nachrichten von dort ab. Message-Broker übernehmen dabei verschiedene Aufgaben: Zum einen können eingehende Nachrichten nach bestimmten Regeln verteilt werden, so dass diese von anderen Komponenten abgerufen werden können (Message-Routing), zum anderen übersetzen Message-Broker Nachrichten zwischen verschiedenen MessagingProtokollen. Im Folgenden möchte ich Ihnen zwei bekannte MessagingProtokolle und entsprechende Message-Broker vorstellen und Ihnen
zeigen, wie Sie diese Protokolle/Broker unter JavaScript verwenden.
Abbildung 10.1 Stark gekoppelte Anwendungen
Abbildung 10.2 Lose gekoppelte Anwendungen
10.2 AMQP AMQP (Advanced Messaging Queuing Protocol) ist ein offener Standard und beschreibt ein Protokoll für den Austausch von Nachrichten. Die wesentlichen Komponenten dabei sind (1) Producer bzw. Publisher, (2) Consumer bzw. Subscriber, (3) Exchanges, (4) Queues sowie (5) Bindings (siehe auch Tabelle 10.1 und Abbildung 10.3). Außerdem wird zwischen Connections und Channels unterschieden.
Abbildung 10.3 Nachrichtenfluss bei AMQP
Begriff
Beschreibung
Producer/Publisher
Komponente, die Nachrichten an das Messaging-System sendet
Consumer/Subscriber Komponente, die Nachrichten aus dem Messaging-System verarbeitet Queue
Nachrichtenspeicher, der Nachrichten nach dem FIFO-Prinzip verwaltet
Message
Daten, die vom Producer zum Consumer über das Messaging-System geschickt werden
Begriff
Beschreibung
Connection
TCP-Verbindung zwischen einer Anwendung und dem Messaging-System
Channel
virtuelle Verbindung innerhalb einer Connection
Exchange
Nachrichtenverteiler, der Nachrichten von Producern entgegennimmt und nach bestimmten Kriterien an Queues weiterleitet
Binding
Verbindung zwischen Queue und Exchange
Routing Key
Schlüssel, nach dem ein Exchange entscheidet, an welche Queue(s) eine Nachricht weitergeleitet werden soll
Tabelle 10.1 Übersicht über wichtige Begriffe bei AMQP
10.2.1 Producer und Consumer
Als Producer bzw. Publisher werden die Komponenten bezeichnet, die Nachrichten an das Messaging-System schicken. Auf der anderen Seite befinden sich die Consumer bzw. Subscriber, sprich Komponenten, die die Nachrichten verarbeiten. Producer senden die Nachrichten dabei an Exchanges, von denen es verschiedene Typen gibt (dazu gleich mehr) und die dafür verantwortlich sind, Nachrichten nach bestimmten Regeln an Message-Queues zur verteilen (die Queues werden dazu an Exchanges gebunden, Stichwort: Message-Binding). Subscriber können dann die Nachrichten aus den Queues abrufen. Innerhalb von Queues werden
– der Name lässt es bereits vermuten – die Nachrichten nach dem FIFO-Prinzip (First In, First Out) verwaltet: Nachrichten, die zuerst in eine Queue weitergeleitet werden, werden auch zuerst bearbeitet (Abbildung 10.4).
Abbildung 10.4 Prinzip einer Message-Queue
Sowohl Producer als auch Consumer müssen – bevor sie Nachrichten senden bzw. empfangen können – eine (TCP-)Verbindung zu dem Messaging-System herstellen. Um dann auf Exchanges bzw. Queues zugreifen zu können, werden dann sogenannte Channels – virtuelle Verbindungen innerhalb der TCPVerbindung – aufgebaut. 10.2.2 Exchanges
Insgesamt stehen standardmäßig vier verschiedene Typen von Exchanges zur Verfügung: Direct Exchange, Fanout Exchange, Topic Exchange und Headers Exchange. Darüber hinaus lassen sich über eine entsprechende Plugin-Schnittstelle sogenannte Custom Exchanges implementieren: Hier können Sie Ihren RoutingWünschen mehr oder weniger freien Lauf lassen (beispielsweise geobasiertes Routing implementieren oder Ähnliches). Fanout Exchanges
Der einfachste Fall der vier genannten Exchanges ist ein Fanout Exchange: Bei diesem werden eingehende Nachrichten an alle an diesen Exchange gebundenen Queues weitergeleitet (siehe Abbildung 10.5). Typischer Anwendungsfall für die Verwendung von Fanout Exchanges ist die Umsetzung des Publish-SubscribeEntwurfsmusters: Eine oder mehrere Komponenten registrieren sich für eine bestimmtes Event und werden alle bei Auftreten des Events benachrichtigt.
Abbildung 10.5 Prinzip eines Fanout Exchanges
Direct Exchanges
Beim Direct Exchange dagegen (siehe Abbildung 10.6) wird anhand eines Routing Keys, den der Publisher mit der Nachricht sendet, entschieden, an welche Queue die Nachricht gesendet werden soll. Nur wenn der Routing Key der Nachricht dem Routing Key entspricht, der in dem Binding zwischen einem Exchange und einer Queue definiert ist, wird die Nachricht an die entsprechende Queue weitergeleitet.
Abbildung 10.6 Prinzip eines Direct Exchanges
Topic Exchanges
Flexibler als Direct Exchanges sind die sogenannten Topic Exchanges (Abbildung 10.7). Das Prinzip dabei ist ähnlich wie bei Direct Exchanges, da auch hier anhand des Routing Keys entschieden wird, auf welche Queues eine Nachricht verteilt wird. Im Unterschied aber zum Direct Exchange, bei dem der Routing Key exakt mit dem am Binding zwischen Queue und Exchange definierten Key übereinstimmen muss, ist es beim Topic Exchange möglich, Wildcards bzw. Routing Patterns am Binding zu definieren.
Abbildung 10.7 Prinzip eines Topic Exchanges
Header Exchanges
Wem die genannten Exchange-Typen nicht reichen, der kann auf die sogenannten Header Exchanges zurückgreifen, die die flexibelste Variante eines Exchanges darstellen (Abbildung 10.8). Das Routing
erfolgt nicht auf Basis eines einzelnen Routing Keys, sondern auf Basis von Header-Informationen, die mit einer Nachricht gesendet werden. Header Exchanges eignen sich also immer dann, wenn Sie das Routing anhand mehrerer Parameter vornehmen möchten und diese nicht alle in einem Routing Key unterbringen können oder möchten.
Abbildung 10.8 Prinzip eines Header Exchanges
10.3 AMQP unter JavaScript Da AMQP programmiersprachenunabhängig ist, können Sie es mit entsprechenden Clientbibliotheken auch unter JavaScript verwenden. Doch zunächst benötigen Sie einen Message-Broker, der AMQP unterstützt. 10.3.1 Installation eines Message-Brokers für AMQP
Einer der bekanntesten Message-Broker für AMQP ist das in der Programmiersprache Erlang implementierte RabbitMQ (https://www.rabbitmq.com/). RabbitMQ kann für alle gängigen Betriebssysteme installiert werden, entsprechende Installationsdateien finden Sie auf der Website unter https://www.rabbitmq.com/download.html. Alternativ dazu steht ein Docker-Image zur Verfügung (Details siehe https://hub.docker.com/r/library/rabbitmq/). Clientbibliotheken für verschiedene Programmiersprachen wie Java, C# oder eben JavaScript finden sich unter https://www.rabbitmq.com/devtools.html. 10.3.2 AMQP-Clients für JavaScript
Für JavaScript bzw. Node.js dürfte das Package amqplib (https://github.com/squaremo/amqp.node) einer der bekanntesten AMQP-Clients sein. Über das Package lassen sich Producer und Consumer implementieren, Exchanges und Queues verwalten, Verbindungen herstellen und Channels aufbauen und vieles weitere mehr. Seine API stellt das Package in zwei Varianten zur Verfügung:
zum einen unter Verwendung von Callback-Funktionen, zum anderen unter Verwendung von Promises. Installiert wird amqplib mit Hilfe des Node.js Package Managers über den Befehl npm install amqplib. Anschließend binden Sie es über require('amqplib/callback_api') (bei Verwendung der Callback-API) bzw. require('amqplib') (bei Verwendung der Promise-API) ein. 10.3.3 Senden und Empfangen von Nachrichten
Der einfachste Fall einer Kommunikation zwischen Producer und Consumer verzichtet ganz auf Exchanges und läuft direkt über eine Queue: Der Producer schickt seine Nachricht direkt an diese Queue, der Consumer holt diese Nachricht aus der Queue und verarbeitet sie. Den Code dafür zeigen Listing 9.1 und Listing 10.2 (Producer und Consumer bei Verwendung der Callback-API) sowie Listing 10.3 Listing 10.4 (Producer und Consumer bei Verwendung der PromiseAPI). In allen Fällen wird zunächst über den Aufruf der Methode connect() eine Verbindung zum Messaging-System hergestellt und innerhalb dieser (TCP-)Verbindung eine virtuelle Verbindung aufgebaut (über den Aufruf von createChannel()). Der Aufruf von assertQueue() ist optional: Wurde die Queue bereits vorher erstellt – entweder programmatisch oder über die ManagementOberfläche von RabbitMQ (https://www.rabbitmq.com/management.html) –, wird sie nicht erneut erzeugt. Um eine Nachricht an die Queue zu schicken, rufen Sie innerhalb des Codes für den Producer die Methode sendToQueue() auf. Ihr übergeben Sie den Namen der Queue sowie den Inhalt der
Nachricht in Form eines Buffers. Auf Seite des Consumers wird über die Methode consume() eine Callback-Funktion an dem Channel registriert, der immer dann aufgerufen wird, wenn eine neue Nachricht für den Consumer in der Queue bereitgestellt wird. Zu beachten: Werden mehrere Consumer an einer Queue registriert, werden die Nachrichten nach dem Round-Robin-Verfahren nacheinander an die Consumer verteilt. Round Robin Ganz allgemein bezeichnet Round Robin in der Informatik eine sogenannte Scheduling-Strategie, bei der Prozessen nacheinander für jeweils einen kurzen Zeitraum Ressourcen zur Verfügung gestellt werden. Bei drei Prozessen P1, P2 und P3 beispielsweise geschieht dies in der folgenden Reihenfolge: P1, P2, P3, P1, P2, P3 usw. Übertragen auf das Versenden von Nachrichten beim Messaging bedeutet dies bei drei Consumern C1, C2 und C3 also, dass die erste Nachricht an C1 gesendet wird, die zweite an C2, die dritte an C3, die vierte wieder an C1 usw. 'use strict';
const amqp = require('amqplib/callback_api');
const configuration = {
hostname: '',
username: '',
password: '',
connectionTimeout: 10000,
authMechanism: 'AMQPLAIN',
vhost: '/',
noDelay: true,
ssl: {
enabled: true
}
};
const queue = 'example-queue';
amqp.connect(configuration, (error, connection) => {
connection.createChannel((error, channel) => {
channel.assertQueue(queue);
channel.sendToQueue(queue, new Buffer('Hello World!'));
console.log(` [x] Sent Hello World!`);
});
setTimeout(() => {
connection.close(); process.exit(0)},
500);
});
Listing 10.1 Versenden einer Nachricht an eine Queue (Callback-API) 'use strict';
const amqp = require('amqplib/callback_api');
const configuration = {
/* ... */
}
const queue = 'example-queue';
amqp.connect(configuration, (error, connection) => {
connection.createChannel((error, channel) => {
channel.assertQueue(queue);
channel.consume(queue, message => {
if (message !== null) {
console.log(message.content.toString());
channel.ack(message);
}
});
});
});
Listing 10.2 Verarbeiten einer Nachricht (Callback-API) 'use strict';
const amqp = require('amqplib');
const configuration = {
/* ... */
}
const queue = 'example-queue';
amqp.connect(configuration)
.then(connection => connection.createChannel())
.then(channel => {
return channel.assertQueue(queue).then(ok => {
return channel.sendToQueue(queue, new Buffer('Hello World!'));
});
});
Listing 10.3 Versenden einer Nachricht an eine Queue (Promise-API) 'use strict';
const amqp = require('amqplib');
const configuration = {
/* ... */
}
const queue = 'example-queue';
amqp.connect(configuration)
.then(connection => connection.createChannel())
.then(channel => {
return channel.assertQueue(queue).then(ok => {
return channel.consume(queue, message => {
if (message !== null) {
console.log(message.content.toString());
channel.ack(message);
}
});
});
});
Listing 10.4 Verarbeiten einer Nachricht (Promise-API)
Methode
Beschreibung
assertQueue()
Prüft, ob es eine Queue gibt. Falls dies nicht so ist, wird die Queue angelegt.
checkQueue()
Prüft, ob es eine Queue gibt.
deleteQueue()
Löscht eine Queue.
purgeQueue()
Entfernt alle noch nicht verarbeiteten Nachrichten aus einer Queue.
bindQueue()
Bindet eine Queue an einen Exchange.
unbindQueue()
Löst die Bindung einer Queue zu einem Exchange.
assertExchange() Prüft, ob es einen Exchange gibt. Falls dies nicht so ist, wird der Exchange angelegt. checkExchange()
Prüft, ob es einen Exchange gibt.
deleteExchange() Löscht einen Exchange.
Methode
Beschreibung
bindExchange()
Bindet einen Exchange an einen anderen Exchange.
unbindExchange() Löst die Bindung eines Exchanges zu einem Exchange. publish()
Sendet eine Nachricht an einen Exchange.
sendToQueue()
Sendet eine Nachricht direkt an eine Queue.
consume()
Registriert einen Consumer an einer Queue.
cancel()
Stoppt das Senden von Nachrichten an einen bestimmten Consumer.
get()
Ruft eine Nachricht von einer Queue ab.
ack()
Bestätigt den Empfang einer Nachricht.
ackAll()
Bestätigt den Empfang aller auf dem Channel ausstehenden Nachrichten.
nack()
Weist eine Nachricht ab.
nackAll()
Weist alle auf dem Channel ausstehenden Nachrichten ab.
reject()
Weist eine Nachricht ab. Äquivalent zu nack(), funktioniert aber auch in älteren Versionen von RabbitMQ.
prefetch()
Setzt die Anzahl an über einen Channel abzurufenden Nachrichten.
Methode
Beschreibung
recover()
Reiht Nachrichten, deren Empfang noch nicht bestätigt wurde, wieder in die Queue zurück.
close()
Schließt einen Channel.
Tabelle 10.2 Methoden der amqplib-API
10.3.4 Verwenden von Exchanges
AMQP unterstützt wie bereits erwähnt standardmäßig vier verschiedene Typen von Exchanges, die definieren, auf welche Art und Weise Nachrichten an Queues verteilt werden. Listing 10.5 zeigt exemplarisch die Verwendung eines Fanout Exchanges: Über die Methode connect() und createChannel() werden – wie zuvor auch – zunächst TCP-Verbindung und virtuelle Verbindung zu dem Messaging-Broker aufgebaut, anschließend wird über die Methode assertExchange() sichergestellt, dass es einen Exchange mit dem angegebenen Namen gibt (in diesem Fall »example-fanout-exchange«). Diese Methode funktioniert analog zu der in den vorigen Listings bereits verwendeten Methode assertQueue() und erzeugt einen Exchange nur neu, falls es noch keinen entsprechenden Exchange im System gibt. Außerdem wird über assertQueue() die Existenz zweier Queues sichergestellt (»example-queue« und »example-queue-2«), und diese werden jeweils über bindQueue() an den Exchange gebunden (existieren Exchanges, Queues und die Bindings zwischen beidem bereits, sind die Aufrufe assertExchange(), assertQueue() und bindQueue() natürlich überflüssig).
'use strict';
const amqp = require('amqplib');
const configuration = {
/* ... */
};
const exchange = 'example-fanout-exchange';
const queue1 = 'example-queue';
const queue2 = 'example-queue-2';
amqp.connect(configuration)
.then(connection => connection.createChannel())
.then(channel => {
channel
.assertExchange(exchange, 'fanout')
.then(ok => {
return Promise.all(
[
channel
.assertQueue(queue1)
.then(ok => channel.bindQueue(queue1, exchange, '')),
channel
.assertQueue(queue2)
.then(ok => channel.bindQueue(queue2, exchange, ''))
]
);
})
.then(ok => {
channel.publish(exchange, '', new Buffer('Hello World!'))
});
});
Listing 10.5 Verwenden eines Fanout Exchanges
Das eigentliche Versenden einer Nachricht an den Exchange geschieht über die Methode publish(). Ihr übergeben Sie als ersten Parameter den Namen des Exchanges, an den die Nachricht gesendet werden soll, als zweiten Parameter optional einen Routing Key (im Falle von Fanout Exchanges bleibt dieser leer, da hierbei der Routing Key ohnehin keine Rolle spielt und daher von RabbitMQ ignoriert wird) und schließlich als dritten Parameter den Inhalt der Nachricht in Form eines Buffers. Optional kann als vierter Parameter ein Konfigurationsobjekt übergeben werden, über das man beispielsweise darauf Einfluss nehmen kann, wie lange eine Nachricht in einer Queue vorgehalten wird, bevor sie – bei Nichtbearbeitung – aus dem System gelöscht wird, oder ob
Nachrichten persistiert werden sollen und somit auch einen Neustart des Message-Brokers überdauern (siehe Tabelle 10.2). Des Weiteren lassen sich unter anderem MIME Type und Encoding des Nachrichteninhalts, ein anwendungsspezifischer Nachrichtentyp, ein Timestamp sowie Header definieren, die das Routing beim Header Exchange beeinflussen. Eigenschaft
Beschreibung
expiration
Millisekunden, nach denen eine Nachricht aus dem System gelöscht wird
userId
Wenn eine User-ID angegeben wird, wird sie mit der User-ID verglichen, für die die Verbindung zu dem Message-Broker aufgebaut wurde. Wenn die User-IDs nicht übereinstimmen, wird die Nachricht abgelehnt.
CC
Ermöglicht die Angabe zusätzlicher Routing Keys.
priority
Zahlenwert, der die Priorität der Nachricht repräsentiert
persistent
boolesche Angabe darüber, ob die Nachricht persistiert werden und somit auch einen Neustart des Message-Brokers überstehen soll
Eigenschaft
Beschreibung
deliveryMode
Zahlenwert oder boolesche Angabe darüber, ob die Nachricht persistiert werden soll (1 bzw. true) oder nicht (2 bzw. false). Es empfiehlt sich jedoch die Verwendung der Eigenschaft persistent.
mandatory
boolesche Angabe darüber, ob die Nachricht zurückgeschickt werden soll, falls es kein passendes Binding zwischen Exchange und einer Queue gibt und die Nachricht daher nicht an eine Queue weitergeleitet werden kann
BCC
Wie die Eigenschaft »CC«, allerdings werden die Routing Keys nicht an den Consumer weitergeschickt bzw. sind dann nicht mehr in den Headern enthalten.
immediate
boolesche Angabe darüber, ob die Nachricht zurückgeschickt werden soll, falls sie nicht direkt an einen Consumer geschickt werden kann
contentType
MIME-Typ des Nachrichteninhalts
contentEncoding Encoding des Nachrichteninhalts headers
anwendungsspezifische Header
correlationId
ID, die dazu dient, Anfragenachrichten zu Antwortnachrichten zuzuordnen
Eigenschaft
Beschreibung
replyTo
Ermöglicht die Angabe einer Queue, zu der eine Antwortnachricht gesendet werden soll.
messageId
anwendungsspezifische ID der Nachricht
timestamp
Zeitstempel der Nachricht
type
anwendungsspezifischer Typ der Nachricht
appId
anwendungsspezifische ID der Anwendung, durch die die Nachricht versendet wurde
immediate
boolesche Angabe darüber, ob die Nachricht zurückgeschickt werden soll, falls sie nicht direkt an einen Consumer geschickt werden kann.
contentType
MIME-Typ des Nachrichteninhalts
Tabelle 10.3 Eigenschaften von Nachrichten
Zum Vergleich sehen Sie in Listing 10.6 die Verwendung eines Direct Exchanges. Der Code ist prinzipiell dem Code aus Listing 10.5 sehr ähnlich, die Unterschiede liegen im Detail: Zum einen übergeben wir der Methode assertExchange() als zweiten Parameter den Wert direct, zum anderen übergeben wir der Methode publish() als zweiten Parameter einen Routing Key, anhand dessen das Routing erfolgt. 'use strict';
const amqp = require('amqplib');
const configuration = {
/* ... */
};
const exchange = 'example-direct-exchange';
const queue = 'example-queue';
const key = 'example-key';
amqp.connect(configuration)
.then(connection => connection.createChannel())
.then(channel => {
channel
.assertExchange(exchange, 'direct')
.then(ok => {
return channel
.assertQueue(queue)
.then(ok => channel.bindQueue(queue, exchange, key))
})
.then(ok => {
channel.publish(exchange, key, new Buffer('Hello World!'))
});
});
Listing 10.6 Verwenden eines Direct Exchanges
10.3.5 STOMP
Möchten Sie aus einem Browser heraus auf RabbitMQ zugreifen, eignet sich das Protokoll STOMP (Streaming Text Oriented Message Protocol), ein textbasiertes, HTTP-ähnliches Protokoll, das für den Einsatz in Messaging-Systemen konzipiert wurde und über ein entsprechendes Plugin (https://www.rabbitmq.com/stomp.html) auch von RabbitMQ unterstützt wird. Bibliotheken wie stomp-websocket (https://github.com/jmesnil/stomp-websocket) oder webstomp-client (https://github.com/JSteunou/webstomp-client) stellen Clients zur Verfügung, die es ermöglichen, STOMP über Web-Sockets (STOMP over WebSockets) zu nutzen. Dazu muss allerdings auf Serverseite (d.h. unter RabbitMQ) neben dem STOMP-Plugin zusätzlich das Web-STOMP-Plugin installiert werden (https://www.rabbitmq.com/web-stomp.html). Für die folgenden Beispiele verwenden wir die Bibliothek stomp-websocket, die Sie unter http://jmesnil.net/stomp-websocket/doc/ herunterladen oder über npm install stompjs installieren können.
Nachdem Sie die Bibliothek im HTML-Code eingebunden haben, steht das globale Objekt webstomp zur Verfügung. Über die Methode over() lässt sich wie in Listing 10.7 zu sehen ein STOMPClient erzeugen, wobei als Parameter eine WebSocketObjektinstanz zu übergeben ist (alternativ dazu erstellen Sie über die Methode overTCP() einen STOMP-Client auf Basis einer TCPVerbindung). Um sich zu dem Message-Broker zu verbinden, rufen Sie anschließend die Methode connect() auf, wobei der Nutzername, Passwort, ein Callback-Handler für die erfolgreiche Verbindungsherstellung, ein Callback-Handler für den Fehlerfall und optional ein virtueller Host anzugeben sind. Über die Methode subscribe() wiederum können Sie sich anschließend an einer Queue registrieren, indem Sie den Namen der Queue und einen entsprechenden Callback-Handler übergeben. 'use strict';
const USERNAME = '';
const PASSWORD = '';
const VHOST = '';
const URL = 'ws://localhost:61613/ws';
const ws = new WebSocket(URL);
ws.onerror = (error) => console.error(error);
const client = webstomp.over(ws, { "binary": true });
client.connect(
USERNAME,
PASSWORD,
handleConnect,
handleError,
VHOST
);
function handleConnect() {
client.subscribe('example-queue', message => {
console.log(message.headers);
let content = JSON.parse(message.body);
});
}
function handleError() {
console.log('error');
}
Listing 10.7 Verwenden des STOMP-Clients
Eine Übersicht der zur Verfügung stehenden Methoden der STOMPClient-API zeigt Tabelle 10.4. Methode
Beschreibung
Stomp.over(ws)
Erzeugt einen STOMPClient, der sich unter Verwendung des übergebenen WebSocketObjekts über Web-Sockets zu dem Message-Broker verbindet.
Stomp.overTCP(
host,
port
)
Erzeugt einen STOMPClient, der sich über eine TCP-Verbindung zu dem Message-Broker verbindet.
Stomp.overWS(url)
Erzeugt einen STOMPClient, der sich unter Verwendung der angegebenen URL über Web-Sockets zu dem Message-Broker verbindet.
Methode
Beschreibung
client.connect(
login,
passcode,
connectCallback,
errorCallback,
host
)
Stellt die Verbindung zu dem Message-Broker her.
client.connect(
headers
connectCallback,
errorCallback
) client.disconnect(
disconnectCallback
)
Beendet die Verbindung zu dem Message-Broker.
client.send(
destination,
config,
body
)
Sendet eine Nachricht an den Message-Broker.
client.subscribe(
destination,
callback,
config
)
Registriert den Client an einer Queue.
Methode
Beschreibung
subscription.unsubscribe() Trennt die Verbindung zu einer Queue. client.begin()
Startet eine Transaktion.
transaction.commit()
Löst die Transaktion aus.
transaction.abort()
Bricht die Transaktion ab.
Tabelle 10.4 Methoden der STOMP-Client-API
10.4 MQTT Bei MQTT (Message Queue Telemetry Transport) handelt es sich um ein Messaging-Protokoll, das vor allem im Bereich des Internet of Things zum Einsatz kommt, beispielsweise in der Industrie 4.0, im Zusammenspiel mit eHealth- und mHealth-Anwendungen und in Smart-Home- oder sogar Smart-City-Szenarien. Entwickelt wurde das Protokoll bereits 1999 von den beiden Firmen IBM und Arcom Control Systems im Rahmen eines gemeinsamen Projekts zur Überwachung einer Öl-Pipeline. Maßgebendes Ziel war es, ein Protokoll zu entwickeln, das auch in unzuverlässigen Netzwerken mit niedriger Bandbreite und hoher Latenz funktioniert. 2010 wurde MQTT dann unter einer Open-Source-Lizenz veröffentlicht, seit 2013 ist das Protokoll zudem ein OASIS-Standard (Organization for the Advancement of Structured Information Standards, https://www.oasis-open.org/committees/tc_home.php? wg_abbrev=mqtt) und liegt dort in Version 3.1.1 vor (http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html). Die genannten Anforderungen als Ausgangspunkt gegeben, zeichnet sich MQTT im Wesentlichen durch folgende Eigenschaften aus: Leichtgewichtigkeit: Auch Geräte, die nur über eingeschränkte Ressourcen verfügen, können MQTT einfach nutzen, beispielsweise »IoT-Geräte« oder Mikrocontroller wie der Arduino. Unterschiedliche Servicequalitäten: Um eine Übertragung der Nachrichten auch in instabilen Netzwerken zu gewährleisten, werden unterschiedliche Servicequalitäten (kurz QoS für Quality
of Service) zur Verfügung gestellt. Je nach Stabilität des Netzwerks kann zwischen drei verschiedenen QoS-Leveln gewählt werden. Effiziente Übertragung: Damit der Nachrichtenaustausch auch bei der Kommunikation mit Geräten geringer Bandbreite funktioniert, beispielsweise mit Smartphones oder »IoTGeräten«, überträgt MQTT die Daten in einem kompakten Binärformat und arbeitet daher sehr effizient. Session-Awareness: Setzen Sie MQTT in Szenarien ein, in denen mit vielen Verbindungsabbrüchen seitens der Clients zu rechnen ist, können Nachrichten, die durch solche Verbindungsabbrüche normalerweise verlorengingen, zwischengespeichert und zu einem späteren Zeitpunkt – wenn der Client die Verbindung wiederhergestellt hat – gesendet werden. Datenagnostik: MQTT kann Daten unterschiedlichen Typs übertragen, beispielsweise Text-, Binär- oder Objektnachrichten. 10.4.1 Publish-Subscribe und Topics
MQTT implementiert das Publish-Subscribe Messaging Pattern: Publisher versenden Nachrichten, ein oder mehrere Subscriber empfangen Nachrichten. Dabei kommunizieren Publisher und Subscriber aber nicht direkt miteinander, sondern sind über einen Broker (in diesem Fall über einen MQTT-Broker) voneinander entkoppelt (siehe Abbildung 10.9). Der Broker sorgt anhand sogenannter Topics dafür, dass Nachrichten an den oder die richtigen Empfänger gelangen.
Abbildung 10.9 Workflow bei MQTT
Bei Topics wiederum handelt es sich im Wesentlichen um einfache Zeichenketten, die – ähnlich wie eine URL – hierarchisch aufgebaut sein können und aus mehreren Topic-Ebenen bestehen, beispielsweise »home/livingroom/temperatureSensor« oder »home/garage/lightSensor«. Subscriber registrieren sich beim Broker für ein oder mehrere Topics und können darüber festlegen, an welchen Nachrichten sie interessiert sind. Geht beim Broker dann eine Nachricht zu einem bestimmten Topic ein, wird die Nachricht an alle Subscriber für das jeweilige Topic weitergeleitet (siehe Abbildung 10.10).
Abbildung 10.10 Topics in MQTT
10.4.2 Wildcards
Bei der Definition von Topics können Sie über Wildcards kann relativ flexibel definieren, an welchen Topics Sie interessiert sind: »home/#« informiert den Subscriber über alle Topics, die mit »home/« beginnen, mit anderen Worten: über alle Sensordaten im gesamten Haus, »/home/garage/#« dagegen lediglich über alle Sensordaten in der Garage. Wildcards lassen sich nicht nur am Ende
eines Topic-Strings verwenden, sondern an beliebiger Stelle bzw. an Stelle beliebiger Topic-Ebenen. Möchten Sie beispielsweise über alle Lichtsensoren im gesamten Haus informiert werden, registrieren Sie sich einfach für das Topic »home/#/lightSensor«. Der Wildcard-Operator # deckt dabei mehrere Topic-Ebenen ab, im Beispiel also sowohl »home/garage/lightSensor« als auch »home/groundfloor/bathroom/lightSensor«. Der WildcardOperator + dagegen deckt nur eine Topic-Ebene ab: »home/+/lightSensor« würde demnach »home/garage/lightSensor« abdecken, nicht aber »home/groundfloor/bathroom/lightSensor«. 10.4.3 Quality of Service
Wie eingangs erwähnt bietet MQTT verschiedene Features an, die speziell auf die eingangs genannten Anforderungen ausgelegt sind. Eines dieser Features ist die Möglichkeit, verschiedene Servicequalitätslevel zu verwenden: Level 0: Hierbei gibt es keine Garantie dafür, dass eine Nachricht bei den Subscribern ankommt (Abbildung 10.11). Nachrichten werden weder vom Empfänger bestätigt noch vom Sender gespeichert.
Abbildung 10.11 Quality of Service Level 0
Level 1: Garantiert, dass eine Nachricht mindestens einmal ankommt (Abbildung 10.12). Der Sender der Nachricht speichert die Nachricht so lange, bis er vom Empfänger eine Bestätigung
(PUBACK) erhält. Geschieht dies nicht innerhalb eines bestimmten Zeitfensters, wird die Nachricht erneut gesendet.
Abbildung 10.12 Quality of Service Level 1
Level 2: Garantiert über eine mehrstufige Kommunikation zwischen Sender und Empfänger, dass eine Nachricht genau einmal ankommt (Abbildung 10.13).
Abbildung 10.13 Quality of Service Level 2
Welchen Level Sie wählen, hängt vom konkreten Anwendungsfall ab. Generell gilt: je höher der QoS-Level, desto höher die benötigte Bandbreite. QoS Level
Anzahl der Zustellungen von Nachrichten
Garantie erfolgreicher Zustellungen
0
höchstens einmal
keine Garantie
1
mindestens einmal
Zustellung garantiert, aber Duplikate möglich
2
exakt einmal
Zustellung garantiert, keine Duplikate
Tabelle 10.5 QoS-Level in MQTT
10.4.4 Last Will and Testament
Ein weiteres Feature von MQTT ist das sogenannte Last Will and Testament (LWT), über das es möglich ist, bei einem unerwarteten Verbindungsabbruch eines Clients eine Nachricht (sozusagen einen letzten Willen) an andere Clients zu schicken. 10.4.5 Retained Messages
Bei Retained Messages wiederum handelt es sich um Nachrichten, die für ein bestimmtes Topic vom MQTT-Broker gespeichert und an alle Clients, die sich für das jeweilige Topic registrieren, gesendet werden. Wird beispielsweise unter dem Topic "/home/garage/temperatureSensor" jede volle Stunde der gemessene Temperaturwert veröffentlicht, können Sie über Retained Messages dafür sorgen, dass Subscriber, die sich für das Topic registrieren, direkt den zuletzt gemessenen Wert erhalten. Mit anderen Worten: Auch wenn ein Subscriber zum eigentlichen Zeitpunkt, an dem die Nachricht gesendet wurde, noch nicht für das Topic registriert war, verpasst er die Nachricht nicht. 10.4.6 Persistent Sessions
Ein weiteres Feature von MQTT sind sogenannte Persistent Sessions. Diese sind insbesondere dann nützlich, wenn mit häufigen Verbindungsabbrüchen zu rechnen ist. Verliert ein Client die Verbindung und verbindet sich erneut zum Broker, sendet dieser alle verpassten Nachrichten (für alle Subscriptions) an den Client.
10.5 MQTT unter JavaScript Wie AMQP lässt sich auch MQTT mit entsprechenden Clientbibliotheken unter JavaScript verwenden. 10.5.1 Installation eines Message-Brokers für MQTT
Implementierungen für MQTT-Broker gibt es verschiedene, wobei sich grob unterscheiden lässt zwischen reinen MQTT-Brokern und solchen, die neben anderen Protokollen auch MQTT als MessagingProtokoll unterstützen: HiveMQ (http://www.hivemq.com/), Mosquitto (https://mosquitto.org/) und Moquette (https://github.com/andsel/moquette) fallen in die erste Kategorie, RabbitMQ (https://www.rabbitmq.com/) und ActiveMQ (http://activemq.apache.org/) dagegen implementieren nicht nur MQTT als Messaging-Protokoll, sondern weitere wie beispielsweise AMQP. Welchen MQTT-Broker Sie für die folgenden Beispiele verwenden, spielt eigentlich keine Rolle. Beispielsweise lässt sich der Broker Mosquitto relativ einfach in Form eines Docker-Containers starten (docker run -it -p 1883:1883 -p 9001:9001 eclipse-mosquitto) und stellt anschließend einen Port (1883) für MQTT und einen Port (9001) für MQTT over WebSockets bereit. Etwas weniger bekannt, aber ebenso interessant ist an dieser Stelle mosca (https://github.com/mcollina/mosca), ein Package für Node.js, das Sie entweder als Standalone-MQTT-Broker verwenden oder sich programmatisch in die eigene Anwendung einbinden können. Für Ersteres müssen Sie das Package global installieren (npm install --g mosca), wodurch anschließend der Befehl mosca zur Verfügung steht,
der den MQTT-Broker startet. Für Letzteres installieren Sie das Package lokal (npm install mosca --save) und binden es wie gewohnt per require('mosca') ein. Die Verwendung von mosca ist relativ einfach, Listing 10.8 zeigt ein entsprechendes Beispiel. Nach Einbinden des Packages wird über den Aufruf new mosca.Server({...}) der MQTT-Broker initialisiert, wobei als Parameter Konfigurationsdaten wie Port und Angaben zum verwendeten Backend übergeben werden. Über das Backend werden unter anderem Nachrichten gespeichert, was beispielsweise für die Servicelevel 1 und 2 und die Features Last Will and Testament, Retained Messages und Persistent Sessions notwendig ist. Im Beispiel verwenden wir als Backend die Datenbank MongoDB (https://www.mongodb.com), es stehen aber auch weitere Datenbanksysteme zur Verfügung wie etwa die In-MemoryDatenbank Redis (https://github.com/mcollina/mosca/wiki/Persistence-support). Alternativ dazu können Sie auch existierende MQTT-Broker wie Mosquitto, RabbitMQ und ZeroMQ als »Parent-Broker« angeben, die dann die Persistierung der Daten übernehmen. Über die Methode on() lassen sich anschließend Event-Handler für verschiedene Events definieren, unter anderem für folgende: »ready« wird ausgelöst, wenn der Broker erfolgreich gestartet wurde, »clientConnected« und »clientDisconnected«, wenn sich ein neuer Client zu dem Broker verbindet bzw. die Verbindung wieder löst, und »published«, wenn eine neue Nachricht veröffentlicht wird. 'use strict';
const mosca = require('mosca');
const settings = {
port: 1883,
backend: {
type: 'mongo',
url: 'mongodb://localhost:27017/mqtt',
pubsubCollection: 'ascoltatori',
mongo: {}
}
};
const server = new mosca.Server(settings);
server.on('clientConnected', client => {
console.log(`Client connected: ${client.id}`);
});
server.on('clientDisonnected', client => {
console.log(`Client disconnected: ${client.id}`);
});
server.on('error', error => {
console.error(error);
});
server.on('published', (packet, client) => {
console.log(`Published: ${packet.payload}`);
});
server.on('ready', () => {
console.log('Mosca server is up and running');
});
Listing 10.8 Verwenden des MQTT-Brokers mosca unter Node.js
10.5.2 MQTT-Clients für JavaScript
Haben Sie sich für einen MQTT-Broker entschieden, stellt sich noch die Frage nach einem geeigneten MQTT-Client. Für JavaScript steht beispielsweise das Package MQTT.js (https://github.com/mqttjs/MQTT.js) zur Verfügung, das Sie mit Hilfe des Node.js Package Managers über den Befehl npm install mqtt lokal installieren. Ein Beispiel für die Verwendung sehen Sie in Listing 10.9 und Listing 10.10. Ersteres zeigt den Code für einen Sender von Nachrichten, Letzteres den Code für einen Empfänger von Nachrichten. In beiden Fällen wird über den Aufruf von
mqtt.connect(...) ein Clientobjekt erzeugt, wobei jeweils die
URL des MQTT-Brokers und über ein Konfigurationsobjekt die ID des Clients übergeben werden. Ist die Verbindung zum Broker hergestellt, kann sich der jeweilige Client anschließend über subscribe() für ein Topic registrieren oder über publish() Nachrichten zu einem Topic veröffentlichen (siehe Tabelle 10.6 für eine Übersicht der zur Verfügung stehenden Methoden). Auf Consumer-Seite können eingehende Nachrichten zudem über das Event »message« abgefangen und verarbeitet werden. 'use strict';
const mqtt = require('mqtt');
const HOSTNAME = 'localhost';
const PORT = 1883;
const CLIENT_ID = 'MQTT.js Node.js Client Consumer';
const client = mqtt.connect(`mqtt://${HOSTNAME}:${PORT}`, {
clientId: CLIENT_ID
});
client.on('connect', () => {
console.log('Consumer connected to MQTT broker')
client.subscribe('home/garage/lightSensor');
});
client.on('message', (topic, message) => {
console.log(message.toString());
});
Listing 10.9 Verwenden des MQTT.js-Clients unter Node.js (Consumer) 'use strict';
const mqtt = require('mqtt');
const HOSTNAME = 'localhost';
const PORT = 1883;
const CLIENT_ID = 'MQTT.js Node.js Client Publisher';
const client = mqtt.connect(`mqtt://${HOSTNAME}:${PORT}`, {
clientId: CLIENT_ID
});
client.on('connect', () => {
console.log('Publisher connected to MQTT broker');
client.publish('home/garage/lightSensor', '12.2');
});
Listing 10.10 Verwenden des MQTT.js-Clients unter Node.js (Publisher)
Das Package »mqtt« kann nicht nur unter Node.js, sondern auch im Browser verwendet werden, die API ist dabei die gleiche. Da die Kommunikation allerdings über Web-Sockets läuft, müssen Sie der Methode connect(), wie in Listing 10.11 und Listing 10.12 zu sehen, eine Web-Socket-URL übergeben. 'use strict';
const HOSTNAME = 'localhost';
const PORT = 9001;
const CLIENT_ID = 'MQTT.js Browser Client Consumer';
const client = mqtt.connect(`ws://${HOSTNAME}:${PORT}`, {
clientId: CLIENT_ID
});
client.on('connect', () => {
console.log('Consumer connected to MQTT broker')
client.subscribe('home/garage/lightSensor');
});
client.on('message', (topic, message) => {
console.log(message.toString())
});
Listing 10.11 Verwenden des MQTT.js-Clients im Browser (Consumer) 'use strict';
const HOSTNAME = 'localhost';
const PORT = 9001;
const CLIENT_ID = 'MQTT.js Browser Client Publisher';
const client = mqtt.connect(`ws://${HOSTNAME}:${PORT}`, {
clientId: CLIENT_ID
});
client.on('connect', () => {
console.log('Publisher connected to MQTT broker');
client.publish('home/garage/lightSensor', '12.2');
});
client.on('message', (topic, message) => {
console.log(message.toString())
client.end()
});
Listing 10.12 Verwenden des MQTT.js-Clients im Browser (Publisher)
Methode
Beschreibung
mqtt.connect()
Herstellen einer Verbindung zu einem MQTT-Broker
mqtt.Client()
Repräsentiert eine Verbindung zu einem MQTT-Broker.
mqtt.Client#on()
Registrieren eines Event-Listeners für verschiedene Events
mqtt.Client#publish()
Veröffentlichen einer Nachricht
mqtt.Client#subscribe()
Abonnieren eines Topics
mqtt.Client#unsubscribe()
Kündigen eines TopicAbonnements
mqtt.Client#end()
Schließen der Verbindung
mqtt.Client#handleMessage()
Reagieren auf Nachrichten
mqtt.Client#connected
boolesche Angabe darüber, ob der Client verbunden ist oder nicht
Methode
Beschreibung
mqtt.Client#reconnecting
boolesche Angabe darüber, ob der Client gerade versucht, sich erneut zum MQTTBroker zu verbinden
mqtt.Client#getLastMessageId() Liefert die ID der zuletzt gesendeten Nachricht. mqtt.Store()
Repräsentiert einen InMemoryNachrichtenspeicher.
mqtt.Store#put()
Fügt dem Nachrichtenspeicher eine neue Nachricht hinzu.
mqtt.Store#del()
Löscht eine Nachricht aus dem Nachrichtenspeicher.
mqtt.Store#createStream()
Erzeugt einen Stream mit allen Nachrichten aus dem Nachrichtenspeicher.
mqtt.Store#close()
Schließt den Nachrichtenspeicher.
Tabelle 10.6 Methoden der MQTT.js-Bibliothek
10.6 Zusammenfassung und Ausblick In diesem Kapitel habe ich Ihnen gezeigt, wie Sie Messaging in JavaScript-Anwendungen integrieren können. Die wichtigsten Punkte dabei sind: Messaging-Systeme bzw. Message-Broker helfen bei der Entkopplung von Softwarekomponenten. Zwei der bekanntesten Messaging-Protokolle sind AMQP (Advanced Message Queuing Protocol) und MQTT (Message Queuing Telemetry Transport). Für beide Protokolle stehen verschiedene Message-Broker zur Verfügung, wie beispielsweise RabbitMQ , Mosquitto und mosca. Eine Auswahl weiterer Message-Broker finden Sie in Tabelle 5.1. Für beide Protokolle stehen außerdem sowohl für den Einsatz unter Node.js, als auch für den Einsatz im Browser entsprechende Clientbibliotheken für JavaScript zur Verfügung. Über STOMP lässt sich AMQP über Web-Sockets verwenden. Bibliothek
Link
ActiveMQ
http://activemq.apache.org/
ActiveMQ Apollo http://activemq.apache.org/apollo/ Apache Kafka
http://kafka.apache.org/
Apache Qpid
https://qpid.apache.org/
RabbitMQ
http://www.rabbitmq.com/
Bibliothek
Link
HornetMQ
http://hornetq.jboss.org/
mosca
https://github.com/mcollina/mosca
Mosquitto
https://mosquitto.org/
ZeroMQ
http://zeromq.org/
Tabelle 10.7 Auswahl bekannter Message-Broker
Wenn Sie sich weiter zu den Themen AMQP, MQTT, RabbitMQ oder Messaging im Allgemeinen informieren möchten, finden Sie in Tabelle 10.8 eine kleine Auswahl an Literatur. Insbesondere das Buch »Enterprise Integration Patterns«, das zahlreiche allgemeingültige Messaging-Patterns erläutert, sei Ihnen dabei wärmstens ans Herz gelegt. Autor(en)
Titel
Erscheinungsjahr
Gavin M. Roy
RabbitMQ in Depth
2017
Emrah Mastering RabbitMQ Ayanoglu, Yusuf Aytas, Dotan Nahum
2016
Martin Toshev
Learning RabbitMQ
2015
David Dossot
RabbitMQ Essentials
2014
Sigismondo RabbitMQ Cookbook Boschi, Gabriele Santomaggio
2013
Autor(en)
Titel
Erscheinungsjahr
Alvaro Videla, Jason J. W. Williams
RabbitMQ in Action – Distributed Messaging for Everyone
2012
Gregor Hohpe, Bobby Woolf
Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions
2003
Tabelle 10.8 Bücher zum Thema Messaging und RabbitMQ
11 Continuous Integration Continuous Integration bezeichnet in der Softwareentwicklung das fortlaufende Integrieren von einzelnen Komponenten einer Anwendung, mit dem Ziel, die Qualität der Software zu erhöhen. Wie das für JavaScriptProjekte funktioniert, zeigt dieses Kapitel. Continuous Integration (kurz CI) kann bei Softwareprojekten dabei helfen, die Qualität der Software zu steigern: Werden Änderungen in das jeweilige Versionskontrollsystem (Git, Subversion etc.) »committed«, führt das CI-System automatisch (»kontinuierlich«) bestimmte Prozesse aus, wie beispielsweise das Kompilieren von Quelltext, das Ausführen von Unit- oder Integrationstests, das Überprüfen der Codequalität oder die Ermittlung der Testabdeckung (Abbildung 11.1). Schlagen dabei einzelne Prozesse bzw. Tests fehl, wird der Entwickler, der den jeweiligen Commit durchgeführt hat (oder beliebige andere Personen), per E-Mail (oder auf anderen Wegen) darüber informiert und hat somit zeitnah eine Rückmeldung. Sind die Tests erfolgreich bzw. kann die Software erfolgreich gebaut werden, kann sie auch direkt ausgeliefert werden (dann spricht man nicht nur von Continuous Integration, sondern von Continuous Deployment, dem kontinuierlichen Bereitstellen von Versionen einer Software).
Abbildung 11.1 Das Prinzip von Continuous Integration
Hinweis In der Praxis kann der dargestellte Prozess auch durchaus mehrstufig aufgebaut und in komplexeren Anwendungen insgesamt auch komplexer sein.
Prinzipiell lassen sich mit CI-Systemen beliebige automatisierte Workflows dieser Art konfigurieren. Das Ziel dabei ist immer, den Prozess des Erstellens von Software ohne zusätzlichen Benutzereingriff zuverlässiger zu machen und Fehler frühzeitig erkennen zu können. Dabei lassen sich die diversen CI-Systeme, die es auf dem Markt gibt, wie etwa Jenkins (https://jenkins.io/), Travis CI (https://travis-ci.org/) oder CircleCI (https://circleci.com/), prinzipiell für beliebige Programmiersprachen verwenden.
11.1 Vorbereitungen Im Folgenden möchte ich Ihnen zeigen, wie sich konkret das CISystem Jenkins für JavaScript- bzw. Node.js-Projekte einsetzen lässt. Damit Sie die Beispiele dabei auch ohne entsprechende Serverinfrastruktur lokal auf dem eigenen Entwicklungsrechner nachvollziehen können, zeige ich Ihnen zunächst, wie Sie Jenkins mit Hilfe von Docker lokal nutzen. Zudem setzen wir einen lokalen Git-Server auf, ebenfalls als Docker-Container, von dem Jenkins dann den Quelltext bezieht. Als Git-System kommt hierbei Gogs (»Go Git Service«, siehe https://gogs.io/) zum Einsatz, ein in Go geschriebener Git-Server inklusive einer GitHub-ähnlichen Oberfläche (dazu gleich mehr).
Hinweis Docker ist eine Software zur Containervirtualisierung: Es ermöglicht, andere Softwarekomponenten – beispielsweise Datenbanksysteme, Message-Broker, Webserver oder eben wie in diesem Kapitel gezeigt, einen Git-Server – in Form sogenannter Docker-Container bereitzustellen und zu starten. Der Vorteil davon ist, dass Sie diese Softwarekomponenten nicht direkt auf dem Host-Rechner, beispielsweise auf Ihrem Entwicklungsrechner, installieren müssen, sondern dass sie innerhalb von Docker als Container laufen. Darüber hinaus können Docker-Konfigurationen (auch: Docker-Images) leicht zwischen Entwicklern ausgetauscht werden, beispielsweise um sicherzustellen, dass alle Entwickler in einem Team mit der gleichen Version und Konfiguration einer Softwarekomponente arbeiten.
11.1.1 Installation von Docker
Die Installation von Docker gestaltet sich für alle gängigen Betriebssysteme sehr einfach, die verschiedenen Installationsanweisungen finden Sie auf der Website von Docker unter https://docs.docker.com/engine/installation/. Im Folgenden führe ich Docker unter macOS aus, wofür sich Docker for Mac anbietet (siehe https://docs.docker. com/engine/installation/mac/), das sich wie für macOS gewohnt relativ einfach per Drag & Drop installieren lässt (siehe Abbildung 11.2) und sich anschließend als Dienst in der Toolbar von macOS einnistet (siehe Abbildung 11.3 und Abbildung 11.4).
Abbildung 11.2 Installation von Docker unter macOS
Abbildung 11.3 Docker startet als Dienst ...
Abbildung 11.4 ... und steht anschließend in der Toolbar zur Verfügung.
11.1.2 Installation des Git-Servers Gogs
Die Installation des Git-Servers Gogs bzw. das Erstellen eines entsprechenden Docker-Containers gestaltet sich ebenfalls sehr einfach (eine detaillierte Anleitung dazu finden Sie unter https://github.com/gogits/gogs/tree/master/docker). Im Wesentlichen reicht es, die im Folgenden beschriebenen Befehle auf der Kommandozeile auszuführen. Zunächst laden Sie das Docker-Image von Gogs herunter: docker pull gogs/gogs
Anschließend erstellen Sie ein lokales Verzeichnis, in dem Gogs seine Daten speichert, sprich die Git-Repositories, Konfigurationsdateien, Logs etc. Prinzipiell kann dies ein beliebiges Verzeichnis sein: mkdir -p /var/gogs
Im nächsten Schritt wird dann basierend auf dem Docker-Image "gogs/gogs" ein neuer Container erstellt und direkt gestartet: docker run --name=gogs -p 10022:22 -p 10080:3000 -v /var/gogs:/data gogs/gogs
Über den Parameter -p (bzw. --publish) werden dabei die Ports 22 und 3000 über die Ports 10022 und 10080 auf dem Host-Rechner zur Verfügung gestellt. Ersterer dient dem SSH-Zugriff auf die GitRepositories, über Letzteren gelangen Sie an die Weboberfläche von Gogs. Über den Parameter -v (bzw. --volume) geben Sie zudem das zuvor auf dem Host-Rechner erstellte Verzeichnis an, in dem Gogs seine Daten speichern soll. Außerdem ist es sinnvoll, dem Container über --name einen Namen zuzuweisen (im Beispiel »gogs«). Dann können Sie den Container – für den Fall, dass er gestoppt wurde – bequem über docker start gogs erneut starten (und nicht etwa über die interne Container-ID). Nach erfolgreicher Konfiguration und erfolgreichem Starten des Docker-Containers sollte nun (auf dem Entwicklungsrechner, dem Host-Rechner) die Weboberfläche von Gogs unter http://localhost:10080 zur Verfügung stehen und Sie mit einem entsprechenden Installationsdialog begrüßen (siehe Abbildung 11.5). In diesem Dialog lassen sich nun verschiedene Konfigurationen angeben, wobei ich für das Beispiel (fast) alle Einstellungen auf den Standardwerten belasse. Das Einzige, was ich der Einfachheit halber anpasse, ist die verwendete Datenbank: Hier ist es für den Moment am einfachsten, SQLite3 statt des vorselektierten MySQL auszuwählen.
Abbildung 11.5 Konfiguration von Gogs
Haben Sie die Einstellungen bestätigt, geht es nun daran, einen neuen Git-Nutzer einzurichten (Abbildung 11.6) und sich anschließend mit diesem am System anzumelden (Abbildung 11.7).
Abbildung 11.6 Registrieren eines Git-Accounts
Abbildung 11.7 Anmelden über den Git-Account
11.1.3 Anlegen eines Git-Repositorys
Die in Gogs vorhandenen Funktionalitäten dürften GitHub-Nutzern bereits sehr bekannt vorkommen, und die Verwendung von Gogs sollte ihnen intuitiv von der Hand gehen. Um beispielsweise ein neues Repository zu erzeugen, wählen Sie einfach oben rechts im Menü den Eintrag Neues Repository. In der anschließenden Eingabemaske (Abbildung 11.8) wählen Sie den Nutzer, dem das Repository zugeordnet werden soll, vergeben einen Namen, stellen die Sichtbarkeit ein (privat oder öffentlich) und fügen optional eine Beschreibung, eine .gitignore-Vorlage, eine Lizenz und eine ReadmeDatei hinzu (damit die Vorlagen auch wirklich verwendet werden, müssen Sie zusätzlich die Checkbox Repository mit ausgewählten Dateien und Vorlagen initialisieren aktivieren).
Abbildung 11.8 Anlegen eines neuen Git-Repositorys
11.1.4 Hinzufügen eines SSH-Schlüssels
Zu guter Letzt müssen Sie noch einen SSH-Schlüssel hinzufügen, um von Ihrem Rechner auf das angelegte Repository auch zugreifen zu können. Unter Linux und Mac erledigen Sie dies beispielsweise über das Kommandozeilen-Tool ssh-keygen: ssh-keygen -t rsa
Standardmäßig wird der hierdurch erzeugte Schlüssel im Verzeichnis ~/.ssh/id_rsa.pub gespeichert, dessen Inhalt Sie wie folgt ausgeben: cat ~/.ssh/id_rsa.pub
Über das Menü ganz rechts oben in der Gogs-Weboberfläche gelangen Sie über Ihre Einstellungen • SSH-Schlüssel in die Verwaltung der SSH-Schlüssel und können dort den eben generierten Schlüssel hinzufügen (Abbildung 11.9). Anschließend ist das Repository
einsatzbereit und kann beispielsweise wie folgt auf den lokalen Entwicklungsrechner geklont werden: git clone http://localhost:10080/cleancoderocker/helloworld.git
Abbildung 11.9 Erzeugen eines neuen SSH-Schlüssels
11.1.5 Anlegen des Beispielprojekts
Damit ist auf Git-Seite eigentlich alles vorbereitet. Bevor es nun an die Installation von Jenkins geht, sei aber schnell noch ein kleines Miniprojekt hinzugefügt, das der Einfachheit halber lediglich aus einer einzelnen Klasse (Listing 11.1) und einem dazugehörigen UnitTest (Listing 11.2) besteht. module.exports = class Calculator {
static sum(x, y) {
return x + y;
}
static subtract(x, y) {
return x - y;
}
static product(x, y) {
return x * y;
}
static divide(x, y) {
return x / y;
}
}
Listing 11.1 Eine einfache Klasse für das CI-Beispiel const assert = require('assert');
const Calculator = require('../lib/Calculator');
describe('Calculator', () =>
{
it('should calculate the sum', () =>
{
let result = Calculator.sum(5, 6);
assert.equal(result, 11);
});
it('should calculate the product', () =>
{
let result = Calculator.product(5, 6);
assert.equal(result, 30);
});
});
Listing 11.2 Unit-Test für das CI-Beispiel
Die Git-Befehle zum Initialisieren des Projekts lauten dabei wie folgt: git init git add lib/Calculator.js git add test/CalculatorTest.js git commit -m "Added example class and unit test" git push -u origin master
11.2 Jenkins Jenkins (https://jenkins.io/) ist eines der bekannteren CI-Systeme und steht für verschiedene Betriebssysteme und in verschiedenen Formen zur Verfügung (Abbildung 11.10), unter anderem als DockerImage (https://hub.docker.com/r/jenkinsci/jenkins/).
Abbildung 11.10 Jenkins steht in verschiedenen Varianten zur Verfügung.
11.2.1 Installation
Bei der Installation bietet es sich an, wie unter https://www.cloudbees.com/blog/get-started-jenkins-20-docker beschrieben, Jenkins auf zwei Docker-Images zu verteilen: Ein Image enthält die eigentliche Jenkins-Installation inklusive dem CI-Server, das andere Image enthält die Konfigurationsdaten für die einzelnen Jenkins-Jobs. Durch diese Trennung ist es prinzipiell auch möglich, dieselben Job-Konfigurationen mit verschiedenen JenkinsInstallationen zu verwenden. Folglich benötigen Sie im nächsten Schritt zwei Dockerfiles benötigt: Dockerfile-data enthält den in Listing 11.3 gezeigten Code,
Dockerfile den in Listing 11.4 gezeigten Code. FROM debian:jessie
RUN useradd -d "/var/jenkins_home" -u 1000 -m -s /bin/bash jenkins
RUN mkdir -p /var/log/jenkins
RUN chown -R jenkins:jenkins /var/log/jenkins
VOLUME ["/var/log/jenkins", "/var/jenkins_home"]
USER jenkins
CMD ["echo", "Daten-Container Jenkins"]
Listing 11.3 Dockerfile für das Datenverzeichnis von Jenkins FROM jenkinsci/jenkins
USER root
RUN curl -sL https://deb.nodesource.com/setup_7.x | bash -
RUN apt-get install -y nodejs
RUN mkdir /var/log/jenkins
RUN mkdir /var/cache/jenkins
RUN chown -R jenkins:jenkins /var/log/jenkins
RUN chown -R jenkins:jenkins /var/cache/jenkins
USER jenkins
ENV JAVA_OPTS="-Xmx4096m"
Listing 11.4 Dockerfile für Jenkins
Basierend auf diesen Images erstellen Sie nun über folgende beiden Befehle die zwei Container: docker build -t jenkins-data -f Dockerfile-data . docker build -t jenkins-master .
Anschießend lassen sich die beiden Container wie folgt starten: docker run --name=jenkins-data jenkins-data docker run -p 8080:8080 -p 50000:50000 --name=jenkins-master -volumes-from=jenkins-data -d jenkins2
Sind die beiden Container gestartet, steht die Weboberfläche von Jenkins unter http://localhost:8080 zur Verfügung. Standardmäßig ist dabei ein Nutzer mit dem Nutzernamen »admin« registriert, das entsprechende Passwort zur Eingabe in die Eingangsmaske
(Abbildung 11.11) können Sie sich über folgenden Befehl aus dem Jenkins-Container ausgeben lassen: docker exec jenkins-master cat /var/jenkins_home/secrets/initialAdminPassword
Abbildung 11.11 Vor der ersten Benutzung müssen Sie das Admin-Passwort eingeben.
Anschließend haben Sie die Möglichkeit, entweder eine Vorauswahl populärer Jenkins-Plugins zu installieren oder sich selbst eine Auswahl zusammenzustellen (Abbildung 11.12). Für den Moment reicht Ersteres (Abbildung 11.13), weitere Plugins werden wir in wenigen Momenten manuell nachinstallieren.
Abbildung 11.12 Auswahl der zu installierenden Plugins
Abbildung 11.13 Installation der Standard-Plugins
Nach Installation der Standard-Plugins haben Sie im nächsten Schritt die Möglichkeit, einen weiteren Administrator-Nutzer anzulegen (Abbildung 11.14). Für den Moment soll aber auch hier der Standardnutzer »admin« ausreichen, so dass Sie entsprechend Continue as admin auswählen (übrigens fällt hier und an vielen anderen Stellen in der Weboberfläche etwas negativ auf, dass die Sprache in Jenkins nicht konsequent immer Englisch bzw. immer Deutsch ist).
Abbildung 11.14 Optionales Anlegen eines weiteren Admin-Accounts
Damit ist die Installation abgeschlossen (Abbildung 11.15), und Sie haben Zugriff auf die Weboberfläche (Abbildung 11.16).
Abbildung 11.15 Bestätigung über die erfolgreiche Konfiguration
Abbildung 11.16 Willkommensseite von Jenkins
11.2.2 Installieren von Plugins
Bevor ich im nächsten Schritt endlich zeigen werde, wie Sie einzelne Jenkins-Jobs für Node.js- bzw. JavaScript-Projekte anlegen und konfigurieren, müssen Sie noch einige Plugins manuell nachinstallieren. Die entsprechende Maske zur Verwaltung der Plugins finden Sie unter Jenkins • Jenkins verwalten • Plugins verwalten.
Unter dem Reiter Verfügbar und mit Hilfe des dortigen Suchfeldes (Abbildung 11.17) installieren Sie nun mindestens folgende Plugins aus der Plugin-Registry: TAP Plugin: zur Darstellung von Testergebnissen Checkstyle Plugin: zur Darstellung von Codequalität bzw. Codestyle NodeJS Plugin: zur Konfiguration von Node.js Darüber hinaus müssen Sie ein weiteres Plugin, das Clover-Plugin, installieren, das zur Darstellung von Code-Coverage-Berichten dient. Seine Installation geschieht allerdings nicht wie bei den anderen Plugins über die globale Plugin-Registry, weil die Version dort nicht mit der aktuellen Jenkins-Version funktioniert. Laden Sie stattdessen die entsprechende Datei von http://repo.jenkinsci.org/releases/org/jenkins-ci/plugins/clover/4.6.0/clover-4.6.0.hpi herunter, und installieren Sie sie anschließend über den Reiter Erweiterte Einstellungen unter dem Bereich Plugin hochladen manuell (Abbildung 11.18).
Abbildung 11.17 Eingabemaske für die Installation weiterer Plugins
Abbildung 11.18 Manuelles Hochladen von Jenkins-Plugins
Hinweis Übrigens: wer sich an dem teilweise etwas veralteten Design von Jenkins stört, kann sich optional auch das Simple-Theme-Plugin installieren, das in dieser Hinsicht etwas nachbessert (siehe Tabelle 11.1). Name
URL
CheckstylePlugin
https://wiki.jenkinsci.org/display/JENKINS/Checkstyle+Plugin
Clover-Plugin
https://wiki.jenkinsci.org/display/JENKINS/Clover+ Plugin
Name
URL
NodeJS-Plugin https://wiki.jenkinsci.org/display/JENKINS/NodeJS+ Plugin SimpleTheme-Plugin
https://wiki.jenkinsci.org/display/JENKINS/Simple+ Theme+Plugin
TAP-Plugin
https://wiki.jenkinsci.org/display/JENKINS/TAP+Plugin
Tabelle 11.1 Auswahl an nützlichen Jenkins-Plugins
11.2.3 Anlegen von Jobs
Nachdem Jenkins nun so weit für den ersten Einsatz installiert und konfiguriert ist, können Sie als Nächstes einen neuen Job anlegen.
Abbildung 11.19 Erzeugen eines neuen Jobs
Dies geschieht entweder (sofern noch keine Jobs vorhanden sind) über den Link Legen Sie einen neuen Job an, um loszulegen. oder über den Menüeintrag Element anlegen. Anschließend öffnet sich die in Abbildung 11.19 gezeigte Eingabemaske, über die Sie den Namen des
Jobs sowie dessen Typ auswählen. Für Node.js-Projekte eignet sich hierbei die Auswahl »Free Style«-Softwareprojekt bauen. Allgemeine Konfiguration
Die Konfiguration des Jobs gestaltet sich über verschiedene Bereiche. Ergänzen Sie unter dem Reiter General (Abbildung 11.20) beispielsweise eine Beschreibung des Jobs, geben Sie an, ob alte Builds verworfen werden sollen, und einiges mehr.
Abbildung 11.20 Allgemeine Konfiguration eines Jenkins-Jobs
Angabe des Versionskontrollsystems
Im Reiter Source-Code-Management definieren Sie das Versionskontrollsystem, von dem Jenkins seine Daten beziehen soll (Abbildung 11.21). Zur Auswahl stehen hier standardmäßig Git und Subversion, wobei wir für das in diesem Kapitel beschriebene Beispiel Ersteres verwenden. Unter Repository URL geben Sie die URL zu dem GitRepository ein. Allerdings können Sie hier nicht die »localhost«Adresse verwenden, die Sie zuvor beim Klonen des Repositorys benutzt haben. Der Grund: für den Docker-Container, in dem
Jenkins läuft, ist Gogs (das ja ebenfalls in einem anderen DockerContainer läuft) nicht über »localhost« sichtbar. Stattdessen müssen Sie in diesem Fall die interne Adresse von Docker verwenden, die Sie wie folgt herausfinden: docker run -i -t ubuntu /bin/bash
apt-get update
apt-get install -y netstat
netstat -nr | grep '^0\.0\.0\.0' | awk '{print $2}'
Abbildung 11.21 Angabe des Git-Repositorys für den Jenkins-Job
Definition von Build-Auslösern
Unter dem Reiter Build-Auslöser lässt sich konfigurieren, welche Ereignisse den Jenkins-Job auslösen (Abbildung 11.22). Dies kann durch ein externes Skript geschehen (Auswahl 1), nach einem anderen Jenkins-Job (Auswahl 2), zeitgesteuert (Auswahl 3), bei neuen Commits (Auswahl 4) oder durch eine zeitgesteuerte Prüfung, ob neue Commits vorhanden sind (Auswahl 5). Bei den zeitgesteuerten Varianten geben Sie unter Zeitplan ein Muster in Cron-Syntax an: Das Pattern H * * * * besagt
beispielsweise, dass einmal pro Stunde nach neuen Commits geprüft werden soll. Zusätzlich ist es aber auch möglich, jeden Job manuell über die Weboberfläche von Jenkins zu starten, was insbesondere zu Testzwecken ganz hilfreich ist. Ebenfalls hilfreich ist dabei die Tatsache, dass sich jederzeit der Output einsehen lässt, den der jeweilige Job auf der Kommandozeile erzeugt (Abbildung 11.23).
Abbildung 11.22 Festlegen der Build-Auslöser
Abbildung 11.23 Die Ausgabe eines Jobs können Sie jederzeit einsehen.
Definition des Buildverfahrens
Die eigentliche Konfiguration der Ablauflogik eines Jenkins-Jobs geschieht über den Reiter Buildverfahren (Abbildung 11.24). Hier lassen sich unter Shell ausführen beliebige Shell-Skripte ausführen.
Abbildung 11.24 Festlegen der genauen Build-Schritte
Im Falle von Node.js-Projekten liegt es dabei nahe, die konkreten Build-Schritte über NPM anzustoßen: npm cache clean
npm install
npm run test-ci
npm run coverage-ci
npm run eslint-ci
Mit den obigen Befehlen leeren Sie den Cache von NPM (Zeile 1), installieren die Abhängigkeiten vom Projekt (Zeile 2), führen die Unit-Tests aus (Zeile 3) und ermitteln die Testabdeckung (Zeile 4) sowie anschließend die Codequalität (Zeile 5). Für die letzten drei Schritte sind dabei entsprechende Befehle im scripts-Bereich in der Datei package.json angegeben (siehe Listing 11.5), über die wiederum verschiedene Gulp-Tasks ausgeführt werden. Im Einzelnen sind dies die in Listing 11.6 gezeigten Tasks »test«, »testcoverage« und »lint«.
{
"name": "hello-world",
"version": "1.0.0",
"description": "Beispielprojekt zur Demonstration von Continuous Integration für JavaScript-Projekte",
"main": "index.js",
"scripts": {
"test": "gulp test",
"test-ci": "gulp test --silent > test.tap",
"coverage": "gulp test-coverage",
"coverage-ci": "gulp test-coverage",
"eslint": "gulp lint",
"eslint-ci": "gulp lint --silent > checkstyle-result.xml"
},
"repository": {
"type": "git",
"url": "http://localhost:10080/cleancoderocker/hello-world.git"
},
"keywords": [
"javascript",
"ci"
],
"author": "Philip Ackermann",
"license": "MIT",
"devDependencies": {
"gulp": "^3.9.1",
"gulp-eslint": "^3.0.1",
"gulp-istanbul": "^1.1.1",
"gulp-mocha": "^3.0.1",
"istanbul": "^0.4.5",
"mocha": "^3.2.0"
}
}
Listing 11.5 Die Datei »package.json« für das CI-Beispiel 'use strict';
const gulp = require('gulp');
const mocha = require('gulp-mocha');
const istanbul = require('gulp-istanbul');
const eslint = require('gulp-eslint');
gulp.task('test', () =>
gulp
.src('./test/*.js', {read: false})
.pipe(mocha({reporter: 'tap', timeout: 5000}))
);
gulp.task('pre-test-coverage', () => {
return gulp.src(['./lib/**/*.js'])
.pipe(istanbul())
.pipe(istanbul.hookRequire());
});
gulp.task('test-coverage', ['pre-test-coverage'], () => {
return gulp.src(['./test/*.js'])
.pipe(mocha({timeout: 5000}))
.pipe(istanbul.writeReports({
dir: './coverage',
reporters: [ 'lcov', 'clover', 'json', 'text', 'text-summary'],
}))
.pipe(istanbul.enforceThresholds({ thresholds: { global: 50 }
}));
});
gulp.task('lint', () => {
return gulp.src(['**/*.js','!node_modules/**'])
.pipe(eslint())
.pipe(eslint.format('checkstyle', process.stdout));
});
Listing 11.6 Die Datei »gulpfile.js« für das CI-Beispiel
Generierung von Testergebnissen
Der Task »test« im Gulp-Skript erzeugt die Testergebnisse im TAPFormat, der Befehl gulp test —silent > test.tap sorgt dafür, dass diese Ergebnisse in die Datei test.tap geschrieben werden. Um die Ergebnisse in Jenkins zu integrieren, fügen Sie dort die Post-BuildAktion Publish TAP Results hinzu und geben den Namen der Datei an (Abbildung 11.25). Nach Ausführen des Jobs stehen die Testergebnisse in Jenkins unter TAP Extended Test Results zur Verfügung (Abbildung 11.26).
Abbildung 11.25 Konfiguration der TAP-Darstellung als Post-Build-Aktion
Abbildung 11.26 Darstellung der TAP-Testergebnisse
Generierung von Testabdeckung
Der Task »test-coverage« im Gulp-Skript ermittelt die Testabdeckung und speichert die Ergebnisse direkt im Verzeichnis coverage. Für die Integration in Jenkins müssen Sie dabei unter »reporters« mindestens die Reporter »lcov« und »clover« angeben. Über die Post-Build-Aktion Publish Clover Coverage Report (Abbildung 11.27) fügen Sie das Verzeichnis hinzu, das die Berichte zur Testabdeckung enthält (coverage), sowie den Namen der Datei, die die Ergebnisse im Clover-Format enthält (clover.xml).
Abbildung 11.27 Konfiguration von Clover als Post-Build-Aktion
Nach Ausführen des Jobs können Sie die Ergebnisse unter Clover Summary Report und Clover HTML Report als Übersicht (Abbildung 11.28) bzw. im Detail pro JavaScript-Datei (Abbildung 11.29) einsehen.
Abbildung 11.28 Darstellung der Code-Coverage-Ergebnisse
Abbildung 11.29 Code-Coverage pro Datei
Generierung von Berichten zur Codequalität
Fehlt noch die Ermittlung der Codequalität. Dazu gibt es im GulpSkript einen Task »lint«, das Ergebnis landet in der Datei checkstyleresult.xml. In Jenkins fügen Sie die Post-Build-Aktion Veröffentliche Ergebnisse der Checkstyle-Analyse hinzu (Abbildung 11.30), tragen dort
den Namen der erzeugten XML-Datei ein und erhalten anschließend unter Checkstyle Warnungen einen entsprechenden Report (Abbildung 11.31).
Abbildung 11.30 Konfiguration von Checkstyle als Post-Build-Aktion
Abbildung 11.31 Darstellung der Checkstyle-Ergebnisse
Abbildung 11.32 Gesamtübersicht des Jenkins-Jobs
11.2.4 Jenkins mit Node.js steuern
Die Konfigurationen für einzelne Jobs speichert Jenkins intern im XML-Format. Einen Ausschnitt einer solchen Konfiguration zeigt Listing 11.7. Über eine von Jenkins bereitgestellte API (https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API) lassen sich existierende Konfigurationen aktualisieren oder auch Konfigurationen für neue Jobs erstellen. Dies bietet sich insbesondere dann an, wenn Sie viele Jobs auf einmal anlegen oder verändern und nicht jede Änderung über die Weboberfläche durchführen möchten.
Beispielprojekt zur Demonstration von Continuous Integration für
JavaScript-Projekte
false
2
http://172.17.0.1:10080/cleancoderocker/hello-world.git
12c1e5a2-8876-414e-b98d-ae8112618c68
*/master
false
true
false
false
false
H * * * *
false
false
npm cache clean
npm install
npm run test-ci
npm run coverage-ci
npm run eslint-ci
...
...
...
Listing 11.7 Jenkins-Job-Konfigurationsdatei
Ein praktisches Package für Node.js, das in diesem Zusammenhang einen Blick Wert ist, ist das Modul node-jenkins (https://github.com/silas/node-jenkins). Dabei handelt es sich um ein Plugin für Grunt (für Gulp gibt es leider noch nichts Vergleichbares), über das Sie Jobs anlegen, aktualisieren oder einfach (im XMLFormat) als Backup sichern. Die Verwendung mit Grunt zeigt Listing 11.8. Insgesamt stehen drei Grunt-Tasks zur Verfügung: jenkins-install-jobs: Erstellt ausgehend von den lokalen XMLKonfigurationen in Jenkins neue Jobs oder aktualisiert bestehende Jobs.
jenkins-backup-jobs: Erstellt ausgehend von existierenden Jenkins-Jobs lokal neue XML-Konfigurationen oder aktualisiert bestehende XML-Konfigurationen. jenkins-verify-jobs: Prüft, ob die lokalen XML-Konfigurationen mit den bestehenden Jenkins-Jobs übereinstimmen. 'use strict';
// grunt jenkins-install-jobs: install or update jobs
// grunt jenkins-backup-jobs: backup or update jobs
// grunt jenkins-verify-jobs: look for changes
module.exports = grunt => {
grunt.initConfig({
jenkins: {
serverAddress: 'http://localhost:8081//',
netrcMachine: 'localhost',
netrcLocation: './.netrc'
}
});
grunt.loadNpmTasks('grunt-jenkins');
};
Listing 11.8 Gruntfile für das Steuern von Jenkins
11.3 Alternativen: Travis CI und CircleCI Wer den Aufwand einer Jenkins-Installation scheut und lieber ein Tool nutzen möchte, das mit möglichst wenig Konfigurationsaufwand betrieben wird, der kann auf gehostete Alternativen wie Travis CI (https://travis-ci.org/ für Open-SourceProjekte oder. https://travis-ci.com/ für kommerzielle Projekte, Abbildung 11.33) und CircleCI (https://circleci.com/, Abbildung 11.34) zurückgreifen. Wer seine Projekte ohnehin bei GitHub hostet, dürfte mit Travis CI gut zurechtkommen. Für Open-Source-Projekte kostenlos, für kommerzielle Projekte in verschiedenen PricingModellen vorhanden, lässt es sich relativ schnell mit den GitHubRepositories verknüpfen. Nachteil der gehosteten Alternativen: In den meisten Fällen stehflichtige) Dienste zur Verfügung. Bei Jenkins sind Sie hier deutlich unabhängiger und flexibler.
Abbildung 11.33 Weboberfläche von Travis CI
Abbildung 11.34 Weboberfläche von CircleCI
11.4 Zusammenfassung und Ausblick Continuous Integration ist ein wichtiges Hilfsmittel für die professionelle Softwareentwicklung. Folgende Auflistung enthält die wichtigsten Punkte aus diesem Kapitel: Continuous Integration bezeichnet bei der Softwareentwicklung das fortlaufende Integrieren von einzelnen Komponenten einer Anwendung, mit dem Ziel, die Qualität der Software zu erhöhen. Continuous Deployment bezeichnet das fortlaufende Deployen von Softwarekomponenten. CI-Systeme ermöglichen unter anderem das automatische Durchführen von Tests, die Ermittlung der Testabdeckung, das Generieren von Berichten zur Codequalität und viele weitere Schritte, die bei der Entwicklung von Software eine Rolle spielen. Es gibt verschiedene bekannte CI-Systeme: Mit Jenkins sind Sie am flexibelsten, allerdings ist hier auch der Konfigurations- und Wartungsaufwand am höchsten. Wer beides umgehen möchte, kann auf gehostete Alternativen wie Travis CI oder CircleCI zurückgreifen. CI-Systeme ermöglichen das automatische Durchführen von Tests, die Ermittlung der Testabdeckung, das Generieren von Berichten zur Codequalität und vieler weiterer Schritte, die bei der Entwicklung von Software eine Rolle spielen.
Stichwortverzeichnis ↓A ↓B ↓C ↓D ↓E ↓F ↓G ↓H ↓I ↓J ↓K ↓L ↓M ↓N ↓O ↓P ↓Q ↓R ↓S ↓T ↓U ↓V ↓W ↓X ↓Y ↓Z
__proto__ [→ 3.2 Prototypen] @abstract [→ 5.5 Dokumentation] @author [→ 5.5 Dokumentation] @chainable [→ 5.5 Dokumentation] @class [→ 5.5 Dokumentation] @classdesc [→ 5.5 Dokumentation] @constructor [→ 5.5 Dokumentation] @default [→ 5.5 Dokumentation] @deprecated [→ 5.5 Dokumentation] @enum [→ 5.5 Dokumentation] @event [→ 5.5 Dokumentation] @example [→ 5.5 Dokumentation] @fires [→ 5.5 Dokumentation] @method [→ 5.5 Dokumentation] @module [→ 5.5 Dokumentation] @namespace [→ 5.5 Dokumentation]
@param [→ 5.5 Dokumentation] @private [→ 5.5 Dokumentation] @property [→ 5.5 Dokumentation] @protected [→ 5.5 Dokumentation] @readonly [→ 5.5 Dokumentation] @requires [→ 5.5 Dokumentation] @return [→ 5.5 Dokumentation] @returns [→ 5.5 Dokumentation] @since [→ 5.5 Dokumentation] @static [→ 5.5 Dokumentation] @throws [→ 5.5 Dokumentation] @type [→ 5.5 Dokumentation]
A ⇑ Abstract Factory (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Abstrakte Fabrik (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Adapter (Entwurfsmuster) [→ 8.3 Strukturmuster] Advanced Messaging Queuing Protocol [→ 10.2 AMQP] Advice [→ 7.3 Aspektorientierte Programmierung in JavaScript] After Returning Advice [→ 7.3 Aspektorientierte Programmierung in JavaScript]
After Throwing Advice [→ 7.3 Aspektorientierte Programmierung in JavaScript] Ajax [→ 9.3 MVC und MVP in Webanwendungen] AMD [→ 3.8 Emulieren von Modulen] AMQP [→ 10.2 AMQP] Angular [→ 9.5 Komponentenbasierte Architektur] Anker [→ 9.6 Routing] Annotation [→ 7.3 Aspektorientierte Programmierung in JavaScript] Anwendungslogik [→ 9.1 Model View Controller] AOP [→ 7.3 Aspektorientierte Programmierung in JavaScript] Decorators [→ 7.3 Aspektorientierte Programmierung in JavaScript] durch Methodenneudefinition [→ 7.3 Aspektorientierte Programmierung in JavaScript] apply() [→ 2.2 Standardmethoden jeder Funktion] Aptana Studio 3 (IDE) [→ 1.5 Entwicklungsumgebungen] Architektur komponentenbasierte [→ 9.5 Komponentenbasierte Architektur] Around Advice [→ 7.3 Aspektorientierte Programmierung in JavaScript] Array [→ 1.7 Einführung in die Sprache] Array.from() [→ 4.10 Neue Methoden der Standardobjekte]
Array.of() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.copyWithin() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.entries() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.fill() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.filter() [→ 6.4 DOM-Tests] Array.prototype.find() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.findIndex() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.forEach() [→ 2.2 Standardmethoden jeder Funktion] Array.prototype.keys() [→ 4.10 Neue Methoden der Standardobjekte] Array.prototype.map() [→ 2.4 Von der imperativen Programmierung zur funktionalen Programmierung] Array.prototype.reduce() [→ 2.4 Von der imperativen Programmierung zur funktionalen Programmierung] Array.prototype.values() [→ 4.10 Neue Methoden der Standardobjekte] Array-Destructuring [→ 4.5 Mehrfachzuweisungen über Destructuring]
Arrow-Funktion [→ 4.3 Striktere Trennung zwischen Funktionen und Methoden] Arrow-Funktionen [→ 1.7 Einführung in die Sprache] [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] AspectJ [→ 7.3 Aspektorientierte Programmierung in JavaScript] Aspekt [→ 7.3 Aspektorientierte Programmierung in JavaScript] [→ 7.3 Aspektorientierte Programmierung in JavaScript] Aspektorientierte Programmierung [→ 7.3 Aspektorientierte Programmierung in JavaScript] Assertion [→ 6.1 Testgetriebene Entwicklung] Asynchronous JavaScript and XML [→ 9.3 MVC und MVP in Webanwendungen] Asynchronous Module Definition [→ 3.8 Emulieren von Modulen] Ausgabe indirekte [→ 6.2 Test-Doubles]
B ⇑ Babel (Tool) [→ 4.1 Einführung] Backbone.js [→ 9.3 MVC und MVP in Webanwendungen] BDD [→ 6.1 Testgetriebene Entwicklung] Before Advice [→ 7.3 Aspektorientierte Programmierung in JavaScript]
Behavioral Design Patterns [→ 8.1 Einführung] Behaviour Driven Development [→ 6.1 Testgetriebene Entwicklung] Benannter Parameter [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] Benutzerdefiniertes Objekt [→ 3.1 Objekte] bind() [→ 2.2 Standardmethoden jeder Funktion] Blanket.js [→ 6.3 Testabdeckung] Block-Scope [→ 4.2 Block-Scope und Konstanten] [→ 4.2 BlockScope und Konstanten] Boolean [→ 1.7 Einführung in die Sprache] Bridge (Entwurfsmuster) [→ 8.3 Strukturmuster] Browserify [→ 5.7 Package Management und Module Bundling] Builder (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Building [→ 5.8 Building]
C ⇑ call() [→ 2.2 Standardmethoden jeder Funktion] Callback [→ 2.5 Funktionale Techniken und Entwurfsmuster] als Event-Listener [→ 2.5 Funktionale Techniken und Entwurfsmuster] zur asynchronen Programmierung [→ 2.5 Funktionale Techniken und Entwurfsmuster]
Callback-Entwurfsmuster [→ 2.5 Funktionale Techniken und Entwurfsmuster] Callback-Funktion [→ 2.5 Funktionale Techniken und Entwurfsmuster] Callback-Handler [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] [→ 2.5 Funktionale Techniken und Entwurfsmuster] CasperJS [→ 6.5 Funktionstests] CBA [→ 9.5 Komponentenbasierte Architektur] Chain of Responsibility (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Chakra [→ 1.4 Laufzeitumgebungen] Chrome Developer Tools [→ 1.6 Debugging-Tools] CircleCI (Tool) [→ 11.3 Alternativen: Travis CI und CircleCI] CI-System [→ 6.5 Funktionstests] [→ 11.1 Vorbereitungen] Clean Code [→ 7.1 SOLID] Closure [→ 2.5 Funktionale Techniken und Entwurfsmuster] [→ 3.4 Datenkapselung] Closure Linter [→ 5.4 Codequalität] Code Conventions [→ 5.3 Styleguides und Code Conventions] Code Coverage [→ 6.3 Testabdeckung] [→ 6.3 Testabdeckung] Codegenerator [→ 4.1 Einführung] Codepen (Online-Editor) [→ 1.5 Entwicklungsumgebungen]
Codequalität [→ 5.1 Einleitung] [→ 5.4 Codequalität] CoffeeScript [→ 4.1 Einführung] Command (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Command-Binding unidirektionales [→ 9.4 Model View ViewModel] CommonJS [→ 3.8 Emulieren von Modulen] Component-Based Architecture [→ 9.5 Komponentenbasierte Architektur] Composite (Entwurfsmuster) [→ 8.3 Strukturmuster] Composition over Inheritance [→ 8.3 Strukturmuster] [→ 8.4 Verhaltensmuster] console [→ 1.6 Debugging-Tools] const [→ 4.2 Block-Scope und Konstanten] constructor [→ 3.2 Prototypen] constructor() [→ 3.1 Objekte] Containervirtualisierung [→ 11.1 Vorbereitungen] Continuous Deployment [→ 11.1 Vorbereitungen] Continuous Integration [→ 11.1 Vorbereitungen] Continuous Integration System [→ 6.5 Funktionstests] Controller [→ 9.1 Model View Controller] Creational Design Patterns [→ 8.1 Einführung]
Cross-Cutting Concerns [→ 7.3 Aspektorientierte Programmierung in JavaScript] Currying [→ 2.5 Funktionale Techniken und Entwurfsmuster]
D ⇑ Data-Binding [→ 9.1 Model View Controller] bidirektionales [→ 9.4 Model View ViewModel] unidirektionales [→ 9.4 Model View ViewModel] Datenhaltung [→ 9.1 Model View Controller] Datenkapselung [→ 3.4 Datenkapselung] Decorator (Entwurfsmuster) [→ 8.3 Strukturmuster] Decorator (Sprachfeature) [→ 7.3 Aspektorientierte Programmierung in JavaScript] Definition von Methoden [→ 4.3 Striktere Trennung zwischen Funktionen und Methoden] Dependency-Inversion-Prinzip [→ 7.1 SOLID] Dependent-On Components [→ 6.2 Test-Doubles] Destructuring [→ 4.5 Mehrfachzuweisungen über Destructuring] DOC [→ 6.2 Test-Doubles] Docker [→ 11.1 Vorbereitungen] Container [→ 11.1 Vorbereitungen] Image [→ 11.1 Vorbereitungen] Installation [→ 11.1 Vorbereitungen]
Document Object Model [→ 6.1 Testgetriebene Entwicklung] Dokumentation [→ 5.1 Einleitung] [→ 5.5 Dokumentation] DOM [→ 6.1 Testgetriebene Entwicklung] Dragonfly [→ 1.6 Debugging-Tools] Duck-Typing [→ 3.6 Emulieren von Interfaces] Dyn.js [→ 1.4 Laufzeitumgebungen] Dynamische Anzahl an Funktionsparametern emulieren [→ 4.4 Flexiblerer Umgang mit Funktionsparametern]
E ⇑ ECMA [→ 1.2 Entstehung und Historie] ECMAScript [→ 1.2 Entstehung und Historie] Eigenschaft öffentliche [→ 3.4 Datenkapselung] private [→ 3.4 Datenkapselung] statische [→ 3.5 Emulieren von statischen Eigenschaften und statischen Methoden] Eingabe indirekte [→ 6.2 Test-Doubles] Elternklasse [→ 3.2 Prototypen] Emscripten [→ 4.1 Einführung] Entwicklung verhaltensgetriebene [→ 6.1 Testgetriebene Entwicklung]
Ereignisgesteuerte Programmierung [→ 8.4 Verhaltensmuster] Erzeugungsmuster [→ 8.1 Einführung] ES6 [→ 4.1 Einführung] ES2015 [→ 4.1 Einführung] ESLint [→ 5.4 Codequalität] European Computer Manufacturers Asssociation [→ 1.2 Entstehung und Historie] eval() [→ 5.3 Styleguides und Code Conventions] Event-Bubbling [→ 8.4 Verhaltensmuster] Event-Handler [→ 8.4 Verhaltensmuster] Event-Listener [→ 8.4 Verhaltensmuster] Exercise-Phase (Unit-Testing) [→ 6.1 Testgetriebene Entwicklung] export [→ 3.9 Modulsyntax] extends [→ 3.3 Vererbung] Extreme Programming [→ 6.1 Testgetriebene Entwicklung] Extrinsischer Zustand [→ 8.3 Strukturmuster]
F ⇑ Fabrikmethode (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Facade (Entwurfsmuster) [→ 8.3 Strukturmuster] Factory Method (Entwurfsmuster) [→ 8.2 Erzeugungsmuster]
First In, First Out [→ 10.2 AMQP] First-Class Function [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] First-Class-Objekt [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Fluent API [→ 7.2 Fluent APIs] asynchrone [→ 7.2 Fluent APIs] synchrone [→ 7.2 Fluent APIs] Flyweight (Entwurfsmuster) [→ 8.3 Strukturmuster] for...in-Schleife [→ 1.7 Einführung in die Sprache] for...of-Schleife [→ 1.7 Einführung in die Sprache] for-of-Schleife [→ 4.11 Sonstiges neue Features] function [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Function Borrowing [→ 2.2 Standardmethoden jeder Funktion] Function-Level-Scope [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Funktion [→ 1.7 Einführung in die Sprache] als Funktionsparameter verwenden [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] als Objektmethoden definieren [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] als Rückgabewert verwenden [→ 2.1 Die Besonderheiten von Funktionen in JavaScript]
Ausführungskontext [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Eigenschaften [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] erster Klasse [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] höherer Ordnung [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] in Arrays verwenden [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] innerhalb von Funktionen definieren [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Methoden [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] [→ 2.2 Standardmethoden jeder Funktion] Variablen zuweisen [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] variadische [→ 2.2 Standardmethoden jeder Funktion] Funktionale Programmierung [→ 2.3 Einführung in die funktionale Programmierung] Eigenschaften [→ 2.3 Einführung in die funktionale Programmierung] Unterschied zur imperativen Programmierung [→ 2.3 Einführung in die funktionale Programmierung] Unterschied zur objektorientierten Programmierung [→ 2.3 Einführung in die funktionale Programmierung] Funktionale reaktive Programmierung [→ 2.6 Funktionale reaktive Programmierung] [→ 2.6 Funktionale reaktive Programmierung]
Funktionsanweisung [→ 1.7 Einführung in die Sprache] [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Funktionsausdruck [→ 1.7 Einführung in die Sprache] [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Funktionstest [→ 6.5 Funktionstests]
G ⇑ Gang of Four [→ 8.1 Einführung] Generator [→ 4.6 Iteratoren und Generatoren] Parameter [→ 4.6 Iteratoren und Generatoren] unendlicher [→ 4.6 Iteratoren und Generatoren] Generatorfunktion [→ 4.6 Iteratoren und Generatoren] Geschäftslogik [→ 9.1 Model View Controller] Globale Variable [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Globales Objekt [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] GoF-Entwurfsmuster [→ 8.1 Einführung] Gogs Anlegen eines Git-Repositories [→ 11.1 Vorbereitungen] Installation [→ 11.1 Vorbereitungen] Google Closure Compiler [→ 5.6 Konkatenation, Minification und Obfuscation] Google Traceur (Tool) [→ 4.1 Einführung]
Google Web Toolkit [→ 4.1 Einführung] Grafische Oberfläche [→ 9.1 Model View Controller] Graphical User Interface [→ 9.1 Model View Controller] Grunt (Tool) [→ 5.8 Building] Plugins [→ 5.8 Building] GUI [→ 9.1 Model View Controller] Gulp JS (Tool) [→ 5.8 Building]
H ⇑ Hashbang-URL [→ 9.6 Routing] Headless Browser Testing [→ 6.5 Funktionstests] Higher-Order Function [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] History-API [→ 9.6 Routing] Hoisting [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Host-Objekt [→ 3.1 Objekte] HTML-Fixture [→ 6.4 DOM-Tests] HTML-Template [→ 9.3 MVC und MVP in Webanwendungen] HTTP-Anfrage [→ 9.3 MVC und MVP in Webanwendungen] HTTP-Antwort [→ 9.3 MVC und MVP in Webanwendungen] HTTP-Request [→ 9.3 MVC und MVP in Webanwendungen]
HTTP-Response [→ 9.3 MVC und MVP in Webanwendungen]
I ⇑ IDE [→ 1.5 Entwicklungsumgebungen] IIFE [→ 2.5 Funktionale Techniken und Entwurfsmuster] [→ 3.4 Datenkapselung] Immediately Invoked Function Expression [→ 2.5 Funktionale Techniken und Entwurfsmuster] import [→ 3.9 Modulsyntax] Indirekte Ausgabe [→ 6.2 Test-Doubles] Indirekte Eingabe [→ 6.2 Test-Doubles] Instanzmethode [→ 3.4 Datenkapselung] Integrated Development Environment [→ 1.5 Entwicklungsumgebungen] IntelliJ WebStorm (IDE) [→ 1.5 Entwicklungsumgebungen] Interface [→ 3.6 Emulieren von Interfaces] Interface-Segregation-Prinzip [→ 7.1 SOLID] Interpreter [→ 1.4 Laufzeitumgebungen] Interpreter (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Intrinsischer Zustand [→ 8.3 Strukturmuster] Iterator [→ 4.6 Iteratoren und Generatoren] Iterator (Entwurfsmuster) [→ 8.4 Verhaltensmuster]
J ⇑ JägerMonkey [→ 1.4 Laufzeitumgebungen] JavaScript Compressor [→ 5.6 Konkatenation, Minification und Obfuscation] JavaScript Object Notation [→ 1.3 Einsatzgebiete von JavaScript] JavaScriptCore [→ 1.4 Laufzeitumgebungen] Jenkins (Tool) [→ 11.2 Jenkins] Anlegen von Jobs [→ 11.2 Jenkins] Installation [→ 11.2 Jenkins] Installieren von Plugins [→ 11.2 Jenkins] JHipster (Tool) [→ 5.9 Scaffolding] JIT-Kompilierung [→ 1.4 Laufzeitumgebungen] Join Point [→ 7.3 Aspektorientierte Programmierung in JavaScript] jQuery [→ 9.3 MVC und MVP in Webanwendungen] JSBeautifier (Tool) [→ 5.4 Codequalität] JSBin (Online-Editor) [→ 1.5 Entwicklungsumgebungen] JSDoc 3 (Tool) [→ 5.5 Dokumentation] JSFiddle (Online-Editor) [→ 1.5 Entwicklungsumgebungen] JSFuck (Tool) [→ 5.6 Konkatenation, Minification und Obfuscation] JSHint (Tool) [→ 5.4 Codequalität]
JSLint (Tool) [→ 5.4 Codequalität] JSON [→ 1.3 Einsatzgebiete von JavaScript] [→ 3.1 Objekte] JSON.parse() [→ 3.1 Objekte] JSON.stringify() [→ 3.1 Objekte] JSX [→ 9.5 Komponentenbasierte Architektur] Just-in-time-Compiler [→ 1.4 Laufzeitumgebungen]
K ⇑ Knockout.js [→ 9.4 Model View ViewModel] Komponentenbasierte Architektur [→ 9.5 Komponentenbasierte Architektur] Komposition [→ 2.3 Einführung in die funktionale Programmierung] [→ 2.5 Funktionale Techniken und Entwurfsmuster] Konfigurationsobjekt [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] Konkatenation [→ 5.1 Einleitung] [→ 5.6 Konkatenation, Minification und Obfuscation] [→ 5.6 Konkatenation, Minification und Obfuscation] Konstante [→ 1.7 Einführung in die Sprache] [→ 4.2 BlockScope und Konstanten] [→ 4.2 Block-Scope und Konstanten] emulieren [→ 4.2 Block-Scope und Konstanten] [→ 4.2 Block-Scope und Konstanten]
Konstruktorfunktion [→ 1.7 Einführung in die Sprache] [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] [→ 3.1 Objekte] Konstruktorvererbung [→ 3.3 Vererbung] Kopierende Vererbung [→ 3.3 Vererbung]
L ⇑ Last Will and Testament [→ 10.4 MQTT] Lazy Instantiation [→ 2.5 Funktionale Techniken und Entwurfsmuster] [→ 8.2 Erzeugungsmuster] let [→ 4.2 Block-Scope und Konstanten] Linting [→ 5.4 Codequalität] LLVM-Bitcode [→ 4.1 Einführung] Loose Augmentation [→ 3.8 Emulieren von Modulen] Lower-Camel-Case-Schreibweise [→ 3.3 Vererbung]
M ⇑ Math.acosh() [→ 4.10 Neue Methoden der Standardobjekte] Math.asinh() [→ 4.10 Neue Methoden der Standardobjekte] Math.atanh() [→ 4.10 Neue Methoden der Standardobjekte] Math.cbrt() [→ 4.10 Neue Methoden der Standardobjekte] Math.clz32() [→ 4.10 Neue Methoden der Standardobjekte] Math.cosh() [→ 4.10 Neue Methoden der Standardobjekte]
Math.expm1() [→ 4.10 Neue Methoden der Standardobjekte] Math.fround() [→ 4.10 Neue Methoden der Standardobjekte] Math.hypot() [→ 4.10 Neue Methoden der Standardobjekte] Math.imul() [→ 4.10 Neue Methoden der Standardobjekte] Math.log2() [→ 4.10 Neue Methoden der Standardobjekte] Math.log10() [→ 4.10 Neue Methoden der Standardobjekte] Math.log1p() [→ 4.10 Neue Methoden der Standardobjekte] Math.sign() [→ 4.10 Neue Methoden der Standardobjekte] Math.sinh() [→ 4.10 Neue Methoden der Standardobjekte] Math.tanh() [→ 4.10 Neue Methoden der Standardobjekte] Math.trunc() [→ 4.10 Neue Methoden der Standardobjekte] Mediator (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Mehrfachvererbung [→ 3.3 Vererbung] Memento (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Memoization [→ 2.5 Funktionale Techniken und Entwurfsmuster] Message Queue Telemetry Transport [→ 10.4 MQTT] Message-Binding [→ 10.2 AMQP] Message-Broker [→ 10.1 Einführung] Message-Consumer [→ 10.2 AMQP] Message-Producer [→ 10.2 AMQP] Message-Publisher [→ 10.2 AMQP]
Message-Routing [→ 10.1 Einführung] Message-Subscriber [→ 10.2 AMQP] Messaging Binding [→ 10.2 AMQP] [→ 10.2 AMQP] Channel [→ 10.2 AMQP] [→ 10.2 AMQP] Channels [→ 10.2 AMQP] Connection [→ 10.2 AMQP] [→ 10.2 AMQP] Consumer [→ 10.2 AMQP] Direct Exchanges [→ 10.2 AMQP] Exchange [→ 10.2 AMQP] [→ 10.2 AMQP] Fanout Exchange [→ 10.2 AMQP] Header Exchange [→ 10.2 AMQP] Message [→ 10.2 AMQP] Producer [→ 10.2 AMQP] Publisher [→ 10.2 AMQP] Publish-Subscribe Messaging Pattern [→ 10.4 MQTT] QoS [→ 10.4 MQTT] Quality of Service [→ 10.4 MQTT] Queue [→ 10.2 AMQP] [→ 10.2 AMQP] Routing Key [→ 10.2 AMQP] Subscriber [→ 10.2 AMQP] Topic Exchange [→ 10.2 AMQP] Messaging-Protokolle [→ 10.1 Einführung] Messaging-Systeme [→ 10.1 Einführung]
Method Borrowing [→ 2.2 Standardmethoden jeder Funktion] Methode [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] definieren [→ 4.3 Striktere Trennung zwischen Funktionen und Methoden] nichtprivilegierte öffentliche [→ 3.4 Datenkapselung] private [→ 3.4 Datenkapselung] privilegierte öffentliche [→ 3.4 Datenkapselung] statische [→ 3.5 Emulieren von statischen Eigenschaften und statischen Methoden] überladen, Alternativen [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Microsoft Edge Developer Tools [→ 1.6 Debugging-Tools] Minification [→ 5.1 Einleitung] [→ 5.6 Konkatenation, Minification und Obfuscation] [→ 5.6 Konkatenation, Minification und Obfuscation] Minifizierung [→ 5.1 Einleitung] [→ 5.6 Konkatenation, Minification und Obfuscation] [→ 5.6 Konkatenation, Minification und Obfuscation] Mixin [→ 3.3 Vererbung] mocha (Tool) [→ 6.1 Testgetriebene Entwicklung] Mock [→ 6.2 Test-Doubles] Model [→ 9.1 Model View Controller] Model View Controller [→ 9.1 Model View Controller] Model View Presenter [→ 9.2 Model View Presenter]
Model View ViewModel [→ 9.4 Model View ViewModel] Modul [→ 3.9 Modulsyntax] module [→ 3.9 Modulsyntax] Module Augmentation [→ 3.8 Emulieren von Modulen] Module Bundler [→ 5.7 Package Management und Module Bundling] [→ 5.7 Package Management und Module Bundling] Module Bundling [→ 5.7 Package Management und Module Bundling] Modultest [→ 6.1 Testgetriebene Entwicklung] Modus strikter [→ 1.7 Einführung in die Sprache] MQTT [→ 10.4 MQTT] MQTT over WebSockets [→ 10.5 MQTT unter JavaScript] MV* [→ 9.3 MVC und MVP in Webanwendungen] MVC [→ 9.1 Model View Controller] MVP [→ 9.2 Model View Presenter] MVVM [→ 9.4 Model View ViewModel]
N ⇑ Namespace-Entwurfsmuster [→ 3.7 Emulieren von Namespaces] Nashorn [→ 1.4 Laufzeitumgebungen] Natives Objekt [→ 3.1 Objekte]
Nested Namespacing [→ 3.7 Emulieren von Namespaces] NetBeans (IDE) [→ 1.5 Entwicklungsumgebungen] Node.js Package Manager [→ 5.2 Node.js und NPM] [→ 5.7 Package Management und Module Bundling] NPM [→ 5.2 Node.js und NPM] [→ 5.2 Node.js und NPM] [→ 5.7 Package Management und Module Bundling] [→ 5.7 Package Management und Module Bundling] Anwendungen installieren [→ 5.2 Node.js und NPM] installieren [→ 5.2 Node.js und NPM] Package erstellen [→ 5.7 Package Management und Module Bundling] Package installieren [→ 5.7 Package Management und Module Bundling] Package konfigurieren [→ 5.7 Package Management und Module Bundling] Packages verwenden [→ 5.7 Package Management und Module Bundling] Registry [→ 5.2 Node.js und NPM] null [→ 1.7 Einführung in die Sprache] Number.isFinite() [→ 4.10 Neue Methoden der Standardobjekte] Number.isInteger() [→ 4.10 Neue Methoden der Standardobjekte] Number.isNaN() [→ 4.10 Neue Methoden der Standardobjekte] Number.isSaveInteger() [→ 4.10 Neue Methoden der Standardobjekte]
O ⇑ Obfuscation [→ 5.1 Einleitung] [→ 5.6 Konkatenation, Minification und Obfuscation] [→ 5.6 Konkatenation, Minification und Obfuscation] Object Relational Mapping [→ 9.3 MVC und MVP in Webanwendungen] Object.assign() [→ 4.10 Neue Methoden der Standardobjekte] Object.create() [→ 3.1 Objekte] Object.getOwnPropertySymbols() [→ 4.10 Neue Methoden der Standardobjekte] Object.is() [→ 4.10 Neue Methoden der Standardobjekte] Object.setPrototypeOf() [→ 4.10 Neue Methoden der Standardobjekte] Objekt [→ 1.7 Einführung in die Sprache] benutzerdefiniertes [→ 3.1 Objekte] binden [→ 2.2 Standardmethoden jeder Funktion] globales [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Host- [→ 3.1 Objekte] natives [→ 3.1 Objekte] Objekt-Destructuring [→ 4.5 Mehrfachzuweisungen über Destructuring] Objekt-Literal-Schreibweise [→ 3.1 Objekte]
Objektmethode [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Objektvererbung [→ 3.3 Vererbung] Observer [→ 8.4 Verhaltensmuster] Observer (Entwurfsmuster) [→ 2.6 Funktionale reaktive Programmierung] [→ 8.4 Verhaltensmuster] OdinMonkey [→ 1.4 Laufzeitumgebungen] Open-Closed-Prinzip [→ 7.1 SOLID] ORM [→ 9.3 MVC und MVP in Webanwendungen]
P ⇑ Package Management [→ 5.1 Einleitung] [→ 5.7 Package Management und Module Bundling] clientseitiges [→ 5.7 Package Management und Module Bundling] serverseitiges [→ 5.7 Package Management und Module Bundling] package.json [→ 5.7 Package Management und Module Bundling] Parameter benannter [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] Partial Application [→ 2.5 Funktionale Techniken und Entwurfsmuster]
Partielle Auswertung [→ 2.5 Funktionale Techniken und Entwurfsmuster] [→ 2.5 Funktionale Techniken und Entwurfsmuster] mit Platzhaltern [→ 2.5 Funktionale Techniken und Entwurfsmuster] von rechts ausgehend [→ 2.5 Funktionale Techniken und Entwurfsmuster] Persistent Sessions [→ 10.4 MQTT] PhantomJS (Tool) [→ 6.5 Funktionstests] Pointcut [→ 7.3 Aspektorientierte Programmierung in JavaScript] Polyfill [→ 4.1 Einführung] Präprozessor [→ 5.1 Einleitung] Präsentation [→ 9.1 Model View Controller] Presenter [→ 9.2 Model View Presenter] Programmiersprache rein funktionale [→ 2.3 Einführung in die funktionale Programmierung] Programmierung aspektorientierte [→ 7.3 Aspektorientierte Programmierung in JavaScript] funktionale [→ 2.3 Einführung in die funktionale Programmierung] funktionale reaktive [→ 2.6 Funktionale reaktive Programmierung]
reaktive [→ 2.6 Funktionale reaktive Programmierung] Promise [→ 4.7 Promises] Property-Deskriptor [→ 3.1 Objekte] Prototyp [→ 3.2 Prototypen] [→ 3.2 Prototypen] prototype [→ 3.2 Prototypen] Prototype (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Prototypische Vererbung [→ 3.3 Vererbung] Prototypmethode [→ 3.4 Datenkapselung] Proxy (Entwurfsmuster) [→ 4.8 Proxies] [→ 8.3 Strukturmuster] Anwendungsbeispiel Profiler [→ 4.8 Proxies] Anwendungsbeispiel Validierung [→ 4.8 Proxies] emulieren [→ 4.8 Proxies] Pseudoklassische Vererbung [→ 3.3 Vererbung] Publish-Subscribe [→ 8.4 Verhaltensmuster] Publish-Subscribe (Entwurfsmuster) [→ 2.6 Funktionale reaktive Programmierung] Pyramid of Doom [→ 2.5 Funktionale Techniken und Entwurfsmuster] [→ 4.7 Promises]
Q ⇑ QUnit (Tool) [→ 6.1 Testgetriebene Entwicklung]
R ⇑
React [→ 9.5 Komponentenbasierte Architektur] ReactiveX [→ 2.6 Funktionale reaktive Programmierung] Reaktive Programmierung [→ 2.6 Funktionale reaktive Programmierung] Reaktives Manifest [→ 2.6 Funktionale reaktive Programmierung] [→ 2.6 Funktionale reaktive Programmierung] Reaktives System [→ 2.6 Funktionale reaktive Programmierung] ReferenceError [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] RegExp.prototype.match() [→ 4.10 Neue Methoden der Standardobjekte] RegExp.prototype.replace() [→ 4.10 Neue Methoden der Standardobjekte] RegExp.prototype.search() [→ 4.10 Neue Methoden der Standardobjekte] RegExp.prototype.split() [→ 4.10 Neue Methoden der Standardobjekte] Reguläre Ausdrücke [→ 1.7 Einführung in die Sprache] Rein funktionale Programmiersprachen [→ 2.3 Einführung in die funktionale Programmierung] Rekursion [→ 2.5 Funktionale Techniken und Entwurfsmuster]
Representational State Transfer [→ 1.3 Einsatzgebiete von JavaScript] REST [→ 1.3 Einsatzgebiete von JavaScript] Rest-Parameter [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] Retained Message [→ 10.4 MQTT] Revealing-Module-Entwurfsmuster [→ 3.8 Emulieren von Modulen] Rhino [→ 1.4 Laufzeitumgebungen] RIA [→ 1.3 Einsatzgebiete von JavaScript] Rich Internet Application [→ 1.3 Einsatzgebiete von JavaScript] Routing [→ 9.1 Model View Controller] [→ 9.6 Routing] Routing-Engine [→ 9.3 MVC und MVP in Webanwendungen] RxJS [→ 2.6 Funktionale reaktive Programmierung]
S ⇑ Scaffolding [→ 5.1 Einleitung] [→ 5.9 Scaffolding] [→ 5.9 Scaffolding] Scheme [→ 1.2 Entstehung und Historie] Self [→ 1.2 Entstehung und Historie] Self-defining Function [→ 2.5 Funktionale Techniken und Entwurfsmuster] Self-overwriting Function [→ 2.5 Funktionale Techniken und Entwurfsmuster]
Setup-Phase (Unit-Testing) [→ 6.1 Testgetriebene Entwicklung] Sichtbarkeitsbereich [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Single-Page Application [→ 1.3 Einsatzgebiete von JavaScript] [→ 9.3 MVC und MVP in Webanwendungen] Single-Responsibility-Prinzip [→ 7.1 SOLID] Singleton (Entwurfsmuster) [→ 8.2 Erzeugungsmuster] Sinon.JS (Tool) [→ 6.2 Test-Doubles] SOLID-Prinzipien [→ 7.1 SOLID] Source Map [→ 5.6 Konkatenation, Minification und Obfuscation] Source-to-Source-Compiler [→ 5.7 Package Management und Module Bundling] SPA [→ 1.3 Einsatzgebiete von JavaScript] SpiderMonkey [→ 1.4 Laufzeitumgebungen] Spies (Unit-Testing) [→ 6.2 Test-Doubles] Spread-Operator [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] Spring [→ 7.3 Aspektorientierte Programmierung in JavaScript] Standardwerte für Funktionsparameter [→ 4.4 Flexiblerer Umgang mit Funktionsparametern] emulieren [→ 4.4 Flexiblerer Umgang mit Funktionsparametern]
State (Entwurfsmuster) [→ 8.4 Verhaltensmuster] static [→ 3.1 Objekte] [→ 3.5 Emulieren von statischen Eigenschaften und statischen Methoden] Statische Eigenschaft [→ 3.5 Emulieren von statischen Eigenschaften und statischen Methoden] Statische Methode [→ 3.5 Emulieren von statischen Eigenschaften und statischen Methoden] STOMP [→ 10.3 AMQP unter JavaScript] Strategy [→ 7.1 SOLID] Strategy (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Streaming Text Oriented Message Protocol [→ 10.3 AMQP unter JavaScript] Strikter Modus [→ 1.7 Einführung in die Sprache] String.fromCodePoint() [→ 4.10 Neue Methoden der Standardobjekte] String.prototype.codePointAt() [→ 4.10 Neue Methoden der Standardobjekte] String.prototype.endsWith() [→ 4.10 Neue Methoden der Standardobjekte] String.prototype.includes() [→ 4.10 Neue Methoden der Standardobjekte] String.prototype.normalize() [→ 4.10 Neue Methoden der Standardobjekte] String.prototype.repeat() [→ 4.10 Neue Methoden der Standardobjekte]
String.prototype.startsWith() [→ 4.10 Neue Methoden der Standardobjekte] String.raw() [→ 4.10 Neue Methoden der Standardobjekte] Structural Design Patterns [→ 8.1 Einführung] Strukturmuster [→ 8.1 Einführung] Stubs [→ 6.2 Test-Doubles] Styleguide [→ 5.1 Einleitung] [→ 5.3 Styleguides und Code Conventions] Sublime Text 2 (Editor) [→ 1.5 Entwicklungsumgebungen] super [→ 3.3 Vererbung] super() [→ 3.3 Vererbung] Superklasse [→ 3.2 Prototypen] SUT [→ 6.1 Testgetriebene Entwicklung] Symbol [→ 4.11 Sonstiges neue Features] System Under Test [→ 6.1 Testgetriebene Entwicklung]
T ⇑ Tagged Template [→ 4.11 Sonstiges neue Features] TDD [→ 6.1 Testgetriebene Entwicklung] Teardown-Phase (Unit-Testing) [→ 6.1 Testgetriebene Entwicklung] Template Tagged [→ 4.11 Sonstiges neue Features]
Template Method (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Template-Engine [→ 9.3 MVC und MVP in Webanwendungen] Template-String [→ 4.11 Sonstiges neue Features] Templating [→ 9.1 Model View Controller] Test Anything Protocol [→ 6.1 Testgetriebene Entwicklung] Testabdeckung [→ 6.3 Testabdeckung] Test-Coverage [→ 6.3 Testabdeckung] Test-Double [→ 6.2 Test-Doubles] Test-Driven Development [→ 6.1 Testgetriebene Entwicklung] Testen [→ 5.1 Einleitung] Testfall [→ 6.1 Testgetriebene Entwicklung] Test-Fixture [→ 6.1 Testgetriebene Entwicklung] Testgetriebene Entwicklung [→ 6.1 Testgetriebene Entwicklung] [→ 6.1 Testgetriebene Entwicklung] Testkontext [→ 6.1 Testgetriebene Entwicklung] Test-Lifecycle-Methode [→ 6.1 Testgetriebene Entwicklung] Testrunner [→ 6.1 Testgetriebene Entwicklung] Test-Suite [→ 6.1 Testgetriebene Entwicklung] that = this [→ 4.3 Striktere Trennung zwischen Funktionen und Methoden] Thick Client [→ 9.3 MVC und MVP in Webanwendungen] Thin Client [→ 9.3 MVC und MVP in Webanwendungen]
this [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] globale Funktion [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Konstruktorfunktion [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Laufzeitfehler [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Objektmethode [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Tight Augmentation [→ 3.8 Emulieren von Modulen] TraceMonkey [→ 1.4 Laufzeitumgebungen] Transcompiler [→ 5.7 Package Management und Module Bundling] Transpiler [→ 4.1 Einführung] [→ 5.7 Package Management und Module Bundling] Travis CI (Tool) [→ 11.3 Alternativen: Travis CI und CircleCI] TypeScript [→ 3.6 Emulieren von Interfaces] [→ 4.1 Einführung] [→ 4.1 Einführung] Typisierung [→ 1.7 Einführung in die Sprache] dynamische [→ 1.7 Einführung in die Sprache] statische [→ 1.7 Einführung in die Sprache] Typüberprüfung [→ 3.1 Objekte]
U ⇑ Überladen von Methoden
Alternativen [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] UglifyJS2 (Tool) [→ 5.6 Konkatenation, Minification und Obfuscation] UMD [→ 3.8 Emulieren von Modulen] UML [→ 8.1 Einführung] undefined [→ 1.7 Einführung in die Sprache] Underscore.js [→ 9.3 MVC und MVP in Webanwendungen] Unified Modeling Language [→ 8.1 Einführung] Unit-Test [→ 6.1 Testgetriebene Entwicklung] Universal Module Definition [→ 3.8 Emulieren von Modulen] Upper-Camel-Case-Schreibweise [→ 3.1 Objekte] [→ 3.3 Vererbung]
V ⇑ V8 [→ 1.4 Laufzeitumgebungen] var [→ 1.7 Einführung in die Sprache] [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Variable [→ 1.7 Einführung in die Sprache] globale [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Variablen-Hoisting [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] [→ 4.2 Block-Scope und Konstanten]
Variadische Funktion [→ 2.2 Standardmethoden jeder Funktion] Vererbung [→ 3.3 Vererbung] Konstruktor- [→ 3.3 Vererbung] kopierende [→ 3.3 Vererbung] Mehrfach- [→ 3.3 Vererbung] mit Klassensyntax [→ 3.3 Vererbung] prototypische [→ 3.3 Vererbung] pseudoklassische [→ 3.3 Vererbung] Verhaltensgetriebene Entwicklung [→ 6.1 Testgetriebene Entwicklung] Verhaltensmuster [→ 8.1 Einführung] Verify-Phase (Unit-Testing) [→ 6.1 Testgetriebene Entwicklung] View [→ 9.1 Model View Controller] ViewModel [→ 9.4 Model View ViewModel] View-Template [→ 9.3 MVC und MVP in Webanwendungen] Virtuelle Maschine [→ 4.1 Einführung] Visitor (Entwurfsmuster) [→ 8.4 Verhaltensmuster] Visual Studio Code [→ 1.5 Entwicklungsumgebungen] Vue.js [→ 9.5 Komponentenbasierte Architektur]
W ⇑ Weak Map [→ 4.9 Collections]
Weak Set [→ 4.9 Collections] Web Inspector (Tool) [→ 1.6 Debugging-Tools] Webpack (Tool) [→ 5.7 Package Management und Module Bundling] window [→ 2.1 Die Besonderheiten von Funktionen in JavaScript] Wrapper-Objekt [→ 1.7 Einführung in die Sprache]
X ⇑ XMLHttpRequest [→ 1.3 Einsatzgebiete von JavaScript]
Y ⇑ Yarn [→ 5.7 Package Management und Module Bundling] Yeoman (Tool) [→ 5.9 Scaffolding] yield [→ 4.6 Iteratoren und Generatoren] YUI Compressor (Tool) [→ 5.6 Konkatenation, Minification und Obfuscation] YUIDoc (Tool) [→ 5.5 Dokumentation]
Z ⇑ Zahlen [→ 1.7 Einführung in die Sprache] Zeichenketten [→ 1.7 Einführung in die Sprache] Zustand extrinsischer [→ 8.3 Strukturmuster]
intrinsischer [→ 8.3 Strukturmuster] Zuständigkeitskette (Entwurfsmuster) [→ 8.4 Verhaltensmuster]
Rechtliche Hinweise Das vorliegende Werk ist in all seinen Teilen urheberrechtlich geschützt. Weitere Hinweise dazu finden Sie in den Allgemeinen Geschäftsbedingungen des Anbieters, bei dem Sie das Werk erworben haben.
Markenschutz Die in diesem Werk wiedergegebenen Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. können auch ohne besondere Kennzeichnung Marken sein und als solche den gesetzlichen Bestimmungen unterliegen.
Haftungsausschluss Ungeachtet der Sorgfalt, die auf die Erstellung von Text, Abbildungen und Programmen verwendet wurde, können weder Verlag noch Autor, Herausgeber, Übersetzer oder Anbieter für mögliche Fehler und deren Folgen eine juristische Verantwortung oder irgendeine Haftung übernehmen.
Über den Autor
Softwareentwickler Philip Ackermann ist Autor mehrerer Fachbücher und Fachartikel über JavaScript und Java. Er arbeitet als Senior Software Developer in den Bereichen Industrie 4.0 und Internet of Things. Seine Schwerpunkte liegen in der Konzeption und Entwicklung von Node.js- und JEE-Projekten.
Dokumentenarchiv Das Dokumentenarchiv umfasst alle Abbildungen und ggf. Tabellen und Fußnoten dieses E-Books im Überblick.
Abbildung 1.1 Screenshot der WebStorm-IDE
Abbildung 1.2 Screenshot von Visual Studio Code
Abbildung 1.3 Screenshot der Aptana-IDE
Abbildung 1.4 Screenshot des Editors Sublime Text 2
Abbildung 1.5 Screenshot der NetBeans-IDE
Abbildung 1.6 Screenshot des Online-Editors JSFiddle
Abbildung 1.7 Firefox Firebug
Abbildung 1.8 Chrome Developer Tools
Abbildung 2.1 Funktionen werden durch Objekte repräsentiert.
Abbildung 2.2 Funktionen sind first class, sie können beispielsweise Variablen zugewiesen werden.
Abbildung 2.3 »this« wird dynamisch bei Funktionsaufruf ermittelt und an die aufgerufene Funktion übergeben.
Abbildung 2.4 Zusammenhang zwischen funktionaler, reaktiver und funktionaler reaktiver Programmierung
Abbildung 2.5 Die verschiedenen Datenströme für das Zahlenbeispiel
Abbildung 2.6 Die verschiedenen Datenströme für das Drag-and-Drop-Beispiel
Abbildung 3.1 Aufbau eines Objekts in JavaScript
Abbildung 3.2 Aufbau eines Objekts in JavaScript mit Referenz auf anderes Objekt
Abbildung 3.3 Zusammenhang zwischen Objektinstanzen, Prototypen und Konstruktorfunktionen
Abbildung 3.4 Prototypische Vererbung in JavaScript
Abbildung 3.5 Prototypische Vererbung über mehrere Hierarchieebenen
Abbildung 3.6 Begrifflichkeiten der Objektorientierung in JavaScript
Abbildung 3.7 Schritt 1: die Konstruktorfunktion »Animal«
Abbildung 3.8 Schritt 2: Definition der Methode »eat()« am Prototyp
Abbildung 3.9 Schritt 3: die Konstruktorfunktion »Dog«
Abbildung 3.10 Schritt 4: Neudefinition des »Dog«Prototyps
Abbildung 3.11 Schritt 5: Verlinken von Prototyp zu Konstruktorfunktion
Abbildung 3.12 Schritt 6: Definition der Methode »bark()« am Prototyp
Abbildung 3.13 Klassenhierarchie für das Mixin-Beispiel
Abbildung 3.14 Klassenhierarchie bei verschachtelten Mixins
Abbildung 3.15 Anwendung von Mixins bei unterschiedlichen Klassenhierarchien
Abbildung 3.16 Statische Eigenschaften und statische Methoden werden an der Konstruktorfunktion definiert.
Abbildung 5.1 Überblick über den Entwicklungsprozess
Abbildung 5.2 Konfiguration des JSLint-Onlinedienstes (http://www.jslint.com)
Abbildung 5.3 JSLint-Konfiguration in WebStorm
Abbildung 5.4 JSHint-Konfiguration in WebStorm
Abbildung 5.5 Onlineversion von JSBeautifier
Abbildung 5.6 Beispiel für eine mit JSDoc 3 generierte Dokumentation
Abbildung 5.7 Beispiel für eine mit YUIDoc generierte Dokumentation
Abbildung 5.8 Von ESDoc generierte Dokumentation
Abbildung 5.9 Das Prinzip von Webpack
Abbildung 5.10 Projekt-Outline für ein mit dem webappGenerator erstelltes Webprojekt
Abbildung 5.11 Projekt-Outline für ein mit dem angularfullstack-Generator erstelltes Angular-Projekt
Abbildung 5.12 Auswahlmöglichkeiten beim JHipsterGenerator
Abbildung 5.13 Projekt-Outline für ein mit dem JHipsterGenerator erstelltes JHipster-Projekt
Abbildung 6.1 Workflow der testgetriebenen Entwicklung
Abbildung 6.2 Prinzip des Unit-Testens
Abbildung 6.3 Fehlgeschlagener Test in QUnit
Abbildung 6.4 Bestandener Test in QUnit
Abbildung 6.5 Konfiguration einer Run Configuration für mocha in WebStorm
Abbildung 6.6 mocha-Integration in WebStorm
Abbildung 6.7 Das Prinzip von Test-Doubles
Abbildung 6.8 Das Prinzip von Test-Spies
Abbildung 6.9 Das Prinzip von Test-Stubs
Abbildung 6.10 Das Prinzip von Mock-Objekten
Abbildung 6.11 Testabdeckung mit Blanket.js für einen QUnit-basierten Test
Abbildung 6.12 Testabdeckung mit Blanket.js für einen mocha-basierten Test
Abbildung 6.13 Testabdeckung mit Istanbul
Abbildung 7.1 Vermischung von Anwendungslogik und Cross-Cutting Concerns
Abbildung 7.2 AOP extrahiert den Code für Cross-Cutting Concerns
Abbildung 7.3 Begrifflichkeiten in der aspektorientierten Programmierung
Abbildung 8.1 Klassendiagramm für das Factory-MethodEntwurfsmuster
Abbildung 8.2 Klassendiagramm für das Abstract-FactoryEntwurfsmuster
Abbildung 8.3 Klassendiagramm für das SingletonEntwurfsmuster
Abbildung 8.4 Klassendiagramm für das BuilderEntwurfsmuster
Abbildung 8.5 Klassendiagramm für das PrototypeEntwurfsmuster
Abbildung 8.6 Klassendiagramm für das AdapterEntwurfsmuster
Abbildung 8.7 Klassendiagramm für das BridgeEntwurfsmuster
Abbildung 8.8 Klassendiagramm für das CompositeEntwurfsmuster
Abbildung 8.9 Beispiel für eine auf dem CompositeEntwurfsmuster basierende Baumstruktur
Abbildung 8.10 Klassendiagramm für das FlyweightEntwurfsmuster
Abbildung 8.11 Klassendiagramm für das DecoratorEntwurfsmuster
Abbildung 8.12 Klassendiagramm für das FacadeEntwurfsmuster
Abbildung 8.13 Klassendiagramm für das ProxyEntwurfsmuster
Abbildung 8.14 Klassendiagramm für das IteratorEntwurfsmuster
Abbildung 8.15 Klassendiagramm für das ObserverEntwurfsmuster
Abbildung 8.16 Klassendiagramm für das TemplateMethod-Entwurfsmuster
Abbildung 8.17 Klassendiagramm für das CommandEntwurfsmuster
Abbildung 8.18 Klassendiagramm für das StrategyEntwurfsmuster
Abbildung 8.19 Klassendiagramm für das MediatorEntwurfsmuster
Abbildung 8.20 Klassendiagramm für das MementoEntwurfsmuster
Abbildung 8.21 Klassendiagramm für das VisitorEntwurfsmuster
Abbildung 8.22 Klassendiagramm für das StateEntwurfsmuster
Abbildung 8.23 Klassendiagramm für das Beispiel
Abbildung 8.24 Klassendiagramm für das InterpreterEntwurfsmuster
Abbildung 8.25 Klassendiagramm für das Chain-ofResponsibility-Entwurfsmuster
Abbildung 9.1 Model View Controller
Abbildung 9.2 Model View Presenter
Abbildung 9.3 Model View Controller in klassischen Webanwendungen
Abbildung 9.4 Model View Controller in modernen Webanwendungen
Abbildung 9.5 Eine einfache Backbone.js-Anwendung
Abbildung 9.6 Model View ViewModel
Abbildung 9.7 Eine einfache Knockout.js-Anwendung
Abbildung 9.8 Eine einfache AngularJS-Anwendung
Abbildung 9.9 Gegenüber MVC ist die komponentenbasierte Architektur horizontal.
Abbildung 9.10 Einzelne Komponenten lassen sich in verschiedenen Anwendungen wiederverwenden.
Abbildung 9.11 Beim serverseitigen Routing wird auf Serverseite entschieden, welche Aktion bzw. welcher Controller aufgerufen wird.
Abbildung 9.12 Beim clientseitigen Routing wird auf Clientseite entschieden, welche Aktion bzw. welcher Controller aufgerufen wird.
Abbildung 10.1 Stark gekoppelte Anwendungen
Abbildung 10.2 Lose gekoppelte Anwendungen
Abbildung 10.3 Nachrichtenfluss bei AMQP
Abbildung 10.4 Prinzip einer Message-Queue
Abbildung 10.5 Prinzip eines Fanout Exchanges
Abbildung 10.6 Prinzip eines Direct Exchanges
Abbildung 10.7 Prinzip eines Topic Exchanges
Abbildung 10.8 Prinzip eines Header Exchanges
Abbildung 10.9 Workflow bei MQTT
Abbildung 10.10 Topics in MQTT
Abbildung 10.11 Quality of Service Level 0
Abbildung 10.12 Quality of Service Level 1
Abbildung 10.13 Quality of Service Level 2
Abbildung 11.1 Das Prinzip von Continuous Integration
Abbildung 11.2 Installation von Docker unter macOS
Abbildung 11.3 Docker startet als Dienst ...
Abbildung 11.4 ... und steht anschließend in der Toolbar zur Verfügung.
Abbildung 11.5 Konfiguration von Gogs
Abbildung 11.6 Registrieren eines Git-Accounts
Abbildung 11.7 Anmelden über den Git-Account
Abbildung 11.8 Anlegen eines neuen Git-Repositorys
Abbildung 11.9 Erzeugen eines neuen SSH-Schlüssels
Abbildung 11.10 Jenkins steht in verschiedenen Varianten zur Verfügung.
Abbildung 11.11 Vor der ersten Benutzung müssen Sie das Admin-Passwort eingeben.
Abbildung 11.12 Auswahl der zu installierenden Plugins
Abbildung 11.13 Installation der Standard-Plugins
Abbildung 11.14 Optionales Anlegen eines weiteren Admin-Accounts
Abbildung 11.15 Bestätigung über die erfolgreiche Konfiguration
Abbildung 11.16 Willkommensseite von Jenkins
Abbildung 11.17 Eingabemaske für die Installation weiterer Plugins
Abbildung 11.18 Manuelles Hochladen von JenkinsPlugins
Abbildung 11.19 Erzeugen eines neuen Jobs
Abbildung 11.20 Allgemeine Konfiguration eines JenkinsJobs
Abbildung 11.21 Angabe des Git-Repositorys für den Jenkins-Job
Abbildung 11.22 Festlegen der Build-Auslöser
Abbildung 11.23 Die Ausgabe eines Jobs können Sie jederzeit einsehen.
Abbildung 11.24 Festlegen der genauen Build-Schritte
Abbildung 11.25 Konfiguration der TAP-Darstellung als Post-Build-Aktion
Abbildung 11.26 Darstellung der TAP-Testergebnisse
Abbildung 11.27 Konfiguration von Clover als Post-BuildAktion
Abbildung 11.28 Darstellung der Code-CoverageErgebnisse
Abbildung 11.29 Code-Coverage pro Datei
Abbildung 11.30 Konfiguration von Checkstyle als PostBuild-Aktion
Abbildung 11.31 Darstellung der Checkstyle-Ergebnisse
Abbildung 11.32 Gesamtübersicht des Jenkins-Jobs
Abbildung 11.33 Weboberfläche von Travis CI
Abbildung 11.34 Weboberfläche von CircleCI