Java Magazin   2.2018 - Reaktive Programmierung

Erhältlich ab:  Januar 2018

Autoren / Autorinnen: 
Carina Schipper ,  
Heinz Kabutz ,  
Vadym KazulkinRodion Alukhanov ,  
Giamir Buoncristiani ,  
Gernot StarkeRalf D. Müller ,  
Lars Vogel ,  
,  
Manfred Steyer ,  
Boris FresowMarkus Günther ,  
Hartmut SchlosserMelanie FeldmannCarina Schipper ,  
Michael Müller ,  
Lars Röwekamp ,  
Theodora Milona ,  
Masoud Amri ,  
Adam Bien ,  
Sven Ruppert ,  
Oliver Heger ,  
Michael Kuhn

Der Urknall. Die Mutter aller Reaktionen. Vor fünfzehn Milliarden und ein paar Jahren hat es ziemlich gewaltig gekracht. Genauer gesagt ist es zu einer thermischen Reaktion gekommen. Damals steckte das ganze Universum noch in einem Kern mit enormer Dichte (1096 g/cm3) und unglaublich hoher Temperatur (1030 ºC). Irgendwann hat es dann ordentlich geknallt, und alles ist in die noch nicht vorhandene Luft geflogen. Was nach der Explosion übriggeblieben ist, ist offensichtlich.

Die Frage danach, warum der Urknall stattgefunden hat, ist natürlich höchst spannend. Stephen Hawking hat dazu zum Beispiel gesagt: „Zu fragen, was war vor dem Beginn des Universums, ist so sinnlos wie die Frage: Was ist nördlich vom Nordpol?“ Die reaktive Programmierung ist in der Java-Welt sicherlich kein neuer großer Big Bang. Trotzdem erlebt sie gerade eine kleine Boom-Phase. Die Java-Welt reagiert endlich und liefert Antworten auf die Herausforderungen – in Form von Java 9, Spring 5 und vielen anderen Bibliotheken. Warum reaktiv aktuell so gefragt ist, lässt sich einfach erklären: Seit dem Entwurf des reaktiven Modells hat sich einiges verändert. Moderne Use Cases mit großen Datenmengen, asynchronen Events und Echtzeitverarbeitung lösen einen verspäteten Mini-Urknall in Sachen reaktive Programmierung aus. Sie ist also quasi ein Spätzünder.

Allerdings ist unser Titelthema nicht allein dafür verantwortlich, dass ich vom Urknall spreche. Es hat auch etwas mit Melanie Feldmann zu tun. Der Hintergrund ist eigentlich ganz simpel: Sie steht auf Bilder aus dem Weltall. Eines davon ziert sogar ihren Desktop – nur leider nicht mehr lange. Wenn Sie diese Zeilen lesen, wird Melanie uns schon verlassen haben. Sie stürzt sich in ihr nächstes Abenteuer und hat mir das Steuer des Java Magazins überlassen. „Irgendwann ist nun einmal alles vorbei“, hat Rennfahrerlegende Niki Lauda gesagt. Natürlich wollen wir unsere Melanie fulminant und gebührend verabschieden. Deshalb widmen wir ihr das Cover dieses Java Magazins – sozusagen den letzten großen Knall.

Natürlich geht es auch nach dem Knall beim Java Magazin in gewohnter Manier weiter. Wir haben uns auch für 2018 wie immer viel vorgenommen. Erst einmal werden wir aber die Korken knallen lassen, denn das Java Magazin hat Geburtstag. Vor zwanzig Jahren erblickte es das Licht der Welt. Wir lassen es uns selbstverständlich nicht nehmen, das in einer Ausgabe mit ein paar Überraschungen für unsere Leser angemessen zu feiern. Danach steht dann auch schon im April die JAX vor der Tür. Wir werden natürlich dort sein und freuen uns schon auf ein Wiedersehen mit Ihnen. Bis bald, ein spannendes, reaktives Jahr, und lassen Sie es ordentlich krachen. Im positiven Sinne natürlich.

schipper_carina_sw.tif_fmt1.jpgCarina Schipper | Redakteurin

Mail Website Twitter Xing Google

Die vor zwei Jahren gegründete Initiative MicroProfile ist angetreten, die Lücke zwischen dem Enterprise-Java-Standard Java EE und den Praxisanforderungen Microservices-basierter Anwendungen zu schließen. Dank der Unterstützung führender Application-Server-Hersteller mausert sich das Projekt langsam, aber sicher zu einem neuen De-facto-Standard.

Microservices und Java EE? Das passt nicht wirklich zusammen. So zumindest die landläufige Meinung. Auf der einen Seite ein angeblich zu schwergewichtiger Application Server, der als Laufzeitumgebung für die ach so kleinen Microservices nicht wirklich tauglich scheint, auf der anderen Seite fehlende APIs zur Unterstützung typischer Herausforderungen im Umfeld stark verteilter Architekturen. Dieses Problem haben 2016 auch einige der führenden Java-EE-Application-Server-Hersteller erkannt – unter ihnen IBM, Red Hat, Tomitribe, Payara und KumuluzEE – und eine entsprechende Initiative ins Leben gerufen: MicroProfile [1].

Das Ziel der Initiative ist es, das über Jahre aufgebaute Wissen der Java-EE-Community auch für die Entwicklung und den Betrieb von Microservices zu nutzen, ohne dabei an die starren Restriktionen des Java-Enterprise-Standards gebunden zu sein. Erreicht werden soll dies durch ein schmales Bundle von Java-EE-APIs, ergänzt um neue, speziell auf die Anforderungen von Microservices zugeschnittene APIs.

In Siebenmeilenstiefeln unterwegs

Nach einer kurzen Phase der Orientierung, in der das Projekt u. a. an die Eclipse Foundation übergeben wurde, geht es für Java-EE-Verhältnisse in Siebenmeilenstiefeln voran: Das bestehende Momentum der JEE-Community als Hebel nutzen und organisch um den Bedarf der Microservices-Community ergänzen, so der Plan. Und dieser Plan scheint aufzugehen. In nur wenigen Monaten ist es gelungen, eine Reihe sinnvoller, Microservices-relevanter APIs mit bestehenden Java-EE-7-APIs zu kombinieren und sie in regelmäßigen MicroProfile-Releases zu veröffentlichen. Egal ob Health Check, Metrics, Fault Tolerance, JWT Propagation oder Distributed Configuration Management, MicroProfile scheint die richtigen Antworten – sprich APIs – im Gepäck zu haben. Dabei versucht das mittlerweile in der Version 1.2 angelangte MicroProfile ein Best of both Worlds abzubilden und das Rad, wenn immer möglich und sinnvoll, nicht zwingend neu zu erfinden. So setzt MicroProfile zum Beispiel für die Implementierung von RESTful Services auf die etablierten Java-EE-APIs JAX-RS 2.0, CDI 1.0 und JSONP 1.0.

Roadmap und Backlog

Auch für die nächsten beiden Versionen, die wahrscheinlich im Abstand von etwa drei Monaten erscheinen werden, gibt es schon konkrete Pläne. Version 1.3 soll ein API für verteiltes Tracing sowie für den Support von OpenAPI (Swagger) erhalten. Mit der Version 2.0 soll im Anschluss ein Upgrade auf Java EE 8 erfolgen. Dies spiegelt sich u. a. im Anheben der bestehenden Java-EE-APIs (JAX RS 2.1, CDI 2.0 und JSONP 1.1) und in der Hinzunahme von JSON-B 1.0 wider. Die Liste der Wünsche beziehungsweise der geplanten APIs ist damit aber noch lange nicht am Ende angelangt. Das Backlog ist laut MicroProfile bis zum Rand gefüllt. Lediglich die Reihenfolge der Umsetzung scheint noch in den Sternen zu stehen. Features wie verteiltes Logging, Service Registry und Discovery, Messaging und Eventing oder aber Async und Reactive stehen dabei ebenso auf der Agenda wie die Ergänzung weiterer Java-EE-APIs (z. B. Bean Validation und JPA). Natürlich wird auch an eine permanente Adaption von MicroProfile an die Entwicklung der restlichen Java-Community gedacht. Dies zeigt sich im Backlog beispielsweise durch den Wunsch nach einer verstärkten Unterstützung von Lambdas, des Stream-API und des Java-9-Modularisierungskonzepts.

An open forum to optimize Enterprise Java for a microservices architecture by innovating across multiple implementations and collaborating on common areas of interest with a goal of standardization.

Zitat: MicroProfile, www.microprofile.io

Fazit

Mithilfe des explizit auf Microcservices ausgerichteten De-facto-Standards Eclipse MicroProfile scheint eine Möglichkeit geschaffen worden zu sein, ein für die Java-EE-Community bereits verloren geglaubtes Terrain zurückzuerobern. MicroProfile ist zwar (noch) kein wirklicher Standard, setzt aber, wann immer möglich und sinnvoll, auf standardisierte APIs und hilft so, das in den letzten zehn Jahren aufgebaute Java-EE-Wissen zukünftig auch im Umfeld von Microservices einsetzen zu können. Möglich wurde dies durch die enge Zusammenarbeit mehrerer, eigentlich konkurrierender Application-Server-Hersteller, die allesamt erkannt haben, dass die Evolution im Java-EE-Standard deutlich zu langsam ist, um mit den schnellen Entwicklungen im Umfeld von Microservices mithalten zu können. Gleichzeitig wünscht die Community aber eine größtmögliche Unabhängigkeit von einzelnen Herstellern und proprietären Lösungen.

