Blog Archive

2014-06-05

Die-vielen-Talente-von-JavaScript

Die vielen »Talente« von JavaScript

Die vielen Talente von JavaScript Rollen-orientierte Programmieransätze wie Traits und Mixins verallgemeinern zu können

TL;DR / Abriss

  • Rollen-orientiertes Programmieren ist ein strukturelles Programmierparadigma aus der Objekt-orientierten Programmierung, welches hilft, unterschiedliche Anforderungen voneinander zu Trennen (Separation of Concerns).
  • »Mixin« und »Trait« sind grundlegende Merkmale einiger Programmiersprachen und haben sich als Konzept und Bezeichnung etabliert; beide genügen dem Rollen-Paradigma.
  • Mixin- und Trait-basierte Komposition findet überwiegend auf Klassenebene statt.
  • Traits stellen Operatoren zur Verfügung, um Konflikte während der Komposition aufzulösen; d.h. die Reihenfolge unterschiedlicher Kompositionsschritte ist nicht so zwingend vorgegeben wie bei Mixins, welche ausschließlich lineare Komposition unterstützen.
  • Verhalten, welches durch Mixins und Traits zur Verfügung gestellt wird, kann zur Laufzeit weder hinzugefügt noch weggenommen werden.
  • Gerade für Traits war lange unklar, wie diese mit Zustand umzugehen haben.
  • Talente sind ein akademisches Konzept, welches Rollen-Ansätze wie Traits und Mixins weiterdenkt, indem es einige ihrer Fähigkeiten weiterentwickelt und ein paar ihrer Merkmale zusammenführt und damit deren Einsatz in der Praxis vereinfacht/erleichtert.
  • Aufgrund der Sprachkernkonzepte wie »Closure« und »Delegation« sind JavaScript und Talente füreinander bestimmt.

Delegationsprinzipien

JavaScript1 ist eine Delegationssprache mit sowohl selbstausführendem als auch direktem Delegationsmechanismus.

Funktionsobjekte als Rollen (Traits und Mixins)

JavaScript unterstützt2 3 4 5 schon auf der Ebene des Sprachkerns verschiedene auf Funktionsobjekten aufbauende Implementierungen des Rollen-Musters wie z.B. Traits und Mixins. Zusätzliches Verhalten wird bereitgestellt, indem mindestens eine Methode über das Schlüsselwort this6 im Rumpf eines function7-Objekts gebunden wird. Benötigt ein Objekt zusätzliches Verhalten, welches ihm nicht über die Prototypenkette8 zur Verfügung gestellt werden kann, lässt sich eine Rolle direkt über call9 bzw. apply10 an dieses Objekt delegieren.

var Enumerable_first = function () {
  this.first = function () {
    return this[0];
  };
};
var list = ["foo", "bar", "baz"];

console.log("(typeof list.first)", (typeof list.first)); // "undefined"

Enumerable_first.call(list); // explicit delegation

console.log("list.first()", list.first()); // "foo"

Objektkomposition und Vererbung durch Delegation

Während Komposition in JavaScript über diese direkte Delegation abgedeckt werden kann, kommt automatische Delegation immer dann zur Anwendung, wenn der Interpreter die Prototypenkette eines Objekts nach oben hin abwandern muss, um z.B. eine mit diesem Objekt assoziierte Methode zu finden, die diesem nicht unmittelbar gehört. Sobald die Methode gefunden ist, wird sie im Kontext dieses Objekts aufgerufen. Demzufolge wird Vererbung in JavaScript über einen selbstausführenden Delegationsmechanismus abgebildet, der an die prototype-Eigenschaft von Konstruktorfunktionen gebunden ist.

var Enumerable_first_last = function () {
  this.first = function () {
    return this[0];
  };
  this.last = function () {
    return this[this.length - 1];
  };
};
console.log("(typeof list.first)", (typeof list.first));  // "function"   // as expected
console.log("(typeof list.last)", (typeof list.last));    // "undefined"  // of course

Enumerable_first_last.call(Array.prototype);  // applying behavior to [Array.prototype]

console.log("list.last()", list.last());      // "baz"  // due to delegation automatism

Von den Schwierigkeiten die durch Rollen etablierten Konzepte und Begrifflichkeiten auf JavaScript anzuwenden

»Mixins«

Folgt man der deutschsprachigen Wikipedia-Version, war Flavors, …

… die erste objektorientierte Erweiterung in der Programmiersprachenfamilie Lisp
, (die) … erstmals eine Mehrfachvererbung in der objektorientierten Programmierung unterstützt.
(Wobei) … auch Mixins, ein spezielles Entwurfsmuster im Zusammenhang mit der Mehrfachvererbung, erstmals unterstützt (wurden).

