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
- Delegationsprinzipien
- Von den Schwierigkeiten die durch Rollen etablierten Konzepte und Begrifflichkeiten auf JavaScript anzuwenden
- »Talents: Dynamically Composable Units of Reuse« + + + »Talente: dynamisch komponier- und wiederverwendbare Einheiten«
- Vom »Mixin« zum »Trait« - die Wirkungsweise rein Funktions-getriebener »Talente« in JavaScript
- Die derzeitige Systematik funktionaler »Talente«
- Ausgewählte »Smart Talent«-Muster
- Schlusskommentar
- Anhang
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 this
6 im Rumpf eines function
7-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 call
9 bzw. apply
10 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.
»Promoted Talent«
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.
- de.wikipedia.org: JavaScript; en.wikipedia.org: JavaScript; developer.mozilla.org: JavaScript ↩
- javascriptweblog.wordpress.com: »A fresh look at JavaScript Mixins« ↩
- webreflection.blogspot.de: »Flight Mixins Are Awesome!« ↩
- petsel.github.io: »JavaScript Code Reuse Patterns« ↩
- github.com/petsel/javascript-code-reuse-patterns: »Composition« ↩
- developer.mozilla.org: this keyword ↩
- developer.mozilla.org: Function Object ↩
- developer.mozilla.org: Inheritance and the prototype chain ↩
- developer.mozilla.org: Function.prototype.call ↩
- developer.mozilla.org: Function.prototype.apply ↩
- Software Composition Group (SCG), University of Bern: Trait ↩
- Software Composition Group (SCG), University of Bern ↩
- SCG, University of Bern: »Traits: Composable Units of Behavior« ↩
- SCG, University of Bern: »Stateful Traits« ↩
- SCG, University of Bern: SCG: Talents ↩
- SCG, University of Bern: »Talents: Dynamically Composable Units of Reuse« ↩
No comments:
Post a Comment