Die ersten drei Versionen von MicroProfile haben gezeigt, wie durch eine sinnvolle Kombination etablierter Java-EE-APIs und neuer innovativer Microservices-APIs in kürzester Zeit ein echter Mehrwert geschaffen werden kann. Abzuwarten bleibt, ob sich diese positive Entwicklung auch in der Zukunft mit der bisher an den Tag gelegten Qualität und dem gleichzeitig hohen Tempo weiter realisieren lässt. Die Paneldiskussion zum Thema MicroProfile auf der diesjährigen JavaOne lässt uns diesbezüglich auf jeden Fall positiv in die Zukunft blicken und macht Lust auf mehr. David Blevins von Tomitribe betonte im Rahmen des Panels, dass er MicroProfile als eine Art Inkubator für neue Ideen rund um das Thema Enterprise Java und Microservices sieht. Es ist somit nicht auszuschließen, dass das eine oder andere API, nachdem es sich in der Community etabliert hat, am Ende auch Einzug in den Standard hält. Auch die vollständige Standardisierung von MicroProfile als weiteres offizielles Java EE Profile wäre denkbar. Die Wahl des Namens kommt sicherlich nicht von ungefähr. Wie genau es weitergehen wird, hängt dabei sicherlich auch von der allgemeinen Zukunft des Java-EE-Standards (EE4J) nach der Übergabe an die Eclipse Foundation ab. In diesem Sinne: Stay tuned!

roewekamp_lars_sw.tif_fmt1.jpgLars Röwekamp ist Geschäftsführer der OPEN KNOWLEDGE GmbH und berät seit mehr als zehn Jahren Kunden in internationalen Projekten rund um das Thema Enterprise Computing.

Twitter

Die beiden Entwicklerinnen Julia und Su unterhalten sich beim Mittagessen über Frauen in der IT-Branche.

Julia: „Ich bin den ganzen Tag von Männern umgeben. Damit habe ich eigentlich kein Problem, aber es wäre echt schön, wenn endlich mal eine Frau in unser Team dazukäme ...“

Su: „Das kenn ich sehr gut. Nach einer Weile gehen mir die Jungswitze und Rülpser schon ein wenig auf die Nerven.“

Video: "Failure" as Success: The Mindset, the Methods and the Landmines

Julia: „Bei mir ist das nicht ganz so schlimm. Aber man muss auch dazu sagen, es gibt einfach noch viel zu wenig Mädels in der IT. Es hat sich in den letzten Monaten einfach keine Frau beworben.“

Su: „Das finde ich echt schade.“

Julia: „Leider nichts Neues für mich … in meinem Studiengang war ich meist die einzige Frau im Saal.“

Su: „Hätte mein Papa mir damals vor fünfzehn Jahren nicht ein paar Computerspiele gezeigt, wäre ich selbst nie auf die Idee gekommen, Informatik zu studieren.“

Julia: „Man verbindet mit dem Begriff Informatiker oder Programmierer einfach so ein Stereotyp, das extrem veraltet und überzogen ist.“

Su: „Dabei kann man als Informatiker so viele verschiedene Sachen machen ...“

Julia: „Ja, die Welt steht einem offen!“

milona_devops_1.tif_fmt1.jpgAbb. 1: Julia und Su beim Mittagessen

In der Tech-Branche fehlen Frauen

Leider ist das, was Su und Julia erleben, keine Seltenheit im IT-Business. Laut einer Kienbaum-Studie liegt der Frauenanteil der gesamten deutschen IT-Industrie bei ungefähr 17 Prozent, in Informatikstudiengängen beträgt er nur rund 21 Prozent. Auch in großen Konzernen sieht es in Sachen Gender Diversity nicht besonders rosig aus: bei Apple [1] lag der Frauenanteil in technischen Abteilungen bei lediglich 22 Prozent. Microsoft setzt auf eine systematische Förderung von Frauen durch spezielle Mentoring-Programme und die Unterstützung von Frauennetzwerken. Das Unternehmen wurde bereits vielfach für sein Diversity-Engagement ausgezeichnet. Und doch lag der Frauenanteil im Tech-Bereich trotz dieser Bemühungen im Jahr 2015 auch hier bei nur 17 Prozent.

Vielfalt erhöht Gewinneinnahmen und sorgt für Kreativität in Teams

Dass Unternehmen seit einiger Zeit um eine größere Vielfalt in ihren Teams bemüht sind, hat viele Gründe. Zum einen haben Studien [2] bewiesen, dass Teams, die gleichermaßen aus Männern wie aus Frauen bestehen, kreativer arbeiten und viel bessere Ergebnisse erzielen. Auch Kundenwünsche werden genauer umgesetzt. Zum anderen erhöhen kulturelle und geschlechtliche Vielfalt tatsächlich auch die Gewinneinnahmen in Unternehmen [3]. Divers aufgebaute Teams neigen eher dazu, Neues auszuprobieren und ihr Wissen miteinander zu teilen als Teams, die, wie in der IT-Branche häufig üblich, männerdominiert sind. Abgesehen davon hat dies natürlich auch ökonomische Gründe. Es gibt einfach zu wenig männliche IT-Fachkräfte, die alle vorhandenen Stellen besetzen könnten. Allein im Jahr 2016 gab es 51 000 offene Stellen in der Branche, und es werden von Jahr zu Jahr mehr.

Wie weckt Mann mehr Interesse?

Das traurige Klischee: Informatiker sind pickelige Außenseiter, möchten lieber für sich bleiben und den ganzen Tag über Hardware reden. Diese Art von Schubladendenken schadet Frauen und Männern gleichermaßen. Vielen sagt der Begriff Informatik nichts Konkretes, und meist entsprechen die Vorstellungen über die Tätigkeit kaum der Realität. Dabei bietet die IT-Branche viele Freiheiten, eine gute Bezahlung und überraschend viele kreative Entfaltungsmöglichkeiten. Auch sind Diplomatie, ein gewisses Maß an Empathie und Kommunikationstalent gefragt.

Es fängt oft in der Erziehung an: Viele Mädchen werden von Technik isoliert und erhalten nicht genügend Zugang zu Naturwissenschaften. In Schulen sollten Fächer wie Informatik unbedingt stärker gefördert und auch für Mädchen attraktiver gestaltet werden. Auch mithilfe von Veranstaltungen wie dem Girls’ Day oder gemeinnützigen Organisationen wie Girls Who Code sollen junge Frauen für Informatik begeistert werden, damit ihre Talente und ihr Potenzial erkannt werden. Immer mehr Initiativen zur Frauenförderung werden gestartet. Es ist wichtig, dass sich Frauen in der IT-Branche ihrer Verantwortung bewusst werden, sich miteinander vernetzen und gegenseitig motivieren. So werden weibliche Vorbilder geschaffen, Wissen wird verbreitet und das Networking untereinander verstärkt.

Nach einer von Microsoft durchgeführten Studie [4] wurde festgestellt, dass das geringe Interesse junger Frauen an der IT-Branche meist daran liegt, dass es nicht genügend Praxiserfahrung und weibliche Vorbilder gibt. Zudem muss man vor allem mit Klischees und Vorurteilen aufräumen, die innerhalb der Informatik herrschen, denn diese halten Frauen davon ab, sich mit Informatik zu befassen und schüchtern ein. Auch Hochschulen sollten den Studiengang Informatik moderner gestalten und aktiver auf Schülerinnen und Frauen zugehen. Es gibt an einigen Hochschulen sogenannte Mentorinnennetzwerke mit dem Ziel, Studentinnen beim Übergang in den Beruf zu unterstützen, gerade in naturwissenschaftlichen Fächern. Mithilfe von geschlossenen Konferenzen wie dem Ada Lovelace Festival, das jährlich in Berlin stattfindet [5], können sich Frauen über aktuelle Themen wie Big Data und Female Leadership austauschen.

Wenn Unternehmen mehr Frauen für IT-Berufe gewinnen wollen, müssen sie aktiv eine Unternehmenskultur schaffen, die Frauen nicht abschreckt. Dafür genügen schon kleine Signale wie ein monatlich stattfindendes Mittagessen unter Kolleginnen oder die aktive Unterstützung von Communities wie den Techettes Frankfurt [6], die sich für mehr Frauen im Tech-Bereich engagieren.

Will man Frauen mithilfe von Stellenausschreibungen bewusster ansprechen, braucht man Feedback von Frauen, die selbst in diesem Bereich tätig sind. Diese beantworten dann Fragen wie „Fühlst du dich als Frau angesprochen?“ oder „Würdest du dich auf diese Stelle bewerben? Wenn nicht, warum?“

milona_devops_2.tif_fmt1.jpgAbb. 2: Gewinn durch Vielfältigkeit: Wie hoch ist die Wahrscheinlichkeit, dass Unternehmen andere an Leistung übertreffen? Quelle: McKinsey-Analyse

Flexible Arbeitsmodelle und Problembewusstsein sorgen für mehr Vielfalt

