Javascript und Clean Code

Für die Online-Tagebuchplattform monkkee habe ich viel Javascript geschrieben. Beruflich habe ich mehrere Jahre JavaEE-Anwendungen mitentwickelt. Während ich in der Java-Welt eine Begeisterung für Clean Code und das gleichnamige Buch von Robert C. Martin entwickelt habe, war ich in der Javascript-Welt eher erschrocken, wie wenig Wert auf sauberen Code gelegt wird. Wie komme ich zu dieser Einschätzung?

Anonyme Funktionen wohin man schaut

Wenn ich mir Javascript-Codebeispiele ansehe, dann werden im großen Stil anonyme Funktionen (meistens als sogenannte Callbacks) eingesetzt. Das betrifft zum einen sehr einfache Beispiele, in denen dieses schnelle und einfache Mittel zu rechtfertigen wäre (Quelle: http://api.jquery.com/on/):

$( "#dataTable tbody" ).on( "click", "tr", function() {
    alert( $( this ).text() );
});

Aber auch Bibliotheken wie AngularJS, die mit ihrem Dependency Injection Konzept viel für gut strukturierten Javascript-Code leisten, arbeiten in ihrem Codebeispielen mit anonymen Funktionen (Quelle: http://docs.angularjs.org/tutorial/step_10)

function PhoneDetailCtrl($scope, $routeParams, $http) {
    $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
        $scope.phone = data;
        $scope.mainImageUrl = data.images[0];
    });
    $scope.setImage = function(imageUrl) {
        $scope.mainImageUrl = imageUrl;
    }
}

Das sind zwar nur kleine Codebeispiele, aber auch in renommierten Bibliotheken werden anonyme Funktionen im großen Stil verwendet, ganz zu schweigen vom produktiven Code vieler Entwickler, die durch obige Beispiele an diese Art der Entwicklung herangeführt werden.

Wieso sind anonyme Funktionen meiner Meinung nach nicht sauber?

Anonyme Funktionen haben keinen Namen

Der Benennung von Dingen ist in Clean Code das gesamte Kapitel 2 gewidmet. Zurecht meiner Meinung nach, denn gute Namen machen den Code lesbarer und machen Kommentare im besten Fall überflüssig.

Anonyme Funktionen haben keinen Namen. Mit jeder anonymen Funktion verzichtet man auf die beschreibende und dokumentierende Eigenschaft eines Namens. Oft kann der Leser des Codes die Semantik einer anonymen Funktion nicht erahnen. Wann wird die Funktion als Callback aufgerufen? Handelt es sich um einen Callback im success- oder error-Fall? Mit einem guten Namen wäre das klar.

Die Lösung besteht darin, dass die anonyme Funktion zunächst einer gut benannten lokalen Variablen zugewiesen würde.

Anonyme Funktionen sind üblicherweise so groß wie der aufzurufende Code

Robert C. Martin empfiehlt kurze Methoden, die genau eine Sache tun (Kapitel 5). In einer Java-Klasse lässt sich das einfach realisieren, indem kleine private Methoden gebildet werden, die jeweils genau eine Sache auf genau einer Abstraktionsebene tun.

Diese Möglichkeit besteht nicht, wenn ich eine anonyme Funktion einsetze. Zugriff auf andere Methoden hätte ich auch hier nur über einen Workaround, indem ich dafür sorge, dass im Closure-Scope der anonymen Funktion Objekte oder Funktionen verfügbar sind, die ich intern aufrufen kann. Das empfinde ich aber nicht als optimal.

Ich vermute, dass in den meisten Fällen von produktivem Javascript-Code eine anonyme Funktion so groß wie der Code ist, der abgearbeitet werden muss, also nicht an feingranularere Funktionen delegiert wird, wie Clean Code das vorschlägt.

Anonyme Funktionen fühlen sich wie Unter-Funktionen an

Mein Empfinden ist, dass anonyme Funktionen wie minderwertige Gehilfen daherkommen. Als Unter-Funktion der Funktion, in der sie deklariert werden. Das fühlt sich für mich falsch an, da der Callback häufig genauso bedeutend für den Programmfluss ist, wie die umgebende Funktion.

Ein gutes Beispiel ist ein Ajax-Call. Nehmen wir an, dass vor dem Call Dinge stattfinden, um den Call vorzubereiten, beispielsweise die Berechnung von Request-Parametern. Und wenn der Response wiederkommt, finden wieder Dinge statt, beispielsweise erneut eine Berechung und Veränderungen auf der Benutzeroberfläche. Mit anonymen Funktionen könnte man den Eindruck bekommen, dass der zweite Abschnitt, also die Verarbeitung des Responses, minderwertig ist. Für mich sind das aber zwei Abschnitte, die sich auf der gleichen Abstraktionsebene befinden und die daher auch gleich aussehen sollten.

Die Position von anonymen Funktionen ist fix

Ebenfalls in Kapitel 3 wird eine Empfehlung für die Reihenfolge von Funktionen gegeben. Um den Code möglichst lesbar zu machen, wird die Step Down Regel empfohlen. Nach dieser Regel sollen einer Funktion diejenigen Funktionen folgen, die von dieser aufgerufen werden und die sich auf der nächstfeineren Abstraktionsstufe befinden. Ziel ist, dass ein Leser den Code wie eine Geschichte lesen kann.

Mit anonymen Funktionen sind mir allerdings bezüglich der Reihenfolge des Codes die Hände gebunden, denn die Position der Funktion ist genau dort, wo ich die Funktion verwende bzw. als Callback übergebe.

Die oben erwähnte Lösung, anonyme Funktionen einer lokalen Variable zuzuweisen, offenbart hier ein Schwäche, denn hier muss die Funktion bereits vor ihrer Verwendung deklariert werden, was nicht der Step Down Regel entspricht.

