Montag, 11. November 2013

Implementierung des Flow Design Lösungsansatzes



Aufbauend auf den vorigen Eintrag Flow Design:


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);
    }


1.2 Zwei Ausgänge - auch kein Problem:


    function forAllJobDefinitions(
             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.



    class CompareWithLastNumberAndMaxCount {
        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":



    function sort(outCallback: (string) => void): void {
        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:



    function forEachJobMarkOrderIfPossible(
             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:



    class GenerateOrderString extends events.EventEmitter {
        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);
        }
    }




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.


    class CompareWithLastNumberAndMaxCount extends events.EventEmitter {
        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.


    export class Sort extends events.EventEmitter {
        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);
        }

    }


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.

Keine Kommentare:

Kommentar veröffentlichen