Um den Traum der Gender Diversity in Unternehmen verwirklichen zu können, sollten diese flexible Arbeitsmodelle anbieten, die alle Beschäftigten gleichermaßen berücksichtigen. Vor allem Frauen haben auch heutzutage noch mit dem Problem zu kämpfen, dass Familie und Beruf schwer zu vereinbaren sind. Als optimale Lösung bietet sich an, dass die Teams im Unternehmen selbst entscheiden, wann der Tag beginnt oder an welchen Tagen auch die Möglichkeit zum Home Office besteht.

Ein vorurteilsfreier und wertschätzender Umgang zwischen allen Kolleginnen und Kollegen ist essenzielle Grundvoraussetzung und muss auf allen Managementebenen gelebt werden. Frauenfeindliche Bemerkungen und herabwürdigende Sprüche vergiften eine respektvolle Arbeitsatmosphäre und verbieten sich von selbst. Die Verantwortung, am Arbeitsplatz Vielfalt und Diversität zu leben, liegt bei jedem Einzelnen. Viel zu oft entsteht aus Unsicherheit, welches Verhalten nun korrekt ist, ein gekünsteltes Klima. Ist das Umfeld aber von Vertrauen und gegenseitigem Respekt geprägt, kann offen kommuniziert werden, unabhängig von Geschlecht und Rolle. Unternehmen, die hierauf großen Wert legen, sind erfolgreicher und lassen die Konkurrenz meist hinter sich [7]. Wie also erreicht man dies am besten?

Jocelyn Margan, Chief Operating Officer bei Snagajob, hat vier Tipps für Unternehmen und Mitarbeiter [8] aufgestellt, die dabei helfen könnten (Kasten: „Vier Tipps, um mehr Gender Diversity zu erreichen“).

Vier Tipps, um mehr Gender Diversity zu erreichen

  • Sich mit dem Thema Vielfältigkeit am Arbeitsplatz beschäftigen: Sich das Problem bewusst zu machen und sich mit dem Thema auseinanderzusetzen, ist der erste Schritt zu einer besseren Unternehmenskultur. Hierzu gehören beispielsweise Statistiken darüber, wie hoch der Frauenanteil im Unternehmen ist.

  • Lerne deine Kolleginnen besser kennen: Studien haben gezeigt, dass viele Frauen nicht gerne im Mittelpunkt stehen und auch nicht mit ihrer getanen Arbeit protzen. Jocelyn Margan empfiehlt, dass man sich mit seinen Kolleginnen über berufliche Ziele austauscht, ein gemeinsames Mittagessen für die Frauen (Girls’ Lunch) einführt oder Mentoring anbietet.

  • Hilfe beim Netzwerken: Frauen die Möglichkeiten zu geben, sich mit anderen zu vernetzen, macht ebenfalls einen großen Unterschied. Interne oder externe Jobempfehlungen für seine Kolleginnen auszusprechen, trägt auch zu einem ausgewogenen Geschlechtergleichgewicht bei.

  • Auch auf die kleinen Dinge Acht geben: Das Gegenüber beim Sprechen nicht zu unterbrechen, seine oder ihre Ideen wertzuschätzen … Das klingt zunächst selbstverständlich. Aber es ist immer wieder überraschend, dass Frauen als Minderheiten in IT-Berufen immer wieder mit solchen Respektlosigkeiten konfrontiert werden.

Netzwerken für Frauen beitreten und etwas verändern

Wir befinden uns auf einem guten Weg, denn die Zahlen werden auf lange Sicht von Jahr zu Jahr ausgeglichener: Die sich einschreibenden IT-Studentinnen sind im Jahr 2014 auf einen Rekordwert von 23 Prozent gestiegen. Auch im Jahr 2015 befanden sich unter 37 219 Erstsemestern des Studiengangs Informatik immerhin 8 519 Frauen [9]. Je mehr Frauen sich für ein Informatikstudium oder eine Ausbildung im IT-Bereich entscheiden, desto höher stehen die Chancen, dass sie einmal später zu Julia und Su ins Unternehmen stoßen. Nach einem Gespräch mit Silke aus ihrer HR-Abteilung wollen sie die Stellenausschreibungen überarbeiten, um sie ansprechender für Frauen zu gestalten. Zudem haben sie einen monatlichen Girls’ Lunch organisiert, an dem sich alle Frauen aus dem Unternehmen zu einem gemeinsamen Mittagessen treffen, um sich miteinander auszutauschen. Die beiden sind außerdem einem Netzwerk für Frauen im Tech-Bereich beigetreten, um dort auf Gleichgesinnte zu treffen, sich miteinander auszutauschen und vielleicht auch mehr Frauen für das eigene Unternehmen zu begeistern.

milona_theodora_sw.tif_fmt1.jpgTheodora Milona ist Frontend-Entwicklerin bei cosee und beschäftigt sich gerne mit Themen wie Gender Diversity. Wenn sie nicht fleißig in die Tasten haut und sich Angular um die Ohren wirft, fertigt sie gerne Illustrationen an oder spielt Computerspiele.

Twitter

Da Maschinen intelligentes Verhalten überwiegend durch Menschen lernen, ist das Beibringen ein weiteres grundlegendes Konzept zum Maschinellen Lernen. Realisieren lässt sich das durch explizites und deklaratives Beibringen sowie implizites Lernen.

Das explizite Beibringen ist dadurch charakterisiert, dass Gelerntes mit Worten beschrieben und erklärt werden kann. Ein Beispiel wäre, wenn der Nutzer dem Digitalassistenten folgenden Satz diktiert und erwartet, dass der Digitalassistent sich die Angaben merkt:

Benutzer: Bestelle eine 15-kg-Packung trockenes Hundefutter mit Lammfleisch von der Marke Happy Dog.

Maschine: Soll ich mir deine Angaben merken, damit du es nächstes Mal einfacher hast?

Benutzer: Ja, gerne. Merke es dir unter „Bestelle Hundefutter“.

Es passiert selten, dass ein Benutzer eine Bestellung so perfekt mit den wichtigen Parametern angibt. Dennoch reichen diese Angaben zur Bestellung noch nicht aus. Denn bevor sich der Digitalassistent eine Bestellung merken kann, ist es wichtig, wie er mit unvollständigen Angaben umgeht. Listing 1 zeigt die Implementierung der Sentence Component, die diese Aussage verarbeitet.

Listing 1: Sentence Component „FutterBestellen“

@SentenceComponent
public class FutterBestellen
{
  private Anbieter anbieter;
  private Futter futter;
  private DateInterval liefertermin;
  private BankKonto bankKonto;
 
  @Sentence("Bestelle von irgendeinem Anbieter irgendein Futter " +
"für irgendeinen Liefertermin")
  public void codeSegment()
  {
// Die Implementierung dieser Methode ist an dieser Stelle unwichtig!
  }
 
  @NaturalTypeMethod(naturalName = "von irgendeinem Anbieter",
naturalType = "Anbieter")
  public void setAnbieter(String anbieter){ this.anbieter = anbieter; }
 
  @Question ("Welches Futter möchtest du bestellen?")
  @NaturalTypeMethod (naturalName = "irgendein Futter", naturalType = "Futter")
  public void setFutter(Futter futter){ this.futter = futter; }
 
  @Question ("Wann darf es geliefert werden?")
  @NaturalTypeMethod(naturalName = "für irgendeinen Liefertermin",
naturalType = "Datum")
  public void setLiefertermin (DateInterval liefertermin){ this. liefertermin = liefertermin; }
 
  @Question ("Von welchem Konto möchtest du es zahlen?")
  @NaturalTypeMethod(naturalName = null, naturalType = "Bankkonto")
  public void setBankkonto(Bankkonto bankkonto){ this. bankkonto= bankkonto; }
}

Obwohl die Benutzereingabe sehr genau ist, fehlen immer noch einige Parameter, um eine Bestellung durchzuführen. Die Sentence Component aus Listing 1 erwartet einen Anbieter des zu bestellenden Futters sowie eine Marke, einen Liefertermin und ein Konto, von dem die Bestellung bezahlt wird. Dabei sind Anbieter und Marke optional. Falls sie nicht angegeben werden, wird die Sentence Component einfach den günstigsten Anbieter nehmen. Alle anderen Parameter geben durch die Annotation @Question eine W-Frage an, die beim Fehlen der Parameter im Eingabetext durch die Plattform gestellt wird.

@Question ("Welches Futter möchtest du bestellen?")
@NaturalTypeMethod (naturalName = "irgendein Futter", naturalType = "Futter")
public void setFutter(Futter futter){ this.futter = futter; }
 
@Question ("Wann darf es geliefert werden?")
@NaturalTypeMethod(naturalName = "für irgendeinen Liefertermin", naturalType = "Datum")
public void setLiefertermin (DateInterval liefertermin){ this. liefertermin = liefertermin; }
 
@Question ("Von welchem Konto möchtest du es zahlen?")
@NaturalTypeMethod(naturalName = null, naturalType = "Bankkonto")
public void setBankkonto(Bankkonto bankkonto){ 
this. bankkonto= bankkonto; 
}
 

Wir habe im zweiten Teil dieser Artikelserie ausführlich über die natürlichen Datentypen gesprochen und gezeigt, wie durch den Mechanismus der natürlichen Datentypen aus dem Eingabetext die dazu passenden Wortfolgen in technische Datentypen konvertiert werden. Der Datentyp Futter, gekennzeichnet durch naturalType = "Futter", konvertiert die Wortfolge „eine 15-kg-Packung trockenes Hundefutter mit Lammfleisch von der Marke Happy Dog“ in den technischen Datentyp Futter und weist ihm die Inhalte zu:

