OO-Entwurfsmuster und Lambdas - Zusammenführen, was zusammen gehört

Montag, 6.8.2018

Die Entwurfsmuster der „Gang-of-Four“ haben sich als nützliche Werkzeuge für die Lösung
gängiger Herausforderungen in der objektorientierten Programmierung bewährt. Wie profitiert
diese Lösung von einem Einsatz der mit Java 8 eingeführten Lambda-Ausdrücke?

Hinweis: Diesem Blogbeitrag liegt ein Artikel zugrunde, der in der Java aktuell Ausgabe 03/2018 erschienen ist. Alle der hier vorgestellten Code-Beispiele sind frei auf Git-Hub verfügbar.

OO-ENTWURFSMUSTER-UND-LAMBDAS

In ihrer ursprünglichen Form leiten sich die Entwurfsmuster der GoF her aus den grundlegenden  Paradigmen der Objektorientierung: Polymorphie, Vererbung und Kapselung. Zusätzlich hat mit den Lambda-Ausdrücken seit Java 8 die funktionale Programmierung Einzug in die objektorientierte Welt erhalten. Daher ist es an der Zeit, die Entwurfsmuster daraufhin zu prüfen, ob sie durch den Einsatz von Lambda-Ausdrücken vereinfacht oder sinnvoll erweitert werden können.

Jetzt Whitepaper downloaden!

 

Warum überhaupt Entwurfsmuster?

Software-Entwicklerinnen und -Entwickler arbeiten in einer jungen Disziplin und haben sich − bildlich gesprochen − bereits von improvisierten Holzverschlägen zu Konstruktionen vorgearbeitet, die gemäß einer reflektiert-übergeordneten Architektur gestaltet sind. Es setzt sich das Bewusstsein durch, dass nicht jedes Software-Projekt gleichsam das Rad neu erfinden muss; es gibt Standard-Lösungen für Standard-Probleme. An dieser Stelle setzen Entwurfsmuster an. Bei ihnen handelt es sich um Werkzeuge, die genutzt werden können, um wiederkehrende Probleme auf eine bewährte Art und Weise zu lösen. Qualitätskriterien wie Wiederverwendbarkeit, Änderbarkeit und Verständlichkeit stehen dabei im Vordergrund. Sie sind quasi der Versuch, eine Baustatik für objektorientierte Software zu etablieren.

Im Folgenden wird beispielhaft das Erzgeugungsmuster "Abstrakte-Fabrik" mit Lambda-Ausdrücken angereichert. Am Ende dieses Blog-Beitrags finden Sie einen umfassenden Artikel, in dem darüber hinaus noch das Erzeugungsmuster "Erbauer" und die Verhaltensmuster "Schablonen-Methode" und "Beobachter" besprochen werden.

 

Abstrakte Fabrik

Hinter einer „abstrakten Fabrik“ steht die Idee, konkrete Erzeugungs-Mechanismen (bezeichnet als Fabriken) für Objekte, die sich in der Struktur gleichen (also das gleiche Interface implementieren oder eine gemeinsame Oberklasse besitzen), zusammenzufassen und über eine einheitliche Schnittstelle verfügbar zu machen. Dabei wissen Aufrufende der abstrakten Fabrik nicht, welche konkrete Fabrik letztlich genutzt wird, um ein Objekt zu erzeugen. Des Weiteren hat die abstrakte Fabrik nur Informationen zur allgemeinen Struktur des erstellten Objekts (Interface oder Oberklasse). Es wird sozusagen ein Fahrzeug ausgeliefert, ohne dass angegeben wird, ob es sich um einen Fabia, A8 oder eine E-Klasse handelt.

Ein wesentlicher Vorteil dieses Entwurfsmusters ist die Möglichkeit, konkrete Fabriken zu ergänzen oder auszuwechseln, ohne andere Komponenten der Software dafür anfassen zu müssen. Dadurch wird dem unschätzbar wertvollen Open-Closed-Principle Rechnung getragen, welches besagt, dass "eine Klasse offen für Erweiterungen sein muss, jedoch geschlossen gegenüber Modifikationen". Anders ausgedrückt: Wenn einer Komponente Verhalten hinzugefügt werden soll, darf es dafür nicht nötig sein, bereits vorhandenes Verhalten zu ändern. Wer schon einmal in der Situation war, ein kompliziertes (vielleicht auch noch ungetestetes) Stück Code ändern zu müssen, dass vor langer Zeit geschrieben wurde, kennt die Gefahr, dass die Änderung das Kartenhaus der Gesamt-Software zusammenbrechen lässt. Eine konsequente Anwendung des Open-Closed-Principles reduziert diese Gefahr deutlich.

 