Mit diesem Konzept wurde es möglich, Quellcode auf Klassenebene wiederzuverwenden. Obwohl Mixin-Ansätze über die Jahre hinweg weiterentwickelt wurden, ließe sich sehr kurz zusammengefasst sagen, dass sich Komposition auch weiterhin auf Klassen beschränkt(, obwohl z.B. Ruby diese auch auf Instanzebene unterstützt). Und wegen des Fehlens geeigneter Kompositionsoperatoren, welche dabei helfen, Namenskonflikte zwischen miteinander konkurrierenden Methoden zu lösen, erfolgt Mixin-Komposition immer geradlinig in geordeter Reihenfolge.

»Traits«

Das Konzept der Traits11 wurde eine Dekade lang wissenschaftlich besonders intensiv erforscht und weiterentwickelt. Erste Veröffentlichungen der »Software Composition Group«12 (SCG) an der Universität Bern reichen zurück bis November 200213.

Traits überwinden einige Schranken Mixin-gestützter Komposition. Traits stellen für letzteres Operatoren für Kombination, Ausschluß und Umbennung/Parallelbezeichnung von Methoden zur Verfügung. Durch diese Flexibilität erschließen sie sich im Vergleich zu Mixins mehr Anwendungsfälle. Und da zustandsbehaftete Traits14 erstmals 2006 in einem weiteren Entwicklungsschritt beschrieben wurden (die ersten SCG Traits hatten zustandlos zu sein), sah es zumindest in den letzten Jahren ganz so aus, als ob die besonderen Sprachmerkmale von JavaScript und eine rein auf Funktionen basierende Umsetzung des Traits-Konzepts füreinander bestimmt seien.

JavaScript als klassenlose, fast komplett auf Objekten basierende, auf vielfältige Weise hochdynamische Delegationssprache mit großem funktionalen Naturell ist aber mühelos in der Lage, weit über die Grenzen der Traits hinauszugehen. Deren Konzept unterstützt z.B. nicht Verhaltensveränderungen von Objekten zur Laufzeit. Und obwohl Trait-Komposition sehr flexibel ist, beschränkt sie sich auf die Ebene von Klassen und anderer Traits. Außerdem lassen sich Traits zur Programmlaufzeit weder zu Klassen/Traits hinzufügen noch von diesen entfernen.

Seit geraumer Zeit aber gibt es einen heimlichen Neuzugang15, welcher JavaScript auf Augenhöhe begegnet. Somit liegt der Ball für das spielerische Ausloten der neuen Möglichkeiten und zum “an sich selber wachsen” wiederum bei JavaScript, wobei diese Entdeckungsreise eben erst beginnt …

»Talents: Dynamically Composable Units of Reuse« + + + »Talente: dynamisch komponier- und wiederverwendbare Einheiten«

Die gleichnamige SCG-Veröffentlichung vom Juli 2011 beschreibt Talente16 als …

… object-specific units of reuse which model features that an object can acquire at run-time. Like a trait, a talent represents a set of methods that constitute part of the behavior of an object. Unlike traits, talents can be acquired (or lost) dynamically. When a talent is applied to an object, no other instance of the object’s class are affected. Talents may be composed of other talents, however, as with traits, the composition order is irrelevant. Conflicts must be explicitly resolved.

Like traits, talents can be flattened, either by incorporating the talent into an existing class, or by introducing a new class with the new methods. However, flattening is purely static and results in the loss of the dynamic description of the talent on the object. Flattening is not mandatory, on the contrary, it is just a convenience feature which shows how traits are a subset of talents.

Und das wiederum hört sich nach einem perfekten Treffer an; als ob das Konzept der Talente ganau auf das Sprachdesign von JavaScript abgestimmt wurde, auf dass Talente auf dem für diese Sprache natürlichstem Wege umsetzbar sind. Komposition erfolgt sowohl auf reiner Objekt-/Instanz-Ebene als auch auf Konstruktor-/”Klassen”-Ebene. Und … Talente berücksichtigen Zustand.

Vom »Mixin« zum »Trait« - die Wirkungsweise rein Funktions-getriebener »Talente« in JavaScript

Den wissenschaftlichen Erkenntnissen zufolge sollen Talente sowohl auf Instanz- als auch auf Klassen-Ebenen wirken. Dabei können Talente so einfach und zielgerichtet wie Mixins eingesetzt werden; über sie darf aber auch zusätzliche Funktionlität für Kompositions-Operationen zur Verfügung stehen, damit sie wie Traits benutzt werden können.

Der »Mixin-Ansatz«

Das strukturell einfachste Talent setzt auf zustandlosem Weg genau eine Methode um und berücksichtigt möglicherweise autretende Konflikte nicht. Für JavaScript bietet sich die Umsetzung der schon einmal weiter oben verwendeten Verhaltens-Funktionalität für abzählbare Listen an - das Beispiel von Enumerable_first sieht dann in einem ersten Schritt so aus:

