Suche
In kleinen Schritten zu Funktionaler Programmierung

Checkpoint Java: Funktional mit Java 9

Sven Ruppert

© Shutterstock / Suttha Burawonk

Der Übergang von Objekt-orientierter zu Funktionaler Programmierung erscheint oft fließend. Ein in kleine Schritte aufgeteiltest Beispiel macht die Unterschiede, Vorzüge und Einsatzmöglichkeiten deutlich und lädt zum Ausprobieren ein.

In den nächsten Teilen dieser Kolumne werden wir uns dem Thema „Funktional und Reaktiv mit Core Java“ zuwenden. Das bedeutet, dass wir die Bereiche Funktionale Programmierung und Reaktive Systeme nach und nach unter die Lupe nehmen werden.

Es gibt schon viele Systeme und Bibliotheken, die dieses Thema voll und ganz abdecken oder es zumindest versuchen. Zum einen gibt es die Ansätze, die sich ausschließlich mit den funktionalen Aspekten beschäftigen – ich meine hiermit nicht die Entwicklung neuer Sprachen –, und zum anderen die reinen reaktiven Ansätze. Meistens sind diese Ansätze und Frameworks schon weit entwickelt und demnach mächtig und umfangreich. Wir werden uns zu Beginn mit den funktionalen Aspekten beschäftigen und diese dann mit den reaktiven kombinieren. Ziel ist es, die grundsätzlichen Gedanken dabei zu beleuchten und minimalistische Lösungen basierend auf dem JDK zu bauen.

Wir werden für diese Artikelreihe von Beginn an mit JDK 9 arbeiten, auch wenn dieses zum jetzigen Zeitpunkt noch nicht final verfügbar ist. Das OpenJDK kann hier gefunden und heruntergeladen werden. Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source-Projekts Functional-Reactive. Die Sourcen befinden sich hier.

Checkpoint Java

In dieser Kolumne klopft der Autor Sven Ruppert (Vaadin) Java auf alltägliche Probleme ab. Er gibt hilfreiche Tipps und Tricks, wie Entwickler gängige Stolperfallen vermeiden und klareren Code schreiben können. Einen besonderen Blick wirft er auf die neuen Möglichkeiten von Funktionaler und Reaktiver Programmierung. Alle Teile der Kolumne Checkpoint Java finden sich hier.

Wann ist etwas objekt-orientiert und wann funktional?

Ganz zu Beginn stellt sich die Frage aller Fragen. Ab wann sprechen wir von objekt-orientiert und ab wann von funktional? Diese Frage kann einen recht lange beschäftigen, wenn man Java als Grundlage verwendet. Beginnen wir mit ein paar Beispielen.

Seit Java 8 gibt es die FunctionalInterfaces. Diese werden gerne zusammen mit den Lambdas verwendet. Um Lambdas verwenden zu können, muss man ein FunctionalInterface als Ziel haben. Gehen wir also davon aus, dass wir einen CalculationService haben.

Wer hier mehr über die Anwendung selbst erfahren möchte, die hier angesprochen ist, der schaut sich bitte die Vaadin-Kolumne Backend Meets Frontend von mir an. Dort entwickeln wir eine Web-Anwendung für Kinder, mit der wir unter anderem Matheaufgaben stellen und auswerten.

Unser CalculationService muss in der Lage sein, Matheaufgaben zu stellen und zu überprüfen, ob das Ergebnis vom Benutzer richtig berechnet worden ist. Starten wir hier mit einer einfachen Definition und schauen uns einmal den dazugehörigen Screenshot an (Abb. 1).

Abb. 1: Wir fangen klein an: mit einer einfachen Addition

Was wir hier für den ersten Entwurf benötigen, ist eine Methode, die uns eine Aufgabe liefert und ein Ergebnis. Also erwarten wir drei Werte, die zusammengehören. Nennen wir diese Klasse MathTask. Diese könnte wie folgt aussehen.

public class MathTask {
  
  private int first;
  private int second;
  private int result;

  public MathTask(int first , int second , int result) {
    this.first = first;
    this.second = second;
    this.result = result;
  }

  @Override
  public String toString() {
    return "MathTask{" +
           "first=" + first +
           ", second=" + second +
           ", result=" + result +
           '}';
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (! (o instanceof MathTask)) return false;

    MathTask mathTask = (MathTask) o;

    if (first != mathTask.first) return false;
    if (second != mathTask.second) return false;
    return result == mathTask.result;
  }