Im Folgenden werden konkrete Implementierungen mit und ohne Lambda-Ausdrücken vorgestellt.

Klassisch

Das folgende Listing zeigt eine Beispiel-Implementierung für eine abstrakte Autofabrik.

 

public static void main(String[] args) {
AbstractFactoryClassic classicFactory = new AbstractFactoryClassic();

Car fabia = classicFactory.getCarFactoryByModel(Model.FABIA).assemble();
System.out.println(fabia.toJson());
// Assembling Fabia...
// {"brand":"Skoda","model":"Fabia","ps":54}

Car a8 = classicFactory.getCarFactoryByModel(Model.A8).assemble();
System.out.println(a8.toJson());
// Assembling A8...
// {"brand":"Audi","model":"A8","ps":190}

Car eKlasse = classicFactory.getCarFactoryByModel(Model.EKLASSE).assemble();
System.out.println(eKlasse.toJson());
// Assembling E-Klasse...
// {"brand":"Mercedes","model":"E-Klasse","ps":110}
}

Listing 1: Nutzung der abstrakten Fabrik

In der aufrufenden Komponente wird hier lediglich angegeben, welches Modell gebaut werden soll. Die Entscheidung darüber, welche konkrete Fabrik für diese Aufgabe gewählt wird, übernimmt die abstrakte Fabrik. Das Ergebnis ist ein allgemeines Auto, das erst im Rahmen der weiteren Verwendung (Log-Ausgabe der konkreten Attribute) die inneren Eigenschaften preisgibt.

Die konkreten Fabriken sind dabei Implementierungen eines allgemeinen Auto-Fabrik-Interfaces, die in der abstrakten Fabrik registriert werden (in diesem Beispiel unter Verwendung einer Map):

 

public AbstractFactoryClassic() {
carFactoryRegistry.put(Model.FABIA, new FabiaFactory());
carFactoryRegistry.put(Model.A8, new A8Factory());
carFactoryRegistry.put(Model.EKLASSE, new EKlasseFactory());
}

Listing 2: Registrierung von konkreten Fabriken

Hier das allgemeine Interface und ein Beispiel für eine konkrete Implementierung:

 

public interface CarFactoryClassic<T extends Car> {
T assemble();
}

Listing 3: Interface für konkrete Fabriken

 

public class EKlasseFactory implements CarFactoryClassic<EKlasse> {
@Override
public EKlasse assemble() {
return new EKlasse("Mercedes", "E-Klasse", 110);
}
}

Listing 4: Beispiel-Implementierung einer Autofabrik: klassisch

Man benötigt also für jede Fabrik eine eigene Implementierungsklasse.

Nun ist zu klären, ob mit Hilfe von Lambdas die Umsetzung des Entwurfsmusters "Abstrakte Fabrik" vereinfacht und/oder verbessert werden kann.

 

Lambda

Konkrete Fabriken können, statt mit eigenen Klassen, als Lambda-Ausdrücke umgesetzt werden. Dazu muss zunächst ein Functional-Interface definiert werden, welches die allgemeine Struktur der Erzeugung vorgibt.

@FunctionalInterface
public interface CarFactoryLambda<T extends Car> extends Supplier<T> {

T assemble();

@Override
default T get() {
return assemble();
}
}

Listing 5: Functional-Interface für konkrete Fabriken

Per Definition ist jedes Interface, dass lediglich eine Methode definiert, ein Functional-Interface (die Annotation @FunctionalInterface ist optional). Das in Listing 3 dargestellte Interface erfüllte diese Voraussetzung bereits.

Allerdings ermöglicht die Extendierung des Supplier-Interfaces die Nutzung im Rahmen einiger Standard-JDK8-APIs. Da die get()-Methode des Supplier-Interfaces in Listing 5 in Form einer Default-Methode überschrieben wird, ist dadurch die Functional-Interface-Voraussetzung nicht verletzt.

So muss Code, in dem das CarFactory-Interface bereits genutzt wird, nicht geändert werden. Es wird lediglich die Möglichkeit ergänzt, es auch als Supplier zu verwenden – ganz im Sinne des Open-Closed-Principles.

Nun können Lambda-Factories in der abstrakten Fabrik registriert werden.

public AbstractFactoryLambda() {
carFactoryRegistry.put(Model.FABIA, () -> new Fabia("Skoda", "Fabia", 52));
carFactoryRegistry.put(Model.A8, () -> new A8("Audi", "A8", 190));
carFactoryRegistry.put(Model.EKLASSE, () -> new EKlasse("Mercedes", "E-Klasse", 110));
}