var Enumerable_first = function () {
  this.first = function () {
    return this[0];
  };
};

Die Anwendung dieses Talents auf ein Array bringt letzterem das im Beispielcode festgelegte Verhalten bei …

var list = ["foo", "bar", "baz"];

Enumerable_first.call(list);

console.log("list.first()", list.first()); // "foo"

.., das Agieren auf “Klassen”-Ebene (Array.prototype) ohne Konfliktlösung, kommt dem klassischen Mixin-Ansatz dann so nahe wie möglich …

var list = ["foo", "bar", "baz"];

Enumerable_first.call(Array.prototype);

console.log("list.first()", list.first()); // "foo"

Der Einsatz dieses Musters, sowohl auf “Klassen”- als auch auf “Instanz”- Ebene wurde 2011 von Angus Croll wiederentdeckt und von ihm mit dem Namen Flight Mixin bedacht.

Obwohl dieser erste Ansatz funktioniert, weist er ein paar Nachteile auf, die es zu korrigieren gilt. Denn momentan wird für jedes Objekt, auf welches das Talent angewendet wird, eine eigene unreferenzierte first-Methode erzeugt.

delete Array.prototype.first;

var
  list = ["foo", "bar", "baz"],
  arr = ["biz", "buzz"]
;
Enumerable_first.call(list);
Enumerable_first.call(arr);

console.log("arr.first()", arr.first()); // "biz"
console.log("(list.first === arr.first)", (list.first === arr.first)); // false

Für eine einzelne, speicherschonend referenzierbare Methode muss das schon umgesetzte Talent in ein »Module Pattern« verpackt werden - zweiter Schritt:

var Enumerable_first = (function () {

  var
    Mixin,

    first = function () {
      return this[0];
    }
  ;
  Mixin = function () {
    this.first = first;
  };

  return Mixin;

}());

var
  list = ["foo", "bar", "baz"],
  arr = ["biz", "buzz"]
;
Enumerable_first.call(list);
Enumerable_first.call(arr);

console.log("list.first()", list.first());  // "foo"
console.log("arr.first()", arr.first());    // "biz"

console.log("(list.first === arr.first)", (list.first === arr.first)); // true

Da sich der letzte Ansatz als tragfähig erwiesen hat, kann er in einem dritten Schritt wieder gekürzt werden zu …

var Enumerable_first = (function () { // Pure Talent / Lean Mixin.
  var first = function () {
    return this[0];
  };
  return function () { // Mixin mechanics refering to shared code.
    this.first = first;
  };
}());

»Pure Talent« und »Lean Mixin«

An diesem Punkt ließe sich folgendes formulieren …

Jede Umsetzung eines Rollen-Musters soll als Talent bezeichnet werden.

… und …

Jede auf dem Module Pattern basierende zustandslose Umsetzung einer Rolle, die Referenzierung unterstützt und ohne Konfliktlösung auskommt, soll als Pure Talent und/oder Lean Mixin bezeichnet werden.

Spezialisierte »Mixins« als Fundamente für »Traits«

Im folgerichtig nächsten Schritt wäre es denkbar, ein Resolvable Lean Mixin zu entwickeln, welches Kompsoitionsmethoden wie resolveBefore, resolveAfter, resolveAround und resolveWithAlias bereitstellt, um die durch Traits zu unterstützende Fähigkeit zur Konfliktlösung umzusetzen.

Um die nachfolgenden Beispiele lauffähig zu halten, sind diese auf zusätzlich unterstützenden Quellcode angewiesen, welcher die Grundlagen zur Auflösung von Konflikten gleichnamiger konkurrierender Methoden liefert.

Der Quellcode eines Resolvable-Ansatzes, aufbauend auf dem Lean Mixin-Muster, würde dann höchstwahrscheinlich aussehen wie im nächsten Beispiel:

var Resolvable = (function () { // Pure Talent / Lean Mixin.

  var
    Mixin,

    isString    = function (type) {
      return (typeof type == "string");
    },
    isFunction  = function (type) {
      return (typeof type == "function");
    },

    resolveBefore = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].before(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },
    resolveAfter = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].after(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },
    resolveAround = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].around(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },

    resolveWithAlias = function (methodName, aliasName, rivalingMethod) {
      if (
        isString(methodName)
        && isString(aliasName)

        && (methodName != aliasName)

        && isFunction(rivalingMethod)
      ) {
        this[aliasName] = rivalingMethod;
      }
    }
  ;

  Mixin = function () { // Mixin mechanics refering to shared code.
    var type = this;

    type.resolveBefore    = resolveBefore;
    type.resolveAfter     = resolveAfter;
    type.resolveAround    = resolveAround;
    type.resolveWithAlias = resolveWithAlias;
  };

  return Mixin;

}());