Futter futter = new Futter();
futter.setTier("Hund");
futter.setInhalte( {"Lammfleisch"} );
futter.setArt("Trocken");
futter.setGewicht(15);
futter.setEinheit("kg");
futter.setAnzahl(1);
futter.setMarke("Happy Dog");

Dem technischen Datentyp Futter wird dann durch die Plattform die Methode setFutter(Futter futter) zugewiesen. Mit dem Attribut naturalName = "irgendein Futter" wird ein Teil des mit der Annotation @Sentence markierten Definitionssatzes als Variable deklariert. Ein Ausnahmefall liegt vor, wenn naturalName auf NULL gesetzt wird. Dann erwartet die Plattform, dass die Angabe des natürlichen Datentyps durch eine andere Sentence Component erfolgt. Dieser Aspekt ist wichtig, um zwischen dem Eingabetext und einer Sentence Component aus der Menge der in der Plattform vorhandenen Sentence Components eine Übereinstimmung zu finden. Für jeden Eingabetext führt die Plattform eine linguistische Analyse durch. Dadurch separiert sie zuerst die Sätze voneinander und wendet an jedem Eingabesatz einen Matching-Algorithmus an, um dafür eine Übereinstimmung mit den im System vorhandenen Sentence Components zu finden. Als Erstes wird das Prädikat des Eingabesatzes mit jedem Prädikat der Definitionssätze der Sentence Components verglichen. Die Übereinstimmung der Prädikate wird stark gewichtet, da dies als Absicht des Satzes verstanden wird. Danach kommen Subjekt, Objekt und die Anzahl der übereinstimmenden Parameter zum Tragen (Abb. 1).

amri_maschinelles_lernen_1.tif_fmt1.jpgAbb. 1: Linguistische Analyse eines Eingabesatzes

Die Plattform zählt also die Methode setBankkonto() mit der Annotation @NaturalTypeMethod(naturalName = null, naturalType = "Bankkonto") nicht zu den Parametern der Sentence Component mit dem Definitionssatz @Sentence ("Bestelle von irgendeinem Anbieter irgendein Futter von irgendeiner Marke für irgendeinen Liefertermin"). In diesem Beispiel stimmen das Prädikat Bestelle und die Parameter Futter und Marke mit den Inhalten des Eingabetexts überein. Der Parameter Anbieter hat keine Annotation @Question, und damit kann die Plattform den Benutzer zur Ergänzung des Parameters nicht befragen. Der Parameter Liefertermin wurde durch eine W-Frage beantwortet. Damit ist eine akzeptable Übereinstimmung des Eingabesatzes mit der Sentence Component erreicht.

Um die Bestellung durchführen zu können, müssen die Kontodaten durch die W-Frage „Von welchem Konto möchtest du bezahlen?“ geklärt werden. Die Antwort wird durch den Faktensammler mit der Annotation @ Sentence("Zahle den Betrag von irgendeinem Kreditinstitut mit irgendeiner IBAN und irgendeinem Passwort") realisiert (Listing 2).

Listing 2: Sentence Component „BankAccount“

@SentenceComponent
public class BankAccount
{
  private Iban iban;
  private Kreditinstitut kreditinstitut;
  private Passwort passwort;
 
  @Sentence("Zahle den Betrag von irgendeinem Kreditinstitut mit irgendeiner IBAN und irgendeinem Passwort")
  public BankData codeSegment()
  {
BankData  bankData = new BankData(kreditinstitut, iban, passwort);
KnowlageBase.insert("welches.bankkonto", bankData ) )
 
return new bankData;
  }
 
  @NaturalTypeMethod (naturalName = "von irgendeinem Kreditinstitut",
naturalType = " Kreditinstitut ")
  public void setInstitute (Kreditinstitut kreditinstitut){
    this. kreditinstitut = kreditinstitut;
  }
 
  @NaturalTypeMethod (naturalName = "mit irgendeiner IBAN", naturalType = "IBAN")
  public void setIban(Iban iban){
    this.iban = iban;
  }
 
  @NaturalTypeMethod (naturalName = "mit irgendeinem Passwort", naturalType = "Passwort")
  public void setPasswort(Passwort passwort){
    this.passwort = passwort;
  }
}

Wir haben im letzten Artikel Faktensammler, Themenersteller und Fakteninterpreten vorgestellt und gezeigt, wie Cogniology mit Schlussfolgerungen umgeht. Die Faktensammler sind Sentence Components, die die Antwort einer W-Frage in einem Objekt sammeln und sie in die Wissensbasis schreiben. Eine wichtige Eigenschaft der Faktensammler ist – neben der Speicherung der Fakten in der Wissensbasis –, dass sie das Faktenobjekt als Rückgabewert zurückgeben. Diese Rückgabe der Werte spielt bei der Programmierung in natürlicher Sprache eine große Rolle. Auf dieses Thema werden wir im nächsten Artikel ausführlich eingehen. Die Sentence Component Liefertermin konvertiert den Text „Montagvormittags und mittwochs ab 14 Uhr 30“ in eine Liste von Datumsintervallen. Somit wird die Konversation zwischen dem Digitalassistenten und dem Benutzer wie folgt ablaufen:

Benutzer: Bestelle eine 15-kg-Packung trockenes Hundefutter mit Lammfleisch von der Marke Happy Dog.

Maschine: Wann darf es geliefert werden?

Benutzer: Ich kann die Lieferung nur Montagvormittags und mittwochs ab 14 Uhr 30 entgegennehmen.

Maschine: Von welchem Konto möchtest du es zahlen?

Benutzer: Zahle den Betrag für Hundefutter aus meinem Sparkassenkonto mit IBAN DE12 3456789 00 112223344 und Passwort 1234.

Nachdem alle Parameter für die Bestellung vollständig angegeben worden sind, sucht die Plattform in der Wissensbasis, um herauszufinden, ob diese Sentence Component schon vorher von diesem Benutzer angegeben wurde. Sie schaut also, ob der Benutzer schon einmal Hundefutter bestellt hat. Ist das der Fall, geht sie davon aus, dass der Benutzer kein Interesse daran hat, den Vorgang durch den Digitalassistenten speichern zu lassen. Ist es nicht der Fall, wird er aufgrund der Anzahl der Parameter und der beteiligten Sentence Components diesen Vorgang als komplex einstufen und den Nutzer zwecks Automatisierung dieses Vorgangs ansprechen:

Maschine: Soll ich mir deine Angaben merken, damit du es nächstes Mal einfacher hast?

Benutzer: Ja, gerne. Merke es dir unter „Bestelle Hundefutter“.

Maschine: Also, mit „Bestelle Hundefutter“ meinst du: Bestelle eine 15-kg-Packung trockenes Hundefutter mit Lammfleisch von der Marke Happy Dog. Ich kann die Lieferung nur Montagvormittags und mittwochs ab 14 Uhr 30 entgegennehmen. Zahle den Betrag für Hundefutter aus meinem Sparkassenkonto mit IBAN DE12 3456789 00 112223344 und Passwort 1234.

Nachdem der Benutzer dies bestätigt hat, programmiert der Digitalassistent eine neue Sentence Component mit dem Definitionssatz @Sentence("Bestelle Hundefutter") und fügt sie zu den privaten Sentence Components des aktuellen Benutzers hinzu (Listing 3).

Listing 3: Sentence Component „BestelleHundefutter“

@SentenceComponent
public class AutomaticCreated_Besstelle_Hundefutter_001
{
  @Sentence("Bestelle Hundefutter")
  public void codeSegment()
  {
Anbieter anbieter = null;
 
Futter futter = new Futter();
futter.setTier("Hund");
futter.setInhaltList( {"Lammfleisch"} );
futter.setArt("Trocken");
futter.setGewicht(15);
futter.setEinheit("kg");
futter.setAnzahl(1);
futter.setMarke("Happy Dog");
 
List<DateInterval> moeglicheLiefertermine = null;
DateType dateType = new DateType();
Boolean matched  = null;
matched = dateType.matches("Montagvormittags und mittwochs ab 14 Uhr 30");
if(matched)
{
moeglicheLiefertermine = dateType.getDateIntervalList();
}
Liefertermin liefertermin = new Liefertermin();
liefertermin.setMoeglicheLiefertermine ( moeglicheLiefertermine );
moeglicheLiefertermine = liefertermin.codeSegment();
 
BankAccount bankAccount = new BankAccount();
bankAccount.setKreditinstitut("Sparkasse");
bankAccount.setInstitute("DE12 3456789 00 112223344");
bankAccount.setPassword("1234");
BankData bankData = bankAccount.codeSegment();
 
FutterBestellen futterBestellen = new FutterBestellen();
futterBestellen.setAnbieter( anbieter );
futterBestellen.setMoeglicheLiefertermine ( moeglicheLiefertermine );
futterBestellen.setBankDaten( bankData );
futterBestellen.codeSegment();
  }
}