Listing 6: Registrierung von Lambda-Factories

Die Nutzung der AbstractFactoryLambda, ist identisch zur klassischen, in Listing 1 dargestellten Art und Weise.

Neben schlankerem Code ergeben sich dadurch noch weitere Vorteile, die in Listing 7 dargestellt sind.

System.out.println("\nAssemble 3 Fabias in a row:");
Stream.generate(abstractLambdaFactory.getCarFactoryByModel(Model.FABIA))
.limit(3)
.map(Car::toJson)
.forEach(System.out::println);
// Assemble 3 Fabias in a row:
// Assembling Fabia...
// {"brand":"Skoda","model":"Fabia","ps":52}
// Assembling Fabia...
// {"brand":"Skoda","model":"Fabia","ps":52}
// Assembling Fabia...
// {"brand":"Skoda","model":"Fabia","ps":52}

Car absent = null;
Car present = Optional.ofNullable(absent)
.orElseGet(abstractLambdaFactory.getCarFactoryByModel(Model.A8));
System.out.println(present.toJson());
// Assembling A8...
// {"brand":"Audi","model":"A8","ps":190}

Listing 7: Nutzung einer Lambda-Factory in der JDK8-API

Durch die abstrakte Fabrik bereit gestellte Lambda-Fabriken können in der JDK8-Stream-API genutzt werden, um auf einfache Art und Weise mehrere Autos zu produzieren.

Daneben ist z.B. auch eine Verwendung als Fallback-Lösung beim Einsatz von Optionals sinnvoll. In der orElseGet()-Methode kann mit Hilfe der abstrakten Fabrik eine Alternative im Falle der Abwesenheit eines Autos generiert werden. Dabei wird einer der großen Vorteile von Lambdas genutzt: die Objekt-Repräsentation des Lambda-Ausdrucks wird erst zur Laufzeit (mit Hilfe der mit Java 7 eingeführten ByteCode-Instruktion invokedynamic) instanziiert und zwar nur dann, wenn sie wirklich benötigt wird (der optionale Wert also nicht vorhanden ist). Dadurch entsteht kein Aufwand für die Erzeugung des Fallback-Objekts im Falle des Vorhandenseins des Optional-Inhalts.

Die Variante, die ohne Lambdas arbeitet (orElse()), führt den darin übergebenen Code hingegen immer sofort aus, unabhängig davon, ob das Ergebnis tatsächlich benötigt wird.

Abgesehen von der Einsparung von Implementierungsklassen, profitiert die Nutzung von Lambdas im Kontext der abstrakten Fabrik also von Synergieeffekten mit JDK8-APIs.

 

Fazit

Wie dieser Blog-Beitrag verdeutlicht, haben die Entwurfsmuster der Gang-of-Four auch in Zeiten der Einführung von funktionaler in die objekt-orientierte Programmierung nicht an Relevanz und Nützlichkeit verloren.

Der Einsatz von Lambda-Ausdrücken macht einen wesentlichen Anteil der Entwurfsmuster schlanker, effizienter und eröffnet weitere neue Anwendungsmöglichkeiten.

Darüber hinaus sind die klassischen Implementierungen der Entwurfsmuster mit den jeweiligen Lambda-Varianten kompatibel und austauschbar, wenn Functional-Interfaces sinnvoll definiert und genutzt werden. Dadurch müssen bereits vorhandene Umsetzungen der einzelnen Entwurfsmuster nicht komplett überarbeitet werden.

Hier finden Sie den Artikel, der diesem Blog-Beitrag zugrunde liegt, in dem noch weitere klassische Entwurfsmuster beispielhaft um Lambda-Ausdrücke erweitert werden:

Jetzt Whitepaper downloaden!


zurück zur Blogübersicht

Diese Beiträge könnten Sie ebenfalls interessieren

Keinen Beitrag verpassen – viadee Blog abonnieren

Jetzt Blog abonnieren!

Kommentare

Christian Nockemann

Christian Nockemann

Diplom-Wirtschaftsinformatiker Christian Nockemann arbeitet seit 2009 als IT-Berater und Software-Architekt bei der viadee IT-Unternehmensberatung. Sein Fokus liegt auf dem Design und der Entwicklung von Java-basierten Enterprise-Anwendungen. Eine besondere Bedeutung gibt er dabei Qualitätskriterien des Softwareerstellungsprozesses wie bspw. der Anwendung des Domain-Driven-Designs, dem sinnvollen Einsatz von Entwurfsmustern und der Einhaltung von Clean-Code-Richtlinien.

Christian Nockemann bei Xing  Christian Nockemann auf Twitter