Ein Anwendungsfall des gerade erstellten Resolvable-Talents/Mixins könnte dann so aussehen:

var Floatable = function () { // implementing a [Floatable] Trait.

  this.resolveAfter("initialize", function () {
    console.log("make it floatable.");
  });
};
var Ridable = function () { // implementing a [Ridable] Trait.

  this.resolveBefore("initialize", function () {
    console.log("make it ridable.");
  });
};

var Amphicar = function () {  // implementing an [Amphicar] Constructor.

  Resolvable.call(this);      // applying the [Resolvable] Mixin.

  Floatable.call(this);       // applying the [Floatable] Trait.
  Ridable.call(this);         // applying the [Ridable] Trait.

  this.initialize();
};

var ac = new Amphicar;
// "make it ridable."
// "make it floatable."

console.log(Object.keys(ac));
// ["resolveBefore", "resolveAfter", "resolveAround", "resolveWithAlias", "initialize"]

Wie sich unschwer erkennen lässt, erzwingt diese einfache Umsetzung eines Resolvable-Talents für jede Amphicar-Instanz 4 adressierbare Methoden, die aber wirklich nicht sichtbar sein sollten.

Dennoch, der ausschließlich auf Funktionen und Delegation bauende Ansatz ist tragfähig genug, ein Resolvable voll funktionsfähig umzusetzen und dessen Wirkungsweise vor der Außenwelt zu verstecken.

Ein Hidden/Undercover Talent oder Silent/Stealth Mixin verändert das Objekt, auf welches es direkt wirkt, nicht. Sein Hauptziel ist es, den Bezugspunkt seines Wirkens (context) zu sichern, um diesen Kontext dann an zusätzliches Verhalten zu delegieren, welches nach außen hin niemals sichtbar wird. Deswegen erweitert ein solches Mixin ein zusätzlich injiziertes (lokales) Proxy-Objekt um genau diese Delegationsfunktionalität.

Der »Stealth Mixin«-Ansatz, der hier Resolvable_silent genannten Variante, versteckt seine Konfliktlösungsmethoden vor jeder Amphicar-Instanz, wie es das nächste Beispiel sofort anschaulich demonstrieren wird:

var Resolvable_silent = (function () { // Undercover Talent / Stealth Mixin.

  var
    StealthMixin,

    isString    = function (type) {
      return (typeof type == "string");
    },
    isFunction  = function (type) {
      return (typeof type == "function");
    },

    resolveBefore = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].before(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },
    resolveAfter = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].after(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },
    resolveAround = function (methodName, rivalingMethod) {
      if (isString(methodName) && isFunction(rivalingMethod)) {

        var type = this;
        if (isFunction(type[methodName])) {

          type[methodName] = type[methodName].around(rivalingMethod, type);
        } else {
          type[methodName] = rivalingMethod;
        }
      }
    },

    resolveWithAlias = function (methodName, aliasName, rivalingMethod) {
      if (
        isString(methodName)
        && isString(aliasName)

        && (methodName != aliasName)

        && isFunction(rivalingMethod)
      ) {
        this[aliasName] = rivalingMethod;
      }
    }
  ;

  StealthMixin = function (resolvableProxy) { // Stealth Mixin using an injected proxy.
    if (resolvableProxy && (typeof resolvableProxy == "object")) {

      var type = this;
  /*
   *  special functional Mixin pattern that preserves and foldes
   *  two different contexts and does delegation with partly code reuse.
   */
      resolvableProxy.resolveBefore    = function (/*methodName, rivalingMethod*/) {
        resolveBefore.apply(type, arguments);
      };
      resolvableProxy.resolveAfter     = function (/*methodName, rivalingMethod*/) {
        resolveAfter.apply(type, arguments);
      };
      resolvableProxy.resolveAround    = function (/*methodName, rivalingMethod*/) {
        resolveAround.apply(type, arguments);
      };
      resolvableProxy.resolveWithAlias = function (/*methodName, aliasName, rivalingMethod*/) {
        resolveWithAlias.apply(type, arguments);
      };
    }
  };

  return StealthMixin;

}());

Der Quellcode des weiter oben schon einmal benutzten Anwendungsfalls für Resolvable lässt sich dann leicht abändern zu …

var Floatable = function () { // implementing a "silent" [Floatable] Trait featuring ...
                              // ... the "Stealth Mixin" variant of [Resolvable].
  var resolvable = {};
  Resolvable_silent.call(this, resolvable);

  resolvable.resolveAfter("initialize", function () {
    console.log("initialize :: make this floatable.", this);
  });
};
var Ridable = function () { // implementing a "silent" [Ridable] Trait featuring ...
                            // ... the "Stealth Mixin" variant of [Resolvable].
  var resolvable = {};
  Resolvable_silent.call(this, resolvable);

  resolvable.resolveBefore("initialize", function () {
    console.log("initialize :: make this ridable.", this);
  });
};