Der Codegenerator der Plattform erstellt eine neue Klasse mit einem automatisch generierten Namen. Die Namen spielen dabei keine Rolle. Der Codegenerator der Plattform startet mit der ersten Sentence Component und bearbeitet sie in der Reihenfolge, wie sie vom Benutzer eingegeben wurden. Zu jedem Parameter der Sentence Component generiert er ein entsprechendes Objekt. Die Generierung der Parameter erfolgt von links nach rechts, wie sie im Definitionssatz der Sentence Component angegeben sind.

Die erste Sentence Component in der Hundefutterkonversation bezieht sich auf @Sentence("Bestelle von irgendeinem Anbieter irgendein Futter für irgendeinen Liefertermin"). Die Parameter sind: irgendein Anbieter, irgendein Futter und für irgendeinen Liefertermin. Für den ersten Parameter gab es keine Angabe im Eingabetext, daher wird die Variable mit NULL initiiert: Anbieter anbieter = null;. Der zweite Parameter ist der technische Datentyp des natürlichen Datentyps „eine 15-kg-Packung trockenes Hundefutter mit Lammfleisch von der Marke Happy Dog“:

Futter futter = new Futter();
futter.setTier("Hund");
futter.setInhaltList( {"Lammfleisch"} );
futter.setArt("Trocken");
futter.setGewicht(15);
futter.setEinheit("kg");
futter.setAnzahl(1);
futter.setMarke("Happy Dog");

Der dritte Parameter ist der Liefertermin, der durch die Sentence Component mit der Definition @Sentence("Ich nehme die Lieferung zu jeder Zeit an") bearbeitet wird (Listing 4).

Listing 4: Sentence Component „Terminvereinbarung“

@SentenceComponent
public class Terminvereinbarung
{
  private List<DateInterval> moeglicheLiefertermine;
 
  @DynamicValue
@Sentence("Ich nehme die Lieferung zu jeder Zeit an")
  public List<DateInterval> codeSegment()
  {
return moeglicheLiefertermine.
  }
 
  @NaturalTypeMethod (naturalName = "zu jeder Zeit", naturalType = "Datum")
  public void setMoeglicheLiefertermine (List<DateInterval> moeglicheLiefertermine){
    this. moeglicheLiefertermine = moeglicheLiefertermine;
  }
}

Da die Methode codeSegment() mit @DynamicValue annotiert ist, muss der natürliche Datentyp, also „Montagvormittags und mittwochs ab 14 Uhr 30“, immer neu berechnet werden. So ist es auch richtig, da Montagvormittags abhängig vom Durchführungszeitpunkt dieser Methode jeweils ein anderes Datum ergeben kann.

List<DateInterval> moeglicheLiefertermine = null;
DateType dateType = new DateType();
Boolean matched  = null;
matched = dateType.matches("Montagvormittags und mittwochs ab 14 Uhr 30");
if(matched)
{
moeglicheLiefertermine = dateType.getDateIntervalList();
}
Liefertermin liefertermin = new Liefertermin();
liefertermin.setMoeglicheLiefertermine ( moeglicheLiefertermine );
moeglicheLiefertermine = liefertermin.codeSegment();

Da die Plattform die Implementierung der Methode codeSegment nicht kennt, ruft sie diese immer wieder auf.

Die letzte Angabe ist die Bankverbindung, die zwar kein Parameter ist, jedoch in der Codegenerierung genau wie alle anderen Parameter behandelt wird:

BankAccount bankAccount = new BankAccount ();
bankAccount.setInstitute("Sparkasse");
bankAccount.setInstitute("DE12 3456789 00 112223344");
bankAccount.setPassword("1234");
BankData bankData = bankAccount.codeSegment();

Jetzt ist es soweit. Die Bestellung kann durchgeführt werden.

FutterBestellen futterBestellen = new FutterBestellen();
futterBestellen.setAnbieter( anbieter );
futterBestellen.setMoeglicheLiefertermine ( moeglicheLiefertermine );
futterBestellen.setBankDaten( bankData );
futterBestellen.codeSegment();

Diese erzeugte Sentence Component besteht aus sequenziellen Aufrufen einiger einfacher Methoden, also ohne bedingte Anweisungen, ohne Schleifen, ohne Abfrage der Wissensbasis und ohne komplizierte mathematische Berechnungen. Es ist jedoch ein gutes Beispiel für das Lernen durch Selbstprogrammierung. Wir werden in den nächsten Teilen dieser Artikelserie zeigen, wie wir dem Digitalassistent komplexe Aufgaben beibringen können.

Implizites Lernen: auf Einflüsse reagieren

Das implizite Lernen umfasst Fertigkeiten, die automatisch, ohne Nachdenken eingesetzt werden. Dazu gehören vor allem motorische Abläufe wie Gehen, Fahrradfahren oder Greifen. Das implizit Gelernte wird im Kontext einer bestimmten Prozedur, eines bestimmten Verhaltens abgerufen. Implizites Lernen versetzt eine Maschine in die Lage, durch Geräte wie Sensoren, Kameras oder Radar die Umwelt wahrzunehmen und angepasst auf die Bedürfnisse des jeweiligen Nutzers zu reagieren. Wenn es z. B. kalt wird, soll die Heizung eingeschaltet und die Jalousie heruntergelassen werden.

Benutzer: Es ist kalt.

Maschine: Was soll ich machen, wenn es kalt ist?

Benutzer: Schalte die Heizung an und lasse die Jalousie herunter.

Bisher hat unser Digitalassistent mit seinen Benutzern mittels Mikrofon und Lautsprecher kommuniziert. Dabei setzt er die beiden bekannten Technologien Voice to Text und Text to Voice ein, um die akustischen Signale aus dem Mikrofon in Text und in umgekehrter Richtung einen Text in akustische Signale zu konvertieren, die vom Lautsprecher ausgegeben werden. Diese Art der Konvertierung von Signalen in Text lässt sich auf jedes Gerät übertragen. So kann ein Sensor die Innentemperatur messen und das Ergebnis durch ein Funksignal an eine Software weiterleiten, die aus diesem Funksignal einen Satz erzeugt und ihn an die Plattform sendet. Die Plattform behandelt diesen Satz, als sei er von einem Menschen gesprochen worden, und führt die entsprechende Sentence Component aus (Abb. 2).

amri_maschinelles_lernen_2.tif_fmt1.jpgAbb. 2: Wenn es kalt wird, soll die Heizung eingeschaltet werden

In Cogniology wird Software, die technische Signale in Text und umgekehrt die Texte in Signale konvertiert, als Device Controller bezeichnet. Durch dieses Konzept kommuniziert die Plattform mit ihrer Umgebung immer durch natürlichsprachliche Texte. Die Plattform stellt mehrere Schnittstellen wie REST, Web Service oder Binary zur Verfügung, um sich vom Device Controller zu entkoppeln. Somit können Device Controller in beliebige Programmiersprachen implementiert sein.

Ein konkretes Beispiel für einen Device Controller wäre ein Temperaturregulationssystem (Listing 5). Wenn die Innentemperatur auf 17 Grad fällt, sendet der Device Controller den Satz „Es ist kalt“ an die Plattform. Die Plattform sucht in der Menge der vorhanden Sentence Components nach einer mit dem Definitionssatz „Es ist kalt“ und führt deren Codesegment aus. Die Sentence Component „Es ist kalt“ kann, wie die Sentence Component „Bestelle Hundefutter“, vom Benutzer programmiert worden sein. Dadurch kann der Benutzer die Raumtemperatur individuell automatisieren.

Das Prinzip der Device Controller ist ähnlich wie das von Hardwaretreibern, die verwendet werden, um das Betriebssystem von Herstellern und tatsächlich verwendeter Hardware unabhängig zu machen. Ein gutes Beispiel dafür ist der Druckertreiber. Jeder Druckerhersteller liefert für seinen Drucker einen passenden Treiber, der ein bestimmtes API implementiert. Somit können die Applikationen ihre Inhalte über dieses API ausdrucken, ohne direkt den Drucker zu steuern.

Das API der Device Controller sind die von Herstellern mitgelieferten Sentence Components und eine so genannte DeviceControllerConfiguration, die die nötigen Konfigurationen für den Betrieb der Hardware sowie der Plattform vornimmt. Diese Komponenten werden per Plug and Play an die Plattform angedockt (Listing 5).

Listing 5: Device Contoler „Temperaturregulationssystem“