  @Override
  public int hashCode() {
    int result1 = first;
    result1 = 31 * result1 + second;
    result1 = 31 * result1 + result;
    return result1;
  }

  public int getFirst() {
    return first;
  }

  public int getSecond() {
    return second;
  }

  public int getResult() {
    return result;
  }
}

(Zum Quelltext auf GitHub)

Das ist jede Menge Quelltext, um lediglich drei Werte zu erhalten. Man beachte, das es keine set-Methoden für die Attribute gibt. Aber gehen wir erst einmal weiter.

Die Methode, die dieses Ergebnis liefert, könnte im ersten Entwurf recht einfach aussehen: public MathTask nextMathTask(). Bisher ist alles rein objekt-orientiert. Wirklich? Sagen wir erst einmal: Ja, es ist rein objekt-orientiert.

public class MathService {
  public MathTask nextMathTask() {
    int first = 0;
    int second = 0;
    // create task values...
    return new MathTask(first , second , (first + second));
  }
}

Nun haben wir soweit alles zusammen, um eine Aufgabe stellen zu können. Wie sieht es nun mit dem Ergebnis aus? Hier wollen wir entweder rein transient das Ergebnis mit dem vom Benutzer vergleichen und dann ein Feedback geben, ob es gleich ist, oder das Ergebnis (der Vergleich) wird weiter verarbeitet. Wenn wir das speichern möchten, um später eine Auswertung zu erstellen, benötigen wir noch das Ergebnis, das von dem Benutzer eingegeben worden ist.

Nun können wir zwei Wege gehen. Der erste besteht darin, das Ergebnis vom Benutzer zusätzlich im Objekt zu halten, also in der Instanz der Klasse MathTask. Das bedeutet, dass wir den Zustand dieser Instanz modifizieren. Der zweite Lösungsweg besteht darin, eine weitere Klasse zu erzeugen, die aus dem Ergebnis des Benutzers besteht und aus der dazu gehörigen Aufgabe. Der Leser mag geneigt sein, eine dritte Version mit ins Rennen zu bringen. Diese recht oft genannte Lösung besteht darin, dass die Klasse MathTask das Attribut humanResult bekommt. Wenn das Ergebnis vorliegt, wird eine weitere Instanz der Klasse MathTask erzeugt und dann der Wert zusätzlich zu den schon aus dem ersten Schritt bekannten Werten gesetzt. Dann wird die erste Instanz nicht modifiziert und verworfen. Ich lasse diese Lösung weg, da es sich hierbei meiner Ansicht nach um eine hybride Lösung handelt.

Das Thema der Unveränderbarkeit (Immutability) ist ein Teil der funktionalen Betrachtungsweise. Es kann aber genauso gut in der objekt-orientierten Welt Anwendung finden. Ich entscheide mich hier für die zweite Lösung und erzeuge eine weitere Klasse MathTaskResult mit den Attributen mathTask vom Typ MathTask und einem weiteren Attribut humanResult vom Typ int.

public class MathTaskResult {

  private MathTask mathTask;
  private int humanResult;

  public MathTaskResult(MathTask mathTask , int humanResult) {
    this.mathTask = mathTask;
    this.humanResult = humanResult;
  }

  public MathTask getMathTask() {
    return mathTask;
  }

  public int getHumanResult() {
    return humanResult;
  }
  //SNIPP toString / hashcode / equals ..
}

Nun haben wir beide Ergebnisse. Aber wo wird der Vergleich gemacht ob die beiden Werte gleich sind? Ist die Aufgabe richtig gelöst worden? Sicherlich ist diese Ausgabe trivial, da es sich lediglich um einen Vergleich handelt. Aber wo genau gehört dieser Vergleich hin?

In der UI kann man auf die Eingabe des Benutzers reagieren und dann einen visuellen Effekt erstellen, der den Vergleich darstellt. Aber ist das ein Teil der UI? Oder ist das Ergebnis des Vergleichs ein Attribut der Klasse MathTaskResult? Oder ist es das Ergebnis einer Methode vom MathService? Es ist auf jeden Fall kein Bestandteil der UI, da es eine logische Operation ist. Lediglich die grafische Repräsentation des Ergebnis (Ja/Nein) ist Teil der UI. Nun stellt sich noch die Frage, ob es Teil der Klasse MathTaskResult oder MathService wird.