Lösung 1: Klassen- bzw. Prototyp-Funktionen verwenden

Mithilfe der jQuery Funktion jQuery.proxy() oder dem Pendant von Underscore.js _.bind() ist es möglich, eine Klassen- bzw. Prototyp-Methode als Callback zu verwenden und den Context der Funktionsausführung (= this-Objekt) zu setzen. Wird innerhalb einer Methode einer Objektinstanz ein Callback verwendet, so wrappt man den Funktionsaufruf mit obigen Hilfsfunktionen und setzt die Objektinstanz als Context. Greife ich innerhalb der Funktion auf this zu, bekomme ich die Instanz.

Das folgende Beispiel zeigt einen Javascript-Prototypen mit zwei Methoden: registerListeners() und clicked(). Jede Methode hat eine Verantwortung (Single-Responsibility-Prinzip). Dadurch dass auch clicked() eine Prototyp-Funktion und keine anonyme Funktion ist, wird es als wesentlicher Programmteil wahrgenommen, genau wie die registerListeners(), und nicht als anonymer Gehilfe.

Example = (function() {

    function Example() {
        this.registerListeners();
    }

    Example.prototype.registerListeners = function() {
        return $('#clickme').on('click', $.proxy(this.clicked, this));
    };

    Example.prototype.clicked = function() {
        return console.log('clicked');
    };

    return Example;
})();

new Example();

Das Beispiel als JSFiddle: http://jsfiddle.net/bjoerne/dw53a/

In diesem Fall besteht clicked() nur aus einer Zeile. Es ist legitim zu behaupten, dass hier mit Kanonen auf Spatzen geschossen wird. Aber wie schnell wird aus einer Zeile ein ganzer Abschnitt? Ich neige eher dazu, jeden Callback so zu behandeln, egal ob nur eine CSS-Klasse ausgetauscht wird oder eine umfangreiche Berechnung inklusive Aufruf des Backends angestoßen wird.

Einschränkung dieses Vorgehens: Durch das Setzen eines neuen Contexts habe ich keinen Zugriff auf den originalen Context mit dem der Callback eigentlich aufgerufen würde. Der ist jedoch meist nicht notwendig. Im Fall von Event-Listenern ist er üblicherweise über das Event-Objekt erreichbar, in anderen Fällen wird er als Parameter dem Callback übergeben. Wenn der originale Context denoch benötigt wird, lässt sich sich mit einem Trick (http://jsfiddle.net/bjoerne/7FG9U/) arbeiten, in dem eine Closure gebildet wird.

Exkurs: Performance von jQuery.proxy()

Diesem Theme habe ich einen eigenen Blogeintrag gewidmet. Schon mal vorweg: Der Einsatz ist meist unbedenklich.

Lösung 2: Funktionen über Closure-Scope ansprechen

Ich habe ein nettes Pattern gefunden, mit dem ich Funktionen ähnlich flexibel schneiden und aufrufen kann, wie mit dem $.proxy-Beispiel.

Example = function() {
    
  function init() {
      registerListeners();
  }

  registerListeners = function() {
    return $('#clickme').on('click', clicked);
  };

  clicked = function() {
    return console.log('clicked');
  };

  init();
};

new Example();

Alle Funktionen sind im Scope aller anderen Funktionen vorhanden und können aufgerufen werden. Damit die Reihenfolge der Funktionen flexibel ist, steht der Initialisierungscode in einer init-Methode, die in der letzten Zeile aufgerufen wird. So kann ich aus der init()-Funktion registerListeners() aufrufen, obwohl diese Funktion erst später im Code deklariert wird.

Das Beispiel als JSFiddle: http://jsfiddle.net/bjoerne/r3unL/

Coffeescript einsetzen

Coffeescript ermöglicht, kompakteren Code zu schreiben. Ich mag die Sprache sehr gern und empfinde den Code als gut lesbar. Wer Wert auf Clean Code legt, sollte sich die Sprache mal ansehen.

class @Example

    registerListeners: ->
        $('#clickme').on 'click', $.proxy @clicked, @

    clicked: ->
      console.log 'clicked'

new Example().registerListeners()

Das Beispiel als JSFiddle: http://jsfiddle.net/bjoerne/MnNxB/

Fazit

Jetzt bin auch ich zufrieden und denke, dass sich mit Javascript bzw. Coffeescript sauberer und schöner Code schreiben lässt.

 

bjoerne_com_bjoern_weinbrenner_softwareentwickler_icon_leistungen_02

Lust auf mehr? Als JavaScript-Entwickler und Softwarearchitekt kann ich Sie in Ihren Projekten unterstützen. Melden Sie sich gerne bei mir.

2017-01-28T14:28:41+00:00 26.09.2013|Tags: , , , , |2 Comments

2 Kommentare

  1. Björn Weinbrenner 30. September 2013 um 10:07 Uhr- Antworten

    Weiterer interessanter Artikel zu diesem Thema: http://callbackhell.com/. Danke an Gregor Elke für diesen Hinweis.

  2. Stefan 15. Oktober 2013 um 11:34 Uhr- Antworten

    Hi Björn,

    interessanter Artikel! Vielen Dank dafür!

    Vielleicht auch interessant eine Diskussion in der CCD Xing Gruppe, die mich an deinen Artikel hat denken lassen:

    https://www.xing.com/net/pri07d432x/ccd/fragen-und-antworten-zur-ccd-praxis-336277/clean-code-und-angularjs-45275268/45281156/#45281156

    Viele Grüße
    Stefan

Hinterlassen Sie einen Kommentar