var Amphicar = function () {  // implementing the [Amphicar] Constructor.

  Floatable.call(this);       // applying the "silent" [Floatable] Trait.
  Ridable.call(this);         // applying the "silent" [Ridable] Trait.

  this.initialize();
};

var ac = new Amphicar;
// initialize :: make this ridable.   Amphicar {initialize: function}
// initialize :: make this floatable. Amphicar {initialize: function}

console.log("the new amphicar", ac);
// the new amphicar                   Amphicar {initialize: function}

.., und stellt damit anschaulich die Vorzüge des auf Funktionen basierenden Konzepts für Stealth Mixins und Silent Traits unter Beweis.

»Undercover Talent«, »Stealth Mixin« und »Silent Trait«

Jede Umsetzung einer Rolle, die ihren this-Kontext unverändert lässt, dafür aber einen zusätzlich injizierten Proxy um Verhalten erweitert, welches wiederum Kontext und Proxy durch Delegation miteinander verschränkt, um dieses Verhalten nicht öffentlich abzubilden, soll als Hidden/Undercover Talent und/oder Silent/Stealth Mixin bezeichnet werden.

.

Es existieren Lösungen zum Konfliktlösungsverhalten, die auf Basis von Undercover Talent- / Stealth Mixin- Mustern umgesetzt wurden, und die als Resolvable bezeichnet werden.

.

Jede Umsetzung einer Rolle, die eine Zusammensetzung aus mindestens einem Talent und einem Stealth Mixin-basierten Resolvable ist, soll als Silent Trait bezeichnet werden.

Die derzeitige Systematik funktionaler »Talente«

Die Betrachtung von Zustand, dessen Existenz sowie seine Herkunft, und wie Zustand dann nur erweitert oder vollkommen verändert wird, bestimmt inhaltlich den letzten Teil dieses Dokuments. Diese Richtschnur hilft dabei, noch tiefer in die spezielle Materie der Talente einzutauchen. Zum besseren Verständnis werden ausschließlich Beispiele aus der Praxis herangezogen.

»Taxonomy Matrix« einiger möglicher Talente-Muster

Meine früheren Dokumente und Presentationen zeigen eine Art »Trait Taxonomy Matrix« der vielen Trait / Mixin Varianten als direktes Resultat meines Versuchs, die von mir in der Praxis meistgenutzten Implementierungen des Rollen-Musters zu klassifizieren.

Da nun aber Ausdrücke wie »Trait« und »Mixin« das mit JavaScript mögliche Rollen-Spektrum nicht vollständig abdecken, wurden »Talente« als Konzept und Begriff zur neuen Arbeitsgrundlage, um alle schon in der Praxis identifizierten Varianten an Rollen-basiertem »Code-reuse« abzudecken. Anhand der folgenden »State«-Koordinaten wurde die zuletzt gültige Begrifflichkeit komplett geändert zu …

state stateless no active mutation actively mutating proxy augmentation
stateless Pure Talent
Lean Mixin
- - -
created - Smart Talent
(Skilled Talent)
Smart Talent
(Skillful Talent)
-
injected - Smart Talent
(Promoted Talent)
Smart Talent
(Privileged Talent)
-
both created and injected - Smart Talent
(Privileged Talent)
Smart Talent
(Privileged Talent)
-
injected proxy - - - Hidden/Undercover Talent
Silent/Stealth Mixin

Die hier abgebildete »Taxonomy Matrix« ist nicht in Stein gemeißelt. Es handelt sich vielmehr um eine Momentaufnahme, die auf Bewertung wartet, gerne auch auf kritische. Jeder der denkt bzw. sich dazu berufen fühlt, etwas beitragen zu können bzw. zu müssen, ist hiermit aufgefordert, dies zu tun.

Ausgewählte »Smart Talent«-Muster

Ein Pure Talent zeichnet sich durch absolute Zustandslosigkeit aus, und das Hidden/Undercover Talent ist hinsichtlich seines Proxy-Objekts streng definiert. Im Kontrast dazu muss jede Smart Talent-Variante Zustand besitzen, egal ob innerhalb des Talents erzeugt oder in ebendieses injiziert, egal ob aktiv veränderbar oder eben unveränderlich.

Jede umgesetzte Variante einer Rolle, die zustandsbehaftet ist, egal ob Zustand von außen heineingetragen oder dieser intern selbst definiert wurde und dessen Veränderlichkeit außer Acht lassend, soll als Smart Talent bezeichnet werden.

Es gibt bestimmt mehr Smart Talent-Varianten als die 6 in der Matrix aufgeführten. Und es bedarf dringendem Nachdenkens und guter Diskussion, wie sinnvoll es sowohl aus theoretischer als auch aus praktischer Sicht ist, alle möglichen Varianten unterscheiden und benennen zu wollen. Es gibt aber für mindestens 2 von ihnen ausgewiesene Anwendungsfälle aus der Praxis.