Rein objekt-orientiert würde ich es in der Klasse MathTaskResult verorten. Dort würde die Lösung auch eher so aussehen, das der Boolean-Wert on the fly berechnet  würde. Wenn man über das Speichern der Ergebnisse nachdenkt, wäre es eher ein zu berechnender Wert anstatt eines persistenten Attributes. Also extrahieren wir die Validation in eine Methode des MathService: public boolean validate(MathTaskResult result);.

Erstes Refactoring: der Dataholder

Wenn man nun ein wenig abstrakter formulieren möchte, haben wir nicht nur Integer-Werte, die wir zu einer Aufgabe formulieren. Wir werden auch nicht nur eine einfache Addition erstellen wollen. Wenn wir uns die vier Grundrechenarten als erste Realisierung für unseren MathService zum Ziel setzen, können wir unser MathTask dementsprechend anpassen. Als erstes beschreiben wir generisch ein Tripel an Werten beliebigen Typs. Wir nennen diese Klasse auch Tripel. Die Implementierung ist hier zu finden.

public class Tripel<T1, T2, T3> {
  private T1 t1;
  private T2 t2;
  private T3 t3;

  public Tripel(final T1 t1, final T2 t2, final T3 t3) {
    this.t1 = t1;
    this.t2 = t2;
    this.t3 = t3;
  }

  public T3 getT3() {
    return t3;
  }

  public T1 getT1() {
    return t1;
  }

  public T2 getT2() {
    return t2;
  }
  //SNIP hashcode / equals / toString..
}

Wir wollen lediglich drei Werte zusammen halten, deren Typ wir noch nicht genau definiert haben. Zur Laufzeit allerdings wollen wir Typsicherheit erlangen. Das können wir auch mit unserem MathTaskResult so generisch formulieren.

public class Pair<T1, T2> {
  private T1 t1;
  private T2 t2;

  public Pair(final T1 t1, final T2 t2) {
    this.t1 = t1;
    this.t2 = t2;
  }

  public T1 getT1() {
    return t1;
  }

  public T2 getT2() {
    return t2;
  }
  //SNIP hashcode / equals / toString..
}

Was wir an dieser Stelle verloren haben, sind die ausdrucksstarken Attributnamen. Aber mit ein klein wenig Aufwand bekommen wir dieses wieder.

public class MathTask extends Tripel<Integer, Integer, Integer> {

  public MathTask(Integer first , Integer second , Integer result) {
    super(first , second , result);
  }
  public int getFirst() {
    return getT1();
  }

  public int getSecond() {
    return getT2();
  }

  public int getResult() {
    return getT3();
  }
  
  @Override //optional 
  public String toString() {
    return "MathTask{" +
           "first=" + getT1() +
           ", second=" + getT2() +
           ", result=" + getT3() +
           '}';
  }
}

Genauso verfahren wir mit der Klasse MathTaskResult.

 public class MathTaskResult extends Pair<MathTask, Integer> {

  public MathTaskResult(MathTask mathTask , Integer integer) {
    super(mathTask , integer);
  }

  public MathTask getMathTask() {
    return getT1();
  }

  public int getHumanResult() {
    return getT2();
  }

  @Override //optional
  public String toString() {
    return "MathTaskResult{" +
           "mathTask=" + getT1() +
           ", humanResult=" + getT2() +
           '}';
  }
}

(Zum Quelltext auf GitHub)

Nun haben wir den generischen Teil der Datenholder extrahiert und uns aber für die Verwendung in der fachlichen Logik die Bezüge zur fachlichen Semantik erhalten.

Zweites Refactoring: der MathService

Nun gehen wir den MathService an. Hier haben wir bisher zwei Methoden. Zum einen zum Erzeugen einer Aufgabe und zum anderen zum verifizieren, ob die Aufgabe richtig gelöst worden ist. Gehen wir erst einmal zum Erzeugen der Aufgabe. Wenn man mit Kindern die ersten Matehaufgaben löst,
beginnt man meist in einem Bereich von null bis zehn. Das bedeutet, dass wir verschiedene
Anforderungen an die Aufgabe selbst haben.

  • Das Ergebnis darf nicht größer als 10 sein.
  • Das Ergebnis darf nicht kleiner als 0 sein.
  • Evtl darf 0 nicht als Ergebnis enthalten sein.
  • Keiner der Parameter darf 0 sein.
  • Es soll nicht dieselbe Aufgabe direkt hintereinander nochmal gestellt werden.

