Ich konnte es nicht lassen und habe es nun nicht nur gezeichnet sondern auch implementiert. Dazu habe ich TypeScript verwendet, da ich raufinden
wollte, wie das damit funktioniert und ob das Ergebnis einfach und
übersichtlich wird.
Diese Kombination TypeScript – Flow Design –
EventBasedComponents scheint mir noch zu wenig bearbeitet zu sein. Dies ist
auch der Grund, warum ich meine Erkenntnisse zusammenschreibe. Vielleicht
findet sich jemand, der Verbesserungen sieht oder vielleicht hilft es zumindest
jemanden, der sich auch damit beschäftigen will.
Da ich bereits vor ein paar Wochen ein TypeScript
Beispiel mit Event Based Components versucht habe, begann ich mit diesem
Ansatz. Dazu kann man sehr gut die node.js Event-Loop einsetzen. Im Anschluss
daran habe ich das Beispiel kopiert und versucht die node:event wieder
rauszunehmen, und so nur mit gewöhnlichen Callback Funktionen zu arbeiten.
Die beiden Lösungen findet man auf github OrderedJobs-FD-Kata-TS.
(entspricht dem 3. Lösungsansatz vom vorhergehenden Eintrag: Flow Design)
Ich bin eigentlich auf der Suche, wie man so ein FD
Diagramm möglichst einfach nach JavaScript bekommt. Dazu will ich mir ein paar
Methoden aus dem Sourcecode rausholen, um es in Zukunft vielleicht einfacher zu
haben.
1. Lösung mit Callbacks:
1.1 Eine ganz einfache Transformation, mit nur einem Ein-
und Ausgang:
function generateOrderString(
jobDefinitionList: jd.Domain.JobDefinition[],
outCallback: (string) => void): void {
var orderString = "";
jobDefinitionList.sort(function (left, right) {
return left.order === right.order ?
0 : (left.order < right.order ? -1 : 1)
});
jobDefinitionList.forEach(function (jobDefinition) {
orderString += jobDefinition.job;
});
outCallback(orderString);
}
jobDefinitionList: jd.Domain.JobDefinition[],
outCallback: (string) => void): void {
var orderString = "";
jobDefinitionList.sort(function (left, right) {
return left.order === right.order ?
0 : (left.order < right.order ? -1 : 1)
});
jobDefinitionList.forEach(function (jobDefinition) {
orderString += jobDefinition.job;
});
outCallback(orderString);
}
1.2 Zwei Ausgänge - auch kein Problem:
jobDefinitionList: jd.Domain.JobDefinition[],
outCallback: (IJobDefinition) => void,
afterCallback: (any) => void): void {
var self = this;
jobDefinitionList.forEach(function (jobDefinition) {
outCallback(jobDefinition);
});
afterCallback(jobDefinitionList);
}
1.3 Mehrere Eingänge
Das klappt so nicht, dazu muss man dann schon eine Klasse
schreiben.
private numberOfSortedJobs: number;
private maxJobsCount: number;
private _jobDefinitionList: jd.Domain.JobDefinition[];
public initialize(jobDefinitionList: jd.Domain.JobDefinition[]): void {
this.numberOfSortedJobs = 0;
this.maxJobsCount = jobDefinitionList.length;
this._jobDefinitionList = jobDefinitionList;
}
public process(
jobsAreSortedCount: number,
outAllDoneCallback: (any) => void,
notAllDoneButIncreasedCallback: (any) => void): void {
var LastNumberOfSortedJobs = this.numberOfSortedJobs;
this.numberOfSortedJobs = jobsAreSortedCount;
...
}
}
1.4
"Verdrahten":
createJobDefinitionList(
function (jobDefinitionList: jd.Domain.IJobDefinition[]) {
sortJobDefinitions(jobDefinitionList, outCallback);
});
}
Bei mehreren Callbacks in einem Aufruf wird diese
Schreibweise jedoch sehr verwirrend. Dazu
ein komplexeres Beispiel:
jobDefinitionList: jd.Domain.JobDefinition[],
jobDefinitionListCallback: (any) => void): void {
var setOrderAndIncrement = new SetOrderAndIncrement();
var testIfAllPreJobsAreDone = new TestIfAllPreJobsAreDone();
testIfAllPreJobsAreDone.initialize(jobDefinitionList);
forAllJobDefinitions(jobDefinitionList, function (jobDefinition) {
testIfJobIsSorted(jobDefinition, function (jobDefinition) {
testIfAllPreJobsAreDone.process(jobDefinition,
function (jobDefinition) {
setOrderAndIncrement.process(jobDefinition,
function () { });
});
})
}, function (jobDefinitionList: jd.Domain.JobDefinition[]): void {
jobDefinitionListCallback(jobDefinitionList);
});
}
Man sieht in diesem Beispiel auch, dass hier die
"Kästchen" mit mehreren Eingängen, dann instanziiert werden müssen.
Man könnte sie aber auch global instanziieren, allerdings hat man dann immer
noch das "Problem", dass die Methode (in unserem Beispiel die Methode
process) aufgerufen werden muss und man so wieder kein einheitliches Bild
erhält.
testIfJobIsSorted(jobDefinition, function
() { });
setOrderAndIncrement().process(jobDefinition,
function () { });
2. Lösung mit Event
Based Components:
Wie oben schon erwähnt, wurde dazu die Node.js Event-Loop
verwendet. Dazu braucht man eine Referenz auf node.
/// <reference
path='../../../DefinitelyTyped/node/node.d.ts'/>
import events = require("events");
(Diese Referenzen werden angeblich mit Visual Studio
Unterstützung selbst aufgelöst.)
Dieselben Beispiele wie oben, bei der Callback Variante:
2.1 Eine ganz einfache Transformation, mit nur einem Ein-
und Ausgang:
public process(jobDefinitionList: jd.Domain.JobDefinition[]): void {
var orderString = "";
jobDefinitionList.sort(function (left, right) {
return left.order === right.order ?
0 : (left.order < right.order ? -1 : 1)
});
jobDefinitionList.forEach(function (jobDefinition) {
orderString += jobDefinition.job;
});
this.emit('out', orderString);
}
}
2.2 Zwei Ausgänge:
class ForAllJobDefinitions extends events.EventEmitter {
public process(jobDefinitionList: jd.Domain.JobDefinition[]): void {
var self = this;
jobDefinitionList.forEach(function (jobDefinition) {
self.emit('out', jobDefinition);
});
this.emit('after', jobDefinitionList);
}
}
public process(jobDefinitionList: jd.Domain.JobDefinition[]): void {
var self = this;
jobDefinitionList.forEach(function (jobDefinition) {
self.emit('out', jobDefinition);
});
this.emit('after', jobDefinitionList);
}
}
2.3 Mehrere Eingänge
Das ist im Gegensatz zu oben kein Ausreißer, sondern
sieht aus wie alle anderen Klassen. Man hat eben eine Methode mehr.
private numberOfSortedJobs: number;
private maxJobsCount: number;
private _jobDefinitionList: jd.Domain.JobDefinition[];
public initialize(jobDefinitionList: jd.Domain.JobDefinition[]): void {
this.numberOfSortedJobs = 0;
this.maxJobsCount = jobDefinitionList.length;
this._jobDefinitionList = jobDefinitionList;
}
public process(jobsAreSortedCount: number): void {
var LastNumberOfSortedJobs = this.numberOfSortedJobs;
this.numberOfSortedJobs = jobsAreSortedCount;
if (jobsAreSortedCount === this.maxJobsCount) {
this.emit('allDone', this._jobDefinitionList);
} else {
...
}
}
}
2.4 "Verdrahten"
Das sieht jetzt ganz anders aus wie in der Callback
Variante und meiner Meinung nach viel übersichtlicher.
constructor() {
var createJobDefinitionList = new CreateJobDefinitionList();
var sortJobDefinitions = new SortJobDefinitions();
this._firstTask = createJobDefinitionList;
createJobDefinitionList.on("jobDefinitionList",
sortJobDefinitions.process.bind(sortJobDefinitions));
sortJobDefinitions.on("out", this.result.bind(this));
super();
}
private result(sortedJobs: string): void {
this.emit('out', sortedJobs);
}
private _firstTask: ICreateJobDefinitionList;
public process(): void {
this._firstTask.process();
}
}
Eine Schwierigkeit dabei ist dieses JavaScript Problem
mit dem this Pointer. Leider wurde es auch unter TypeScript nicht versteckt.
(Zumindest bisher noch nicht - ist ja noch in Version 0.9.1.1, aber ich
befürchte, dass es auch nicht gelöst wird)
Will man in der process Methode auch den richtigen this
Pointer zugreifen, weil man die Methode _firstTask aufrufen will, dann muss man
dies, bei der Definition des Events mitgeben!
Statt
createJobDefinitionList.on("jobDefinitionList",
sortJobDefinitions.process);
also
createJobDefinitionList.on("jobDefinitionList",
sortJobDefinitions.process.bind(sortJobDefinitions));
Das macht es schon sehr unschön. Vielleicht gibt es da
eine einfache Lösung, die ich noch nicht gesehen habe.
Es ist in dieser Variante, der Schreibaufwand zwar
größer, aber es ist immer gleich und auch bei komplexeren
"Verdrahtungen" einfach zu verstehen:
class ForEachJobMarkOrderIfPossible extends events.EventEmitter {
constructor() {
var forAllJobDefinitions = new ForAllJobDefinitions();
var testIfJobIsSorted = new TestIfJobIsSorted();
var testIfAllPreJobsAreDone = new TestIfAllPreJobsAreDone();
var setOrderAndIncrement = new SetOrderAndIncrement();
this._firstTask = forAllJobDefinitions;
this._testIfAllPreJobsAreDone = testIfAllPreJobsAreDone;
forAllJobDefinitions.on("out",
testIfJobIsSorted.process.bind(testIfJobIsSorted));
testIfJobIsSorted.on("notSorted",
testIfAllPreJobsAreDone.process.bind(testIfAllPreJobsAreDone));
testIfAllPreJobsAreDone.on("allPreJobsDone",
setOrderAndIncrement.process.bind(setOrderAndIncrement));
forAllJobDefinitions.on("after", this.result.bind(this));
super();
}
private _firstTask: IForAllJobDefinitions;
private _testIfAllPreJobsAreDone: ITestIfAllPreJobsAreDone;
private result(jobDefinitionList: jd.Domain.JobDefinition[]): void {
this.emit('jobDefinitionList', jobDefinitionList);
}
public process(jobDefinitionList:
{ job: string; preJobs: string[]; order: number }[]): void {
this._testIfAllPreJobsAreDone.initialize(jobDefinitionList);
this._firstTask.process(jobDefinitionList);
}
constructor() {
var forAllJobDefinitions = new ForAllJobDefinitions();
var testIfJobIsSorted = new TestIfJobIsSorted();
var testIfAllPreJobsAreDone = new TestIfAllPreJobsAreDone();
var setOrderAndIncrement = new SetOrderAndIncrement();
this._firstTask = forAllJobDefinitions;
this._testIfAllPreJobsAreDone = testIfAllPreJobsAreDone;
forAllJobDefinitions.on("out",
testIfJobIsSorted.process.bind(testIfJobIsSorted));
testIfJobIsSorted.on("notSorted",
testIfAllPreJobsAreDone.process.bind(testIfAllPreJobsAreDone));
testIfAllPreJobsAreDone.on("allPreJobsDone",
setOrderAndIncrement.process.bind(setOrderAndIncrement));
forAllJobDefinitions.on("after", this.result.bind(this));
super();
}
private _firstTask: IForAllJobDefinitions;
private _testIfAllPreJobsAreDone: ITestIfAllPreJobsAreDone;
private result(jobDefinitionList: jd.Domain.JobDefinition[]): void {
this.emit('jobDefinitionList', jobDefinitionList);
}
public process(jobDefinitionList:
{ job: string; preJobs: string[]; order: number }[]): void {
this._testIfAllPreJobsAreDone.initialize(jobDefinitionList);
this._firstTask.process(jobDefinitionList);
}
}
Für JavaScript Programmierer ist die erste Variante mit
den Callbacks inzwischen sehr üblich, auch durch die asynchroner Programmierung
(mit Promises z.B.).
Mir gefällt aber im Moment doch die EBC Variante besser.
Allerdings sollte man bedenken: Hätte ich von dieser
Methodik noch nie gehört und würde mir den Code ansehen, ich würde ihn für sehr
eigenartig halten.
Leider scheint sich TypeScript für die FlowDesign
Implementierung nicht besser zu eignen, als C#.
Vielleicht bin ich aber auch noch nicht tief genug in
TypeScript eingetaucht und es gibt noch Notationen, die praktischer sind.