Highlights aus dem MobileTech 2013 Workshop - Mobile Javascript WebApps professionell entwickeln

Die Beschäftigung mit den verschiedenen Ansätzen der Softwareentwicklung für mobile Endgeräte lenkte mein Augenmerk auf aktuelle Konzepte in der Javascript-Entwicklung. Dass sich bewährte Prinzipien und Praktiken aus der herkömmlichen Webentwicklung auch auf Mobile-Web Projekte übertragen lassen, ist nachvollziehbar. Beispielsweise können dort ebenso CleanCode Richtlinien berücksichtigt und JavaScript Module Test getriebenen entwickelt werden. Dennoch gibt es spezifische Herausforderungen, die bei der Entwicklung von mobilen Applikationen ergeben.

Auf der MobileTech 2013 in München wurden im Workshop „Mobile Javascript WebApps professionell entwickeln“ derartige Herausforderungen adressiert und Lösungsansätze vorgestellt, welche anhand von Programmieraufgaben verinnerlicht werden konnten.

Im Folgenden möchte ich meine persönlichen Highlights aus diesem Workshop vorstellen.

Dependency Injection mit AngularJS

Einen Einblick in das Design Pattern Dependency Injection (kurz: DI) gibt der Blogbeitrag Inversion of Control vom 17.4.2012 [1]. Dieses Pattern wird im Java-Umfeld bereits in vielen Softwareprojekten genutzt und gilt als guter Stil. Nicht zuletzt durch die Inversion of Control Frameworks Spring [2] oder Google Guice [3] kann DI einfach in bestehenden Sourcecode eingesetzt werden.

Die gleichen Vorteile lassen sich auch in JavaScript Softwareprojekten nutzen, nur sind die o.g. Vorreiter-Frameworks Programmiersprachen spezifisch. Mit AngularJS [4] stellt Google eine Realisierung des DI-Patterns für JavaScript zur Verfügung. AngularJS bietet weitaus mehr Funktionalität, hier beschränken wir uns allerdings auf Dependency Injection.

Eine HTML-App für Jogger soll den aktuellen Standortdaten in der zugrundeliegende Datenbank aktualisieren und die Anzahl der Aktualisierungen vorhalten. Die Abhängigkeiten können wie folgt deklariert werden:

// neues Modul für die Runners App

angular.module('runnersApp', []).

// der GPS Tracker ist abhängig von GPS Locator und

// einem Location DAO zur Persistierung der Positionsdaten.

factory('gpsTracker', function(gpsLocator, locationDAO) {

   var trackCount = 0;

   return {

      trackCount: trackCount,

      // Service zur Aktualisierung der Läuferposition erzeugen

      update: function() {

         locationDAO.updateDb(gpsLocator.location());

         trackCount++;

      }

   };

});

// Injector erzeugen (macht Angular implizit) 

var injector = angular.injector(['runnersApp', 'ng']);

// GPS Tracker über den Injector holen

var gpsTracker = injector.get('gpsTracker');

Dieser Code erzeugt einen gpsTracker, der von 2 weiteren Komponenten abhängig ist, dem gpsLocator und dem locationDAO. Diese müssen auf die gleiche Art und Weise erzeugt werden wie der gpsTracker. Die Hereingabe der Abhängigkeiten über die Factory Funktion stellt die Umsetzung des Dependency Injection Patterns dar.

Auf den gpsTracker kann nun in den eigenen AngularJS Controllern unter Verwendung des injectors zugegriffen werden. Dies verletzt allerdings die Regel von Demeter [5]. Daher werden die verwendeten Module auch mittels DI in den Controller injeziert.

Das dazugehörige HTML Snippet und Controller verwenden den GPS Tracker wie folgt:

<div ng-controller="RunnersAppController">

   <button ng-click="trackLocation()">Standort aktualisieren</button>

</div>

// Controller für die Runners App

function RunnersAppController($scope, gpsTracker) {

   $scope.trackLocation = function() {

      gpsTracker.track();

   };

}

Damit ist das Thema Dependency Injection mit AngularJS schon erschlagen. Allerdings ist noch folgendes zu beachten:

  1. Der oben gezeigt Code funktioniert nicht, wenn das JavaScript minifiziert wird, da die Zuordnung der Abhängigkeiten über Parameternamen (wie gpsTracker, gpsLocator, locationDAO, RunnersAppController) nicht mehr erfolgen kann. Die Zuordnung kann explizit über zusätzlich vergebene Namen geschehen.

  2. AngularJS bietet eine übersichtlichere Schreibweise zur Deklaration und Verwendung von Abhängigkeiten

Abhängigkeiten Deklaration für Factories:

runnersAppModule.factory('gpsTracker', ['gpsLocator', 'locationDAO', function(renamedGpsLocator, renamedLocationDAO) {

   ...

}]);

Abhängigkeiten Verwendung in Controllern:

runnersAppModule.controller('runnersAppController', ['$scope', 'gpsTracker', function($scope, renamedGpsTracker) {

   ...

   $scope.updateLocation = function() {

      renamedGpsTracker.track();

   };

   ...

}]);