Das sind schon recht viele Anforderungen, die es zu berücksichtigen gilt. Wenn man nun noch alle Grundrechenarten berücksichtigt, kommen noch weitere spezifische Anforderungen hinzu. Zum Beispiel bei der Division, dass nur ganzzahlige Ergebnisse berücksichtigt werden sollen. Und vieles mehr, was einem dann auch auf einmal bewusst wird, wenn das Kind zum ersten Mal die Anwendung verwendet. Ich lasse hier die möglichen Validationen bei der Eingabe selbst bewusst weg.

Als erstes kümmern wir uns nur um die Addition. Die Grundrechenart selbst als Auswahlmöglichkeit ist demnach kein Parameter der Methode zum Erzeugen der nächsten Aufgabe. Eine erste Realisierung könnte demnach wie folgt aussehen:

public class MathService {
  
  public MathTask nextMathTask(int paramBoundFirst , int paramBoundSecond) {
    final Random random = new Random(System.nanoTime());
    int first = random.nextInt(paramBoundFirst);
    int second = random.nextInt(paramBoundSecond);
    return new MathTask(first , second , (first + second));
  }

  public boolean validate(MathTaskResult mathTaskResult){
    return mathTaskResult.getHumanResult() ==
           mathTaskResult.getMathTask().getResult();
  }
}

(Zum Quelltext auf GitHub)

Nun stellt sich mir die Frage, ob wir für diese beiden Methoden wirklich eine Instanz einer Klasse benötigen. Jede Methode wird einmal aufgerufen, ohne dass es einen Zustand gibt, der über mehr als einen Methodenaufruf erhalten bleiben muss. Auch dieses ist ein Merkmal funktionaler Programmierung, also die Zustandslosigkeit selbst. Lediglich innerhalb einer Methode darf ein Zustand solange gehalten werden, bis die Methode terminiert. Wie sieht es aus, wenn wir unseren Code weiter extrahieren, damit wir keine Instanz der Klasse MathService halten müssen? Diese stellt ja selbst einen Zustand dar.

Verlagern wir erst einmal alles in ein Interface, sodass auch nicht mit der Zeit aus Versehen ein Attribut auf Klassenebene der Klasse MathService entsteht. Aus der Klasse wird ein Interface und die Methoden selbst werden static definiert

public interface MathService {
  public static MathTask nextMathTask(int paramBoundFirst , int paramBoundSecond) {
    final Random random = new Random(System.nanoTime());
    int first = random.nextInt(paramBoundFirst);
    int second = random.nextInt(paramBoundSecond);
    return new MathTask(first , second , (first + second));
  }

  public static boolean validate(MathTaskResult mathTaskResult){
    return mathTaskResult.getHumanResult() ==
           mathTaskResult.getMathTask().getResult();
  }
}

Damit reduziert sich der Quelltext:

public class Main {
  public static void main(String[] args) {
    final MathTask mathTask = MathService.nextMathTask(5,5);

    int humanResult = 0;
    MathTaskResult mathTaskResult = new MathTaskResult(mathTask , humanResult);

    boolean validate = MathService.validate(mathTaskResult);

    System.out.println("mathTask = " + mathTask);
    System.out.println("mathTaskResult = " + mathTaskResult);
    System.out.println("validate = " + validate);
  }
}

Nun noch static Imports und wir erhalten:

public class Main {
  public static void main(String[] args) {
    final MathTask mathTask = nextMathTask(5,5);

    int humanResult = 0;
    MathTaskResult mathTaskResult = new MathTaskResult(mathTask , humanResult);

    boolean validate = validate(mathTaskResult);

    System.out.println("mathTask = " + mathTask);
    System.out.println("mathTaskResult = " + mathTaskResult);
    System.out.println("validate = " + validate);
  }
}