@DeviceControllerConfiguration
public class Temperaturregulationssystem
{
  @Callback (sentenceDefinition = "Es ist kalt",
Question = ("Was soll ich machen, wenn es kalt ist?")
  @Callback (sentenceDefinition = "Es ist warm",
Question = ("Was soll ich machen, wenn es warm ist?")
  @ConfigurationMethod
  public void configure()
  {
// Hier kann die nötige Konfiguration durchgeführt werden.
  }
}

Nach dem Andocken der Device-Controller-Konfiguration ruft die Plattform deren mit @Configuration annotierte Methode auf, damit der Device Controller sich selbst konfiguriert. Damit kann der Hersteller die für sein Gerät nötigen Konfigurationen vornehmen. Die in der Annotation @Callback angegebene sentenceDefinition bezieht sich auf einen Satz, der vom Gerät zur Plattform gesendet wird, sobald sich sein Zustand ändert. Die Plattform behandelt diesen Satz, als ob ihn ein Mensch gesprochen hätte, und führt die entsprechende Sentence Component aus.

Wenn der Benutzer den in sentenceDefinition angegebenen Satz zum ersten Mal spricht, wird die Plattform dem Benutzer die in der Annotation Question angegebene Frage stellen. Somit kann der Benutzer beliebige verschiedene Geräte kombinieren und steuern.

Benutzer: Es ist kalt.

Maschine: Was soll ich machen, wenn es kalt ist?

Benutzer: Schalte die Heizung an und lasse die Jalousie herunter.

Die Plattform generiert durch einen Codegenerator für alle Sätze, die der Benutzer nach der W-Frage sagt, eine neue Sentence Component mit dem Definitionssatz @Sentence("Es ist kalt"). Wenn der Benutzer oder der Sensor beim nächsten Mal sagt „Es ist kalt“, wird die Heizung angeschaltet und die Jalousie heruntergelassen (Listing 6). HeizungService und JalousieService sind die Implementierung der beiden Sentence Components @Sentence("Schalte die Heizung an") und @ Sentence("Lasse die Jalousie herunter").

Listing 6: Sentence Component „AutomaticCreated_Es_Ist_Kalt_002”

@SentenceComponent
public class AutomaticCreated_Es_Ist_Kalt_002
{
  @Sentence("Es ist kalt")
  public void codeSegment()
  {
HeizungService heizungService = new HeizungService();
heizungService.codeSegment();
 
JalousieService jalousieService = new JalousieService ();
jalousieService.codeSegment();
  }
}

Deklaratives Beibringen: Lernen durch Fragen

In unseren beiden Beispielen zum expliziten Beibringen und impliziten Lernen initiierten die in der Sentence Component angegebenen Fragen das Lernen und Beibringen. In folgendem Beispiel stellt die Plattform die Frage:

Benutzer: Ich habe Hunger.

Maschine: Was soll ich unter „Ich habe Hunger“ verstehen?

Benutzer: Bestelle ein Roast-Beef-Sandwich auf Italian Brot mit Salat außer Zwiebeln, American-Sauce, extra Käse und eine Cola.

Maschine: Okay, wenn du sagst „Ich habe Hunger“, dann meinst du: Bestelle ein Roast-Beef-Sandwich auf Italian Brot mit Salat außer Zwiebeln, American-Sauce, extra Käse und eine Cola. Bezahle den Betrag von Sparkassenkonto mit IBAN DE12 3456789 00 112223344 und Passwort 1234.

In diesem Fall findet die Plattform für den Satz „Ich habe Hunger“ keine entsprechende Sentence Component. Daher stellt sie eine Frage und generiert für die Antwort durch den Codegenerator eine neue Sentence Component mit dem Definitionssatz @Sentence("Ich habe Hunger"). Da die Sentence Component zur Bestellung eines Sandwiches eine Kontoverbindung verlangt und die Plattform auf die W-Frage „Von welchem Konto möchtest du es zahlen?“ in der Wissensbasis eine entsprechende Antwort findet, schlägt sie diese dem Nutzer vor.

Fazit

Wenn wir uns von den aktuellen Konzepten des Maschinellen Lernens wie Deep Learning oder neuronalen Netzen für einen Moment freimachen und uns fragen, wie eine Maschine intelligentes Verhalten lernen soll, ist meine Antwort darauf: Genauso, wie wir unseren Kindern etwas Neues beibringen. Sie lernen nicht nur durch Nachahmung implizite Fertigkeiten von uns, sondern wir vermitteln ihnen neue Inhalte durch Sprache. So können Maschinen von uns am schnellsten lernen und uns bei der täglichen Arbeit unterstützen. Wie einer Maschine beliebig komplexe Aufgaben durch Menschen beigebracht werden, ist das Thema unserer nächsten beiden Artikel.

amri_masoud_sw.tif_fmt1.jpgMasoud Amri ist Informatiker mit über zwanzig Jahren Erfahrung als Softwareentwickler und Softwarearchitekt bei namhaften Unternehmen (IBM, Mercedes, Volksbank, Fraunhofer etc.) Er arbeitet seit 2012 an der Theorie und Umsetzung der Cogniology.

Reaktive Programmierung verspricht, moderne Hardware effizient zu nutzen und den Anwender von lästigen Wartezeiten zu befreien. Doch sie hat ihren Preis: Sie stellt hohe Anforderungen an uns Entwickler und zwingt uns, ausgetretene Pfade zu verlassen. Dieser Artikel vergleicht den reaktiven Programmierstil mit dem herkömmlichen.

Es kann ein sehr zufriedenstellendes Gefühl sein, eine Aufgabenliste von oben nach unten abzuarbeiten. Man fokussiert sich exklusiv auf das, was gerade ansteht, und setzt anschließend einen Haken darunter. Offensichtlich führt solch ein rein serieller Ansatz nicht unbedingt zu optimalen Ergebnissen. Lautet eine der Aufgaben beispielsweise „Wäsche waschen“, macht es wenig Sinn, die Waschmaschine zu bestücken und dann gebannt durch das Bullauge zu starren, bis das Programm abgelaufen ist, während man doch nebenher bereits den Fußboden wischen könnte. Das andere Extrem besteht darin, eine Vielzahl von Aufgaben nahezu gleichzeitig zu starten, den Fortschritt zu überwachen und aktiv zu werden, sobald es erforderlich wird oder ein Ergebnis eintritt. Durch geschicktes Jonglieren mit den einzelnen Aktivitäten lässt sich die eigene Produktivität deutlich steigern – vorausgesetzt, Sie wissen noch, wo Ihnen der Kopf steht.

Die Analogie lässt sich ganz gut auf die Softwareentwicklung übertragen. Die meisten von uns werden wohl mit einem seriellen Ansatz aufgewachsen sein und sich damit am wohlsten fühlen. Ein Programm besteht aus einzelnen Anweisungen, die der Computer Schritt für Schritt ausführt. Das ist intuitiv, alle Ergebnisse liegen dann vor, wenn man sie braucht, und man muss sich nicht mit Nebenläufigkeit und anderen esoterischen Konzepten herumschlagen. Leider ist ein solches Vorgehen für moderne Hardware ähnlich ineffizient wie das Waschmaschinenanalogon. Aus der Sicht eines Prozessorkerns dauert eine I/O-Operation eine halbe Ewigkeit. Zeit, die er wesentlich sinnvoller nutzen könnte, als untätig auf das Eintreffen der Ergebnisse zu warten. Um den Anwender mit Software zufriedenzustellen, die auf seinem Rechner mit der erwarteten Geschwindigkeit läuft, müssen wir uns schon mehr anstrengen. Ein Lösungsansatz besteht darin, den Prozessor eben nicht warten zu lassen, sondern die Zeit bis zum Eintreffen der Daten mit alternativen Aufgaben zu überbrücken. Das erhöht natürlich den Koordinationsaufwand.

Dieser Artikel untersucht verschiedene Ansätze zur reaktiven Programmierung und ihren Einfluss auf die Komplexität des resultierenden Codes. Dabei werden sowohl Methoden betrachtet, die Java von sich aus mitbringt, als auch solche, die populäre Open-Source-Frameworks anbieten. Gegenstand der Untersuchung ist ein relativ einfacher Anwendungsfall: das Einlesen einer Datei. Das ist ziemlich übersichtlich und wurde vermutlich von jedem bereits in der einen oder anderen Form implementiert. Die Dateioperation steht dabei stellvertretend für eine ganze Klasse ähnlicher Probleme, bei denen Daten von externen Quellen – typischerweise über blockierende Aufrufe – angefordert werden. Beispiele sind Zugriffe auf das Netzwerk, Aufrufe von entfernten Services oder Datenbankabfragen. Die angestellten Betrachtungen lassen sich größtenteils auf alle diese Probleme übertragen. Um eine Grundlage für weitere Vergleiche zu haben, starten wir mit einer möglichst einfachen, blockierenden Implementierung zum Einlesen einer Datei. Mit den Möglichkeiten einer modernen Java-Version könnte eine Lösung dieses Problems so aussehen (der vollständige Code für alle im Artikel referenzierten Beispiele ist in einem GitHub-Repository [1]verfügbar):

public void readFile(Path path) throws IOException {
  byte[] bytes = Files.readAllBytes(path);
  // tue irgendwas mit den Daten
}

Mithilfe von Bequemlichkeitsfunktionen aus der java.nio.file.Files-Klasse genügt eine einzelne Zeile, um die Aufgabe zu lösen. Das ist natürlich eine sehr naive und nicht gerade optimierte Implementierung. Zunächst kann die Verarbeitung der Daten erst dann erfolgen, wenn die komplette Datei gelesen wurde. Außerdem geht dieses Fragment von der Annahme aus, dass die Datei als Ganzes in den Hauptspeicher passt. Geht es um die Verarbeitung von Massendaten, dürfte sich dieser Ansatz als wenig geeignet erweisen. Beide Kritikpunkte lassen sich durch den Einsatz einer Schleife entschärfen, die über einen traditionellen InputStream die Datei blockweise liest und die einzelnen Blöcke an einen Konsumenten zur Weiterverarbeitung übergibt. Das ist immer noch recht überschaubar und sollte den meisten vertraut sein. Der Nachteil, dass der ausführende Thread jeweils für die Leseoperationen blockiert wird, ist beiden Lösungen gemeinsam.

Brave New I/O

In den Anfangstagen von Java konnten I/O-Operationen ausschließlich blockierend durchgeführt werden. Eine andere Option hat die Standardbibliothek schlichtweg nicht geboten. Seitdem hat die Welt sich weiter gedreht, und mit Java 1.4 ist das nio-Paket (für New I/O) hinzugekommen. Darin enthalten sind erstmals Klassen, über die sich Datei- oder Netzwerkoperationen asynchron, also im Hintergrund, ausführen lassen. Ich weiß nicht, wie es Ihnen geht, aber ich bin mit dem nio-Paket nie so richtig warm geworden. Die verwendeten Abstraktionen unterscheiden sich stark von den gewohnten Stream-Klassen aus dem alten io-Paket, was ihren Einsatz nicht gerade intuitiv macht: Sie müssen Buffer flippen, mit Channels hantieren und Selektoren orchestrieren. Das scheint die These zu bestätigen, dass man tief in die Trickkiste greifen muss, um effiziente Ein-/Ausgabeoperationen hinzukriegen.

Mit Java 1.7 sind dem Paket einige neue Klassen hinzugefügt worden, die sich etwas einfacher nutzen lassen. Eine davon ist AsynchronousFileChannel, die im folgenden Beispiel Verwendung findet. Die Klasse bietet eine read()-Methode, die einen Puffer zur Ablage der Ergebnisse, die Position, ab der gelesen werden soll, sowie ein Objekt erwartet, das nach Beendigung der Operation aufgerufen werden soll. Hier begegnen wir bereits einem grundlegenden Muster der Reaktiven Programmierung: Methoden liefern kein Ergebnis zurück, sondern erwarten ein Rückruf- oder Callback-Objekt als Parameter. Der aktuelle Thread wartet nicht auf das Ergebnis der Methode, sondern läuft direkt weiter. Die eigentliche Aktion wird dann in einem weiteren Thread im Hintergrund ausgeführt. Sobald Ergebnisse vorliegen, werden sie an das Callback-Objekt zur weiteren Verarbeitung übergeben. Da der Callback-Aufruf durch einen anderen Thread erfolgt, sind entsprechende Vorkehrungen zur Synchronisation der Daten erforderlich. AsynchronousFileChannel arbeitet mit einem Callback-Objekt vom Typ CompletionHandler. Das Interface definiert eine Methode für den Erfolgsfall und eine zur Fehlerbehandlung. Hier zeigt sich, dass die Klasse noch aus der Ära vor Java 8 stammt: CompletionHandler ist kein funktionales Interface und kann daher nicht durch einen Lambdaausdruck umgesetzt werden.

Listing 1 zeigt eine mögliche Implementierung basierend auf dieser Klasse. Einstiegspunkt ist die readFile()-Methode, die den ersten Block der zu lesenden Datei anfordert. Sie übergibt dabei eine Callback-Implementierung, die die weitere Koordination der Leseoperation übernimmt. Wenn Daten eintreffen, wird der nächste Block angefordert, so lange, bis das Dateiende erreicht ist. Zur Verwaltung des aktuellen Zustands der Operation (wie die aktuelle Leseposition oder die bereits erhaltenen Daten) kommt ein spezielles Kontextobjekt zum Einsatz. Das umgeht das Problem der Datensynchronisation zwischen mehreren Threads: Der Kontext wird direkt an die read()-Methode übergeben und ist lokal für die aktuelle Operation; er wird also nicht gemeinsam von verschiedenen Threads genutzt. Als Ergebnis liefert readFile() ein CompletableFuture-Objekt zurück. Es wird als komplett markiert, wenn alle Daten gelesen wurden oder ein Fehler aufgetreten ist. Offensichtlich ist diese Lösung um Größenordnungen komplexer als die blockierende Referenzimplementierung. In eigenen Projekten möchte man auf diese Komplexität sicher gerne verzichten. Daher wäre es praktisch, wenn man auf eine entsprechende Funktionalität in Frameworks zurückgreifen könnte.

Listing 1: „AsyncFileReader.java“

public class AsyncFileReader {
  private static final int BUF_SIZE = 1024;
  private static final int CONTENT_BUFFER = 8192;
 
  /** The completion handler used by this instance. */
  private final CompletionHandler<Integer, ReadContext> handler =
    createHandler();
 
  public CompletableFuture<String> readFile(Path path) {
    CompletableFuture<String> future = new CompletableFuture<>();
    try {
      AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,
        StandardOpenOption.READ);
      ReadContext context = new ReadContext(channel, future);
      readBlock(context);
    } catch (IOException e) {
        future.completeExceptionally(e);
    }
    return future;
  }
 
  // Triggers another read operation for a chunk of data.
  private void readBlock(ReadContext context) {
    context.buffer.clear();
    context.channel.read(context.buffer, context.position, context,
      handler);
  }
 
  // Creates a handler to process the results of a read operation.
  private CompletionHandler<Integer, ReadContext> createHandler() {
    return new CompletionHandler<Integer, ReadContext>() {
      @Override
      public void completed(Integer count, ReadContext context) {
        if (count < 0) { // Dateiende erreicht
          context.close();
          context.future.complete(context.content.toString());
        } else { // Lese nächsten Block
            context.buffer.flip();
            byte[] data = new byte[count];
            context.buffer.get(data);
            context.content.append(new String(data));
            context.position += count;
            readBlock(context);
          }
        }
 
        @Override
        public void failed(Throwable exc, ReadContext context) {
          context.fail(exc);
        }
    };
  }
 
  // Context information for a file read operation.
  private static class ReadContext {
    private final AsynchronousFileChannel channel;
    private final CompletableFuture<String> future;
    private final ByteBuffer buffer;
    private final StringBuilder content;
    private int position;
 
    ReadContext(AsynchronousFileChannel c, CompletableFuture<String> f) {
      channel = c;
      future = f;
      content = new StringBuilder(CONTENT_BUFFER);
      buffer = ByteBuffer.allocate(BUF_SIZE);
    }
 
    public void fail(Throwable ex) {
      future.completeExceptionally(ex);
      close();
    }
 
    public void close() {
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

Framework eins: Vert.x

Solche Frameworks gibt es tatsächlich. Eins davon ist Vert.x [2]. Entwickelt unter dem Dach der Eclipse Foundation, stellt dieses Framework eine Reihe nicht blockierender Implementierungen für unterschiedliche Anwendungsfälle zur Verfügung, unter anderem für HTTP-Aufrufe, Socket-Kommunikation und auch für Dateisystemoperationen. Alle diese Dienste folgen dabei einem einheitlichen Programmiermodell basierend auf (diesmal lambdafreundlichen) Callbacks. Außerdem erlaubt Vert.x, Logik in sogenannten Verticles zu organisieren. Ein Verticle entspricht in etwa einem Aktor aus dem Aktoren-Modell [3]. Vert.x stellt sicher, dass ein Verticle immer nur von einem bestimmten Thread aus aufgerufen wird, auch wenn der Aufruf aus einem Callback heraus erfolgt. Dadurch wird es möglich, Zustand in Verticles zu halten, ohne besondere Vorkehrungen zur Synchronisation mit anderen Threads zu treffen. Verglichen mit der Handhabe von Callbacks der AsynchronousFileChannel-Klasse ist das ein deutlicher Vorteil. Betrachten wir nun, wie sich eine einfache Operation zum Lesen einer Datei mit Vert.x realisieren lässt (Listing 2).

Listing 2: Lesen einer Datei mit Vert.x

private void readFile(String path) {
  vertx.fileSystem().readFile(path, res -> {
    if (res.succeeded()) {
      String content = res.result().toString();
      // Verarbeite Daten aus der Datei
      ...
    } else {
      LOG.error("Reading file failed!", res.cause());
    }
  });
}

Der FileSystem-Service von Vert.x bietet eine readFile()-Methode zum Lesen einer Datei als Ganzes. Sie erwartet als Callback ein Objekt vom Typ Handler<AsyncResult<Buffer>>. Das ist der von Vert.x standardmäßig genutzte Typ für Operationen, die fehlschlagen können. Inklusive einfacher Fehlerbehandlung sind wir damit also wieder in Regionen, die nahe bei der Referenzimplementierung liegen.

Kompliziertere Rückrufaktionen

Ermutigt durch dieses Resultat können wir uns jetzt eines etwas komplizierteren Problems annehmen. Die Datei soll nicht nur gelesen, sondern auch modifiziert und wieder geschrieben werden. Als Modifikation implementieren wir eine einfache Base64-Codierung. Außerdem soll die Aktion fehlschlagen, wenn die Ausgabedatei bereits existiert (Abb. 1). Eine Umsetzung dieser neuen Anforderungen sehen Sie in Listing 3.

heger_react_1.tif_fmt1.jpgAbb. 1: Umzusetzende Dateiverarbeitung

Listing 3: Dateiverarbeitung mit Callbacks

private void processFile(Message<Object> msg) {
  String path = String.valueOf(msg.body());
  String outPath = path + ".processed";
  vertx.fileSystem().exists(outPath, rEx -> {
    if (rEx.failed()) {
      sendResponse(msg, false, "Exists check failed");
    } else if (rEx.result()) {
      sendResponse(msg, false, "File already exists");
    } else {
      vertx.fileSystem().readFile(path, rRead -> {
        if (rRead.failed()) {
          sendResponse(msg, false, "Read failed: " + rRead.cause());
        } else {
          byte[] content = rRead.result().getBytes();
          String encoded = printBase64Binary(content);
          vertx.fileSystem().writeFile(outPath, Buffer.buffer(encoded), rWrt -> {
            if (rWrt.succeeded()) {
              sendResponse(msg, true, "Generated " + outPath);
            } else {
              sendResponse(msg, false, "Write failed: " + rWrt.cause());
            }
          });
        }
      });
    }
  });
}

Hier sehen wir deutlich die Nachteile des reinen Callback-Ansatzes. Anstelle eines einzelnen Callbacks haben wir es jetzt mit drei ineinander verschachtelten zu tun. In jedem davon passiert zunächst eine Fehlerprüfung. Solange noch alles in Ordnung ist, erfolgt der nächste nicht blockierende Aufruf unter Angabe eines weiteren Callbacks. Die Einrückungstiefe wird immer größer, und falls Sie einmal in die Verlegenheit kommen sollten, etwas an der Ablauflogik zu ändern, müssen Sie erst die passende Klammerungsebene identifizieren. Willkommen in der Callback-Hölle! Das sollte doch irgendwie besser gehen.

Eine bessere Zukunft

Die Grenzen eines auf Callbacks beruhenden Programmiermodells sind natürlich auch den Entwicklern von Vert.x bewusst. Daher gibt es einige Auswege aus dem Dilemma [4]. Einer davon beruht auf Futures. Ein Future-Objekt hat die Semantik, dass es irgendwann einmal einen Wert enthalten wird. Wenn dieser Wert eintrifft, können weitere Aktionen ausgelöst werden, beispielsweise die Ausführung einer Aktion. Future-Objekte können als eine Art implizite Callbacks gesehen werden [5]. Das Konzept stammt ursprünglich aus der funktionalen Programmierung und kann daher gut kombiniert und zu größeren Einheiten zusammengefasst werden. Das Beispiel aus dem letzten Abschnitt lässt sich so umschreiben, dass für die einzelnen Schritte jeweils ein Future-Objekt angelegt wird. Vert.x stellt dafür eine Future-Implementierung bereit, die die Vert.x-typischen Handler-Interfaces erweitert, und daher anstelle eines Callbacks an einen Service übergeben werden kann. Daraus resultieren drei Future-Objekte, die wir per Komposition in ein neues überführen. Das erhält einen Callback, der nur noch für das Gesamtergebnis zuständig ist. Das deutlich besser lesbare Ergebnis findet sich in Listing 4.

Listing 4: Dateiverarbeitung mit Futures

private void processFile(Message<Object> msg) {
  String path = String.valueOf(msg.body());
  String outPath = path + ".processed";
 
  existsFile(outPath)
    .compose(res -> !res ? Future.succeededFuture() :
      Future.failedFuture(new IOException("File already exists")))
    .compose(v -> readFile(path))
    .map(b -> Buffer.buffer(printHexBinary(b.getBytes())))
    .compose(buf -> writeFile(outPath, buf))
    .setHandler(res -> sendResponse(msg, res.succeeded(),
      res.succeeded() ? "Generated " + outPath :
        res.cause().getMessage()));
}
 
private Future<Boolean> existsFile(String path) {
  Future<Boolean> result = Future.future();
  vertx.fileSystem().exists(path, result);
  return result;
}
 
private Future<Buffer> readFile(String path) { ... }
private Future<Void> writeFile(String outPath, Buffer data) { ... }

Aus der verschachtelten Hierarchie von Callbacks wurde eine lineare Kette von Future-Kompositionen. Das vereinfacht auch stark die Fehlerbehandlung, denn die Kette schlägt fehl, sobald ein Teil davon einen Fehler produziert.

Mit Future-Objekten haben wir ein weiteres Muster der reaktiven Programmierung kennen gelernt. Sie sind einem reinen Callback-basierten Ansatz überlegen. Aufgrund ihrer Kombinierbarkeit eignen sie sich auch gut dafür, eine Reihe voneinander unabhängiger Aufgaben zu parallelisieren [5]. Dazu werden die Aufgaben angestoßen und auf Futures abgebildet; sie werden damit im Hintergrund in eigenen Threads ausgeführt. Die einzelnen Future-Objekte lassen sich wiederum zu einem kombinierten Future-Objekt zusammenfassen, das automatisch aufgerufen wird, sobald alle Teilergebnisse vorliegen (Kasten: „Futures in Java“).

Futures in Java

Bezüglich der in den betrachteten Frameworks verwendeten Future-Implementierungen kann es zu Verwechslungen mit Klassen aus der Java-Standardbibliothek kommen. Java besitzt schon seit Längerem eine Future-Klasse. Diese ist aber für reaktive Programmieransätze ungeeignet, denn sie bietet nur eine Möglichkeit, an das gespeicherte Ergebnis heranzukommen: blockierendes Warten. Erst die mit Java 8 hinzugekommene CompletableFuture-Klasse besitzt die erforderlichen Eigenschaften und befindet sich damit in etwa auf dem Niveau der Frameworkklassen.

Framework zwei: Akka

Als Ergänzung zu den Betrachtungen über Vert.x möchte ich noch kurz auf ein weiteres reaktives Programmiermodell eingehen, das von dem ebenfalls populären Framework Akka [6] angeboten wird. Akka ist in Scala geschrieben und bevorzugt daher eher funktionale Ansätze. Es verzichtet weitestgehend auf Callbacks und setzt stattdessen auf Futures, um nebenläufige Aktivitäten zu koordinieren. Eine Einführung in Akka und das von dem Framework implementierte Aktorenmodell ist in einer früheren Ausgabe des Java Magazins erschienen [7]. Der Teil von Akka, um den es mir hier geht, ist das Streamingmodul [8]. Es beinhaltet eine Implementierung der Reactive-Streams-Spezifikation [9]. Dabei geht es im Wesentlichen um das Versenden, Empfangen und Verarbeiten großer Datenströme unter den Rahmenbedingungen, dass nur begrenzter Speicher genutzt wird und sich Sender und Empfänger auf eine für beide akzeptable Verarbeitungsgeschwindigkeit einigen können.

Akka-Streaming hat die eher technischen Interfaces aus der Spezifikation mit einem intuitiven und funktionalen API ergänzt. Es gibt Abstraktionen für Datenquellen (Sources), Datensenken (Sinks) und Operatoren (Flow stages), die man dazwischenschalten kann, um Daten zu filtern oder zu manipulieren. Da es auch Source- und Sink-Implementierungen für Dateien gibt, lässt sich unser Referenzbeispiel auch mit dieser Bibliothek umsetzen – mit nur geringfügigem Mehraufwand sogar in einer erweiterten Form. Listing 5 (in Akkas nativer Sprache Scala geschrieben) zeigt ein Fragment, das eine Datei zeilenweise einliest, Leer- und Kommentarzeilen herausfiltert, Text in Kleinbuchstaben umwandelt und das Ergebnis in eine neue Datei schreibt. Zurück kommt ein Future-Objekt, das benachrichtigt wird, wenn die Verarbeitung komplett ist.

Listing 5: Dateiverarbeitung mit Akka-Streams

def processFile(input: Path, output: Path)(implicit system: ActorSystem,
  mat: ActorMaterializer): Future[IOResult] = {
  val source = FileIO.fromPath(input)
  val sink = FileIO.toPath(output)
  source.via(Framing.delimiter(ByteString("\r"), 1024, allowTruncation = true))
    .map(_.utf8String.trim)
    .filter(s => s.length > 0 && !s.startsWith(CommentPrefix))
    .map(s => ByteString(s.toLowerCase(Locale.ENGLISH)+System.lineSeparator()))
    .runWith(sink)
}

Ich möchte nicht auf die Details eingehen, sondern den Code für sich selbst sprechen lassen. Wie schon bei Futures zeigt sich auch hier, dass Ansätze aus der funktionalen Programmierung gut mit dem reaktiven Programmierstil zusammenpassen. Meiner Meinung nach werden sich Streamingansätze in Zukunft weiter verbreiten. Viele Probleme lassen sich als ein Strom von Daten modellieren, der auf seinem Weg von der Quelle zur Senke diverse Manipulationen erfährt. Auch Java 9 hat mit dem Flow-API ein ähnliches Konstrukt eingeführt.

Fazit

Reaktive Programmierung kommt nicht zum Nulltarif. Es sind andere und mitunter ungewohnte Ansätze erforderlich, um unter Verzicht auf blockierende Aufrufe mit externen Diensten und Systemen zu kommunizieren und ihre Antworten zu koordinieren. Etablierte Frameworks wie Vert.x oder Akka bieten dabei Unterstützung durch geeignete Abstraktionen. Allerdings sollte man auch hier darauf achten, welche Abstraktion sich für welches Problem eignet. Nach einer gewissen Eingewöhnungsphase erscheint ein asynchrones Programmiermodell dann hoffentlich nicht mehr als Hexenwerk.

heger_oliver_sw.tif_fmt1.jpgOliver Heger arbeitet als Entwickler bei Bosch Software Innovations im Bereich Heimautomatisierung. Er ist auch im Open-Source-Bereich aktiv und Mitglied der Apache Software Foundation.

Desktop Tablet Mobile
Desktop Tablet Mobile