Jede Umsetzung einer Rolle, die auf zusätzlich injizierten Zustand angewiesen ist, diesen aber nur liest und damit unverändert lässt, soll als Promoted Talent bezeichnet werden.

Anwendungsfälle wären dann zum einen ein Wrapper für selbstdefinierte abzählbare Listen, Enumerable_first_last_item_listWrapper

var Enumerable_first_last_item_listWrapper = (function () {

  var
    global = this,

    Talent,

    parse_float = global.parseFloat,
    math_floor  = global.Math.floor
  ;
  Talent = function (list) { // implementing the "promoted" [Enumerable] Talent.
    var
      enumerable = this
    ;
    enumerable.first = function () {
      return list[0];
    };
    enumerable.last = function () {
      return list[list.length - 1];
    };
    enumerable.item = function (idx) {
      return list[math_floor(parse_float(idx, 10))];
    };
  };

  return Talent;

}).call(null);

… und Allocable zum anderen, ebenfalls ein Wrapper, der den kontrollierten Zugriff von außen auf die eingeschlossene Listenstruktur umsetzt und sich dabei auch des ersten Wrappers bedient …

var Allocable = (function () {

  var
    global = this,

    Enumerable_listWrapper = global.Enumerable_first_last_item_listWrapper,

    Talent,

    Array = global.Array,

    array_from = ((typeof Array.from == "function") && Array.from) || (function (proto_slice) {
      return function (listType) {

        return proto_slice.call(listType);
      };
    }(Array.prototype.slice))
  ;

  Talent = function (list) { // implementing the "promoted" [Allocable] Talent.
    var
      allocable = this
    ;
    allocable.valueOf = allocable.toArray = function () {
      return array_from(list);
    };
    allocable.toString = function () {
      return ("" + list);
    };
    allocable.size = function () {
      return list.length;
    };
    Enumerable_listWrapper.call(allocable, list);
  };

  return Talent;

}).call(this);

.., womit beide Wrapper schon die Grundlage für selbstdefinierte Collections, wie z.B. Queue, liefern:

var Queue = (function () {

  var
    global = this,
    Allocable = global.Allocable
  ;

  return function () { // implementing the [Queue] Constructor.
    var list = [];

    this.enqueue = function (type) {

      list.push(type);
      return type;
    };
    this.dequeue = function () {

      return list.shift();
    };

    Allocable.call(this, list); // applying the "promoted" [Allocable] Talent.
  };

}).call(null);



var q = new Queue;

console.log("q.size()", q.size());        // 0

q.enqueue("the");
q.enqueue("quick");
q.enqueue("brown");
q.enqueue("fox");

console.log("q.size()", q.size());        // 4
console.log("q.toArray()", q.toArray());  // ["the", "quick", "brown", "fox"]

console.log("q.first()", q.first());      // "the"
console.log("q.item(1)", q.item(1));      // "quick"
console.log("q.last()", q.last());        // "fox"

q.dequeue();
q.dequeue();

console.log("q.toArray()", q.toArray());  // ["brown", "fox"]
console.log("q.size()", q.size());        // 2

q.dequeue();
q.dequeue();
q.dequeue();

console.log("q.size()", q.size());        // 0

»Skilled Talent« und »Skillful Talent«

Jede Umsetzung einer Rolle, die veränderlichen Zustand selbst erzeugt, um ihre Aufgabe(n) erfüllen zu können und dabei zu keinem Zeitpunkt auf zusätzlich injizierten Zustand angewiesen ist, soll als Skilled Talent oder Skillful Talent bezeichnet werden.

Observable ist wahrscheinlich das am häufigsten anzutreffende praxisbezogene Beispiel für Mixins. Der folgende Quellcode entspricht in seiner Umsetzung dem Skillful Talent, denn bei seiner Anwendung werden viele verschiedene Zustände erzeugt, gelesen und verändert:

var Observable_SignalsAndSlots = (function () { // implementing a "skillful" [Observable] Talent.

  var
    global = this,
    Array = global.Array,

    isFunction = function (type) {
      return (
        (typeof type == "function")
        && (typeof type.call == "function")
        && (typeof type.apply == "function")
      );
    },
    isString = (function (regXCLassName, expose_implementation) {
      return function (type) {

        return regXCLassName.test(expose_implementation.call(type));
      };
    }((/^\[object\s+String\]$/), global.Object.prototype.toString)),

    array_from = ((typeof Array.from == "function") && Array.from) || (function (proto_slice) {
      return function (listType) {

        return proto_slice.call(listType);
      };
    }(Array.prototype.slice))
  ;

  var
    Event = function (target, type) {

      this.target = target;
      this.type = type;
    },

    EventListener = function (target, type, handler) {

      var defaultEvent = new Event(target, type);             // creation of new state.

      this.handleEvent = function (evt) {
        if (evt && (typeof evt == "object")) {

          evt.target = defaultEvent.target;
          evt.type = defaultEvent.type;

        } else {

          evt = {
            target: defaultEvent.target,
            type: defaultEvent.type
          };
        }
        handler(evt);
      };
      this.getType = function () {
        return type;
      };
      this.getHandler = function () {
        return handler;
      };
    },

    EventTargetMixin = function () { // implementing the [EventTarget] Mixin as "skillful" Talent.

      var eventMap = {};                                      // mutable state.

      this.addEventListener = function (type, handler) {      // will trigger creation of new state.
        var reference;
        if (type && isString(type) && isFunction(handler)) {
          var
            event = eventMap[type],
            listener = new EventListener(this, type, handler) // creation of new state.
          ;
          if (event) {
            var
              handlers = event.handlers,
              listeners = event.listeners,
              idx = handlers.indexOf(handler)
              ;
            if (idx == -1) {
              handlers.push(listener.getHandler());
              listeners.push(listener);

              reference = listener;
            } else {
              reference = listeners[idx];
            }
          } else {
            event = eventMap[type] = {};
            event.handlers = [listener.getHandler()];
            event.listeners = [listener];

            reference = listener;
          }
        }
        return reference;
      };

      this.dispatchEvent = function (evt) {
        var
          successfully = false,
          type = (
            (evt && (typeof evt == "object") && isString(evt.type) && evt.type)
            || (isString(evt) && evt)
          ),
          event = (type && eventMap[type])
        ;
        if (event) {
          var
            listeners = (event.listeners && array_from(event.listeners)),
            len = ((listeners && listeners.length) || 0),
            idx = -1
          ;
          if (len >= 1) {
            while (++idx < len) {

              listeners[idx].handleEvent(evt);
            }
            successfully = true;
          }
        }
        return successfully;
      };
    }
  ;

  return EventTargetMixin; // will be exposed to the outside as [Observable_SignalsAndSlots].

}).call(null);

Der Quellcode, der schon einmal weiter oben gezeigten beispielhaften Umsetzung einer Queue, macht sich im nächsten Schritt Observable_SignalsAndSlots zunutze und könnte dann in folgende Richtung verbessert werden:

var Queue = (function () {

  var
    global = this,

    Observable  = global.Observable_SignalsAndSlots,
    Allocable   = global.Allocable,

    Queue,

    onEnqueue = function (queue, type) {
      queue.dispatchEvent({type: "enqueue", item: type});
    },
    onDequeue = function (queue, type) {
      queue.dispatchEvent({type: "dequeue", item: type});
    },
    onEmpty = function (queue) {
      queue.dispatchEvent("empty");
    }
  ;

  Queue = function () { // implementing the [Queue] Constructor.
    var
      queue = this,
      list = []
    ;
    queue.enqueue = function (type) {

      list.push(type);
      onEnqueue(queue, type);

      return type;
    };
    queue.dequeue = function () {

      var type = list.shift();
      onDequeue(queue, type);

      (list.length || onEmpty(queue));

      return type;
    };
    Observable.call(queue);       // applying the "skillful" [Observable_SignalsAndSlots] Talent.
    Allocable.call(queue, list);  // applying the "promoted" [Allocable] Talent.
  };

  return Queue;

}).call(null);



var q = new Queue;

q.addEventListener("enqueue", function (evt) {console.log("enqueue", evt);});
q.addEventListener("dequeue", function (evt) {console.log("dequeue", evt);});
q.addEventListener("empty", function (evt) {console.log("empty", evt);});

q.enqueue("the");   // "enqueue" Object {type: "enqueue", item: "the", target: Queue}
q.enqueue("quick"); // "enqueue" Object {type: "enqueue", item: "quick", target: Queue}
q.enqueue("brown"); // "enqueue" Object {type: "enqueue", item: "brown", target: Queue}
q.enqueue("fox");   // "enqueue" Object {type: "enqueue", item: "fox", target: Queue}

q.dequeue();        // "dequeue" Object {type: "dequeue", item: "the", target: Queue}
q.dequeue();        // "dequeue" Object {type: "dequeue", item: "quick", target: Queue}
q.dequeue();        // "dequeue" Object {type: "dequeue", item: "brown", target: Queue}
q.dequeue();        // "dequeue" Object {type: "dequeue", item: "fox", target: Queue}
                    // "empty"   Object {target: Queue, type: "empty"}
q.dequeue();        // "dequeue" Object {type: "dequeue", item: undefined, target: Queue}
                    // "empty"   Object {target: Queue, type: "empty"}

»Privileged Talent«