Wir haben nun in einem Interface zwei statische Methoden ohne haltende Objektreferenz. Bisher wurde davon ausgegangen, dass ein Ergebnis nicht den Wert 10 überschreiten darf. Also dass die Eingangswerte selbst nicht den Wert fünf überschreiten. Um das für den Benutzer der Methode einfacher zu gestallten, werden wir die Signatur ändern. Es wird nur noch der Maximal-Ergebniswert angegeben. Die Werte für die jeweiligen Parameter bestimmt dann die Methode intern selbst. Wir erhöhen damit die Kapselung in der Art, dass wir weniger Wissen über die interne Funktion der Methode besitzen müssen.

  public static MathTask nextMathTask(int maxResultValue) {
    final Random random = new Random(System.nanoTime());
    int bound = maxResultValue / 2;
    int first = random.nextInt(bound);
    int second = random.nextInt(bound);
    return new MathTask(first , second , (first + second));
  }

Wir haben nun eine Transformation des Max-Ergebniswerts auf eine Instanz der Klasse MathTask. Das können wir nun auch wie folgt formulieren: Function<Integer,MathTask>. Mit der Bedingung, dass der Integer nicht größer als der maximale Ergebniswert sein darf. Und nicht vergessen, es soll auch nicht negativ werden. Aber schreiben wir das nun erst einmal als Funktion auf. Hier gibt es seit Java 8 das FunctionalInterface Function<A,B>.

  public static Function<Integer, MathTask>; mathTask = new Function<Integer, MathTask> () {
    @Override
    public MathTask apply(Integer maxResultValue) {
      final Random random = new Random(System.nanoTime());
      int bound = maxResultValue / 2;
      int first = random.nextInt(bound);
      int second = random.nextInt(bound);
      return new MathTask(first , second , (first + second));
    }
  };

Hierbei handelt es sich um die herkömmliche Schreibweise. Dank der neuen Syntax können wir das auch kompakter schreiben.

  public static Function<Integer, MathTask> mathTask = (maxResultValue) -> {
    final Random random = new Random(System.nanoTime());
    int bound = maxResultValue / 2;
    int first = random.nextInt(bound);
    int second = random.nextInt(bound);
    return new MathTask(first , second , (first + second));
  };

(Zum Quelltext auf GitHub)

Programmieren wir nun funktional? Meiner Meinung nach ist das bisher noch nicht funktional im reinen Sinne. Aber wir nähern uns dem an und wir beachten immer mehr funktionale Aspekte. Bisher haben wir versucht, Funktionen direkt zu formulieren. Aber eigentlich möchte man doch eine Instanz haben. Woher diese also bekommen? Schreiben wir das letzte Beispiel nochmals um, in der Art, dass man eine Funktion als Rückgabewert bekommt.

  public static Function<Integer, MathTask> mathTaskFunction() {
    return (maxResultValue) -> {
      final Random random = new Random(System.nanoTime());
      int bound = maxResultValue / 2;
      int first = random.nextInt(bound);
      int second = random.nextInt(bound);
      return new MathTask(first , second , (first + second));
    };
  }

Nun haben wir eine Methode, die uns eine Instanz einer Funktion liefert, mit der wir nun frei arbeiten können. Da wir sicher sind, dass diese Instanz ausschließlich von uns verwendet wird. Das Gleiche realisieren wir nun mit der Methode validate.

  static Function<MathTaskResult, Boolean> validate(){
    return (mathTaskResult) -> mathTaskResult.getHumanResult() ==
                             mathTaskResult.getMathTask().getResult();
  }

Wie sieht nun der dazugehörige Quelltext in der Verwendung aus?

int maxResultValue = 10;
int humanResult = 0;
final Function<Integer, MathTask> mathTaskFunction = MathService.mathTaskFunction();
final MathTask mathTask = mathTaskFunction.apply(maxResultValue);

final BiFunction<Integer, MathTask, MathTaskResult> resultBiFkt
        = (result , task) -> new MathTaskResult(task , result);

final MathTaskResult mathTaskResult = resultBiFkt.apply(humanResult , mathTask);
         
final Function<MathTaskResult, Boolean> validate = MathService.validate();
final Boolean apply = validate.apply(mathTaskResult);

Eine weitere Funktion habe ich an der Stellen mit definiert. Diese dient der Transformation von MathTask und dem Ergebnis vom Benutzer humanResult in eine Instanz vom Typ MathTaskResult.

Was aber ist der Vorteil von diesen Funktionen? Sicher ist auf jeden Fall, das man mit
Funktionen nicht mehr Probleme lösen kann als mit objekt-orientiertem Java. Die Vorteile müssen also an anderer Stelle zu finden sein. Beginnen wir einmal ein wenig das Vorhandene umzuschreiben.