Bei den letzten beiden Beispielen kann man an der Factory- und Controllerdeklaration sehr schnell erkennen, wo Abhängigkeiten zu anderen Komponenten existieren. Der gpsTracker ist vom gpsLocator abhängig. Jetzt spielt es keine Rolle mehr, ob Parameternamen durch das Minifizieren des JavaScripts verändert werden. Der Code ist dagegen robust.

Durch diese Abhängigkeiten-Struktur ist es nun auch möglich Abhängigkeiten zu Implementierungen auszutauschen, wie es beispielweise für Unittests wichtig ist. Wie das genau passiert, wird im nächsten Abschnitt beschrieben.

 

Unittesting mit Jasmine und AngularJS

Der gpsTracker Service soll nun mittels eines automatisierten Tests wiederholt ausgeführt werden. Da alle Abhängigkeiten des Services über Dependency Injection ermittelt werden, kann ein Test geschrieben werden, der nur die Unit gpsTracker testet und alle Abhängigkeiten durch Mock-Implementierungen kapselt.

Mit dem JavaScript Testing Framework Jasmine [6] kann der gpsTracker einfach getestet werden, indem die abhängigen Services gpsLocator und locationDAO vor der Ausführung der einzelnen Tests durch Mocks ersetzt werden.

describe("GPS Tracker", function() {

   beforeEach(module('runnersApp', function($provide) {

         // mock gpsLocator service

         gpsLocatorMock = { location : jasmine.createSpy('location') };

         $provide.value('gpsLocator', gpsLocatorMock);

         // mock locationDAO

         locationDAOMock = { updateDb : jasmine.createSpy('updateDb') };

         $provide.value('locationDAO', locationDAOMock);

      });

   }));

   it('should count the track calls', inject(function(gpsTracker, gpsLocator, locationDAO) {

      gpsTracker.track();

      gpsTracker.track();

      expect(gpsTracker.trackCount).toEqual(2);

   }));

   it('should call mocks', inject(function(gpsTracker, gpsLocator, locationDAO) {

      gpsTracker.track();

      expect(gpsLocatorMock.location).toHaveBeenCalled();

      expect(locationDAOMock.updateDb).toHaveBeenCalledWith(gpsLocatorMock.location);

   }));

});

Der erste Test ruft die track Methode zweimal auf und prüft auf inhaltliche Korrektheit des Services. Der zweite Test prüft, ob die abhängigen Komponenten, hier die Mocks, erwartungsgemäß aufgerufen wurden. Mit AngularJS und Jasmine lassen sich also sehr elegant und mit geringem Aufwand AngularJS Services testen. Ähnlich einfach können Tests für AngularJS Controller und weitere Komponenten wie z.B. Filter geschrieben werden.

Kurz angeschnitten

Im Rahmen des Workshops kamen noch weitere Themen wie DataBinding, Callbacks mit dem Promise-Pattern oder automatisiertes UI Testing zur Sprache. Im Kontext der JavaScript Entwicklung für mobile möchte ich allerdings noch ein Thema erwähnen, welches ich sehr interessant finde.

Der Einsatz von AngularJS zusammen mit jQuery mobile [7] ist nicht ohne weiteres möglich, da beide Frameworks zur Laufzeit Code generieren und daher das Zusammenspiel beider Frameworks durch die Implementierung koordiniert werden muss. Ein Lösungsansatz dafür bietet der Adapter jquery-mobile-angular-adapter [8]. Im Workshop wurde auf diesen Adapter genauer eingegangen. Mittlerweile hat sich der Entwickler dazu entschieden einen Architekturwechsel für diesen Adapter zu vollziehen. Die Gründe und der neue Ansatz sind in [9] nachzulesen. Die Integration von bootstrap [10] in AngularJS verfolgt einen ähnlichen Ansatz. Es lohnt sich auf jeden Fall an diesem Thema dranzubleiben.

Das Wichtigste: Lessons Learned

Der Besuch dieses Workshops hat mir in vielerlei Hinsicht neue Erkenntnisse gebracht. Nicht nur der technische KnowHow-Zuwachs ist beachtlich, auch die Arbeitsweisen der Workshop-Leiter und deren Anspruch an die Softwarequalität hat mich sehr beeindruckt und führte dazu mein Wertesystem für gute Software nachzuschärfen.

Quellen:
[1] http://www.it-economics.de/blog/-/blogs/inversion-of-control-1

[2] http://www.springsource.org/

[3] https://code.google.com/p/google-guice/

[4] http://angularjs.org/http://docs.angularjs.org/guide/

[5] http://www.clean-code-developer.de/Law-of-Demeter.ashx

[6] http://pivotal.github.io/jasmine/

[7] http://jquerymobile.com/

[8] https://github.com/opitzconsulting/jquery-mobile-angular-adapter

[9] https://groups.google.com/forum/#!msg/angular/qG8YRwVaL_g/zgKAvaArmB0J

[10] http://getbootstrap.com/