Jede andere, bisher noch nicht erfasste Smart Talent-Variante soll dann einfach Privileged Talent heißen, was sich ziemlich gut mit der letzten Definition deckt:

Jede Umsetzung einer Rolle, die entweder ausschließlich auf zusätzlich injizierten veränderlichen Zustand angewiesen ist oder aber auf beides, Erzeugung von veränderlichem Zustand und zusätzlich injizierten Zustand ungeachtet seiner Veränderlichkeit, soll als Privileged Talent bezeichnet werden.


Schlusskommentar

Das Ziel dieses Dokuments war es, nur auf der Sprachkernebene von JavaScript, Wissen über Techniken und Muster zur Vefügung zu stellen/zu verbreiten, die auf einfachem Wege zu besseren Ergebnissen bei Wartung und Wiederverwendung von Quellcode führen.
Vor ein paar Jahren konnte mich schon traits.js nicht vollständig überzeugen, und heutzutage gelingt CocktailJS dies auch nur ansatzweise. Meiner Meinung nach sind beide Bibliotheken nicht offen genug und in ihrer Leistungsfähigkeit deutlich überdimensioniert. Es sind eher schon kleine aber komplexe Frameworks.
Und beide wählen dazu nicht den hier beschriebenen und von mir immer favorisierten Funktions-basierten Ansatzt, dessen Vorteile darin liegen, dass man auf Sprachkernebene nicht nur die Delegation geschenkt bekommt, sondern darüber gleichzeitig auch noch Zustand injizieren und umherreichen kann.

Diese Arbeit hat hoffentlich verständlich genug gezeigt, dass es in JavaScript genügt, einen rein auf Fuktionsobjekten und Delegation beruhenden Ansatz, verpackt in ein »Module Pattern«, zu wählen, um Talent-basierte Komposition zu ermöglichen, die sich ausschließlich auf das Mixin-Muster sowie Trait-spezifische Funktionalität stützt.

Die meisten praxisnahen Anwendungsfälle in JavaScript benötigen wirklich nicht die viel zu komplexen Programmieransätze wie sie z.B. von den beiden erwähnten Bibliotheken verwendet werden. Der im Artikel propagierte Ansatze zur Nutzung Funktions-getriebener Talente-Muster macht den eigenen Code unabhängig von den Objektkompositions-Werkzeugen Dritter.

Die Beschränkung auf dieses eine Grundmuster, welches die Anwendung grundlegender Sprachkernmerkmale wie Function, call und apply sowie closure, context und scope nur variiert, sichert immer den sauberen Ansatz sowie schlanke und bedarfsgerecht umgesetzte Talente.


Anhang

unterstützender Quellcode:

Function.prototype.before = function (behaviorBefore, target) {
  var proceedAfter = this;

  return function () {
    var args = arguments;

    behaviorBefore.apply(target, args);
    return proceedAfter.apply(target, args);
  };
};
Function.prototype.after = function (behaviorAfter, target) {
  var proceedBefore = this;

  return function () {
    var args = arguments;

    proceedBefore.apply(target, args);
    return behaviorAfter.apply(target, args);
  };
};
Function.prototype.around = function (behaviorAround, target) {
  var proceedEnclosed = this;

  return function () {
    return behaviorAround.call(target, proceedEnclosed, behaviorAround, arguments, target);
  };
};

Function.modifiers.adviceTypes.before-after-around ist die tragfähigere Umsetzung aller oben bereitgestellten und vereinfachten Code-Varianten.


  • Dieses Dokument wird öffentlich über google drive und über blogspot geteilt. Jeder darf kommentieren und ist hiermit dazu aufgefordert. Eingeladene Leute haben zusätzlich Editier-Rechte.

  • Das Dokument gibt es auch als gist.


geschrieben mit StackEdit.


  1. de.wikipedia.org: JavaScript; en.wikipedia.org: JavaScript; developer.mozilla.org: JavaScript
  2. javascriptweblog.wordpress.com: »A fresh look at JavaScript Mixins«
  3. webreflection.blogspot.de: »Flight Mixins Are Awesome!«
  4. petsel.github.io: »JavaScript Code Reuse Patterns«
  5. github.com/petsel/javascript-code-reuse-patterns: »Composition«
  6. developer.mozilla.org: this keyword
  7. developer.mozilla.org: Function Object
  8. developer.mozilla.org: Inheritance and the prototype chain
  9. developer.mozilla.org: Function.prototype.call
  10. developer.mozilla.org: Function.prototype.apply
  11. Software Composition Group (SCG), University of Bern: Trait
  12. Software Composition Group (SCG), University of Bern
  13. SCG, University of Bern: »Traits: Composable Units of Behavior«
  14. SCG, University of Bern: »Stateful Traits«
  15. SCG, University of Bern: SCG: Talents
  16. SCG, University of Bern: »Talents: Dynamically Composable Units of Reuse«