final BiFunction<Integer, MathTask, MathTaskResult> resultBiFkt
         = (result , task) -> new MathTaskResult(task , result);

Boolean result = resultBiFkt
         .andThen(validate())
         .apply(humanResult , mathTaskFunction().apply(maxResultValue));

Hier haben wir nun eine Eigenschaft der Funktionen verwendet. Gemeint ist hier die Kombination von Funktionen mit andThen. Damit werden Funktionen hintereinander gehängt. Das Ergebnis der vorherigen Funktion ist der Eingang der nächsten Funktion. Das Ergebnis ist wiederum eine Funktion: Function<A, B> andThen Function<B,C> wird zu Function<A, C>.

Nun verwenden wir hier aber noch eine BiFunction, sprich eine Funktion, die zwei Eingangsparameter hat und ein Ausgangsparameter. Auch diese kann man mit andThen mit einer einfachen Funktion kombinieren. Aber was ist, wenn wir dies ein wenig symmetrischer gestallten wollen? Diese BiFunktion werden wir recht schnell los, indem wir hier wieder die beiden Eingangsparameter zusammenfassen in einem Pair.

    final Function<Integer, Pair<MathTask, Integer>> integerPairFunction = mathTaskFunction()
        .andThen(mathTask -> new Pair<>(mathTask , humanResult));
        
    final Function<Pair&<MathTask, Integer>, MathTaskResult> mapToMathTaskResult
        = (resultPair) -> new MathTaskResult(resultPair.getT1() , resultPair.getT2());

    Boolean aBoolean = integerPairFunction
        .andThen(mapToMathTaskResult)
        .andThen(validate())
        .apply(maxResultValue);

Nun sind wir die BiFunction los. Allerdings ist diese Lösung nicht zu gebrauchen, da wir zum einen die Variable humanResult in der Funktion selbst verwenden. Das sollte ja eher ein Eingangsparameter sein. Zum anderen sind zwischen den Transformationen Interaktionen vom Benutzer notwendig. Die Werte aus der Instanz MathTask müssen ebenfalls noch in der UI angezeigt werden. Lange Rede kurzer Sinn, wir müssen das wieder in zwei Abschnitte teilen.

//t1 - show the values on Screen
    final MathTask mathTask = mathTaskFunction().apply(maxResultValue); 
    
    final Function<Pair<MathTask, Integer>, MathTaskResult> mapToMathTaskResult
        = (resultPair) -> new MathTaskResult(resultPair.getT1() , resultPair.getT2());
        
//t2 - button was pressed
    final Boolean aBoolean = mapToMathTaskResult //t2 - button was pressed
        .andThen(validate())
        .apply(new Pair<>(mathTask , humanResult));

Nun kann die Transformation noch extrahiert werden in die Methode public stativ Function<Pair<MathTask, Integer>, MathTaskResult> mapToMathTaskResul().

//t1 - show the values on Screen
    final MathTask mathTask = mathTaskFunction().apply(maxResultValue);
    
//t2 - button was pressed
    final Boolean aBoolean = mapToMathTaskResult().andThen(validate()).apply(new Pair&amp;amp;lt;&amp;amp;gt;(mathTask , humanResult)

Nun kann man Funktionen auch mittels compose(..) miteinander kombinieren. Hier ist die Abfolge genau invers zu andThen(..). Damit erhalten wir eine weitere Möglichkeit, das letzte Statement zu formulieren.

//t1 - show the values on Screen
    final MathTask mathTask = mathTaskFunction().apply(maxResultValue);
    
//t2 - button was pressed
    final Boolean aBoolean = validate().compose(mapToMathTaskResult()).apply(new Pair&amp;amp;lt;&amp;amp;gt;(mathTask , humanResult));

(Zum Quelltext auf GitHub)

Fazit

Was haben wir jetzt erreicht? Wir haben ein wenig bestehenden Quelltext umgeformt und die ersten Ideen und Ansätze von Funktionaler Programmierung ausprobiert. Wir sind absichtlich keinen formalen Weg gegangen und haben bisher auch nicht eindeutig festgelegt, ab wann wir von Objekt-orientierter und wann von Funktionaler Programmierung sprechen. Das werden wir in den nächsten Teilen weiter erarbeiten und uns um das Einbinden in bestehenden Legacy-Quelltext und auch das Erstellen von neuen Projekten und Modulen kümmern. Den Quelltext findet ihr hier. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.