Suche
Wie man Patterns einfach implementieren kann

Erzeugungsmuster mit Java 8 Lambdas

Michael Simons

© Shutterstock / agsandrew

In diesem Fachartikel zeigt W-JAX-Speaker Michael Simons, wie moderne Programmierparadigma Muster (Pattern) in der Programmierung zwar nicht überflüssig, aber verständlicher und vor allen Dingen einfach anwendbar machen. Zudem legt er dar, wie mit Java 8 Lambdas, eine sehr einfache Variante des factory Patterns und eine etwas aufwendigere Variante des builder Patterns implementiert werden kann.

Factory Pattern

Die abstrakte Fabrik definiert eine Schnittstelle zur Erzeugung einer Familie von Objekten, wobei die konkreten Klassen der zu erzeugenden Objekte erst zur Laufzeit festgelegt werden.

Bekannt – und teilweise berüchtigt – ist im Java-Umfeld natürlich das (abstract) factory Pattern. Berüchtigt deswegen, weil die Namen zeitweise „ausarten“. So müssen einige Klassen des Spring Frameworks, wie zum Beispiel die SimpleBeanFactoryAwareAspectInstanceFactory einige Witze auf ihre Kosten aushalten. Tatsächlich ist der Name aber treffend ausgewählt: Man erhält eine Factory, die Aspektinstanzen mithilfe einer weiteren Factory erzeugt.

Man stelle sich eine Formal vor, zum Beispiel die Berechnung der Summe der natürlichen Zahlen 1 bis n und repräsentiere sie als funktionales Interface:

@FunctionalInterface
public interface Computation {
    public long sum1To(long n);
}

Unsere abstrakte Fabrik soll sowohl naive Implementierungen als auch die bekannte Gaußsche Summenformel liefern können und wird daher als Lieferant von Formeln, Supplier<Computation>, abgebildet. Ein weiteres Interface oder eine abstrakte Klasse ist für Fälle wie diesen nicht nötig und verschiedene Fabriken mit unterschiedlichen Produkten können als Lambdas wie folgt aufgeschrieben werden:

import java.util.function.Supplier;

public class ComputationFactoryClient {

    public static void main(final String... a) {
        final Supplier<Computation> slowFactory = () -> n -> {
            long rv = 0L;
            for (long i = 1; i <= n; ++i) {
                rv += i;
            }
            return rv;
        };
        
        final Supplier<Computation> gaussFactory = () -> n -> (n * (n+1))/2;

        System.out.println(slowFactory.get().sum1To(100));
        System.out.println(gaussFactory.get().sum1To(100));
    }
}

Dies ist so wenig Code, dass man die Fabriken (und damit das Pattern) quasi nicht sieht. Das Factory Pattern kann in dieser Form mit wenig Aufwand deutlich mehr Flexibilität für Module bringen, zum Beispiel wenn eine Berechnung in Testfällen anders (mit anderen Ressourcen, konstantem Ergebnis o.ä.) implementiert werden und austauschbar sein soll. Die Namensfrage für komplexere Fälle bleibt natürlich bestehen.

Builder Pattern

Das Entwurfsmuster Erbauer trennt den Aufbau komplexer Objekte von ihrer konkreten Darstellung. Dadurch soll ermöglicht werden, dass derselbe Konstruktionsprozess verschiedene Darstellungen erzeugen kann.

„Erbauer“ oder Builder werden oft zusammen mit „fluent APIs“ oder kleinen, domänenspezifischen Sprachen (DSLs) in Java-Programmen genutzt.

Zum zitierten Abschnitt von Wikipedia möchte ich Folgendes ergänzen: Durch den Einsatz von Buildern zur Konstruktion unveränderlicher Instanzen können teleskopartige Konstruktoren verhindert werden, die sich zwangsläufig ergeben, wenn das erzeugte Objekte unveränderlich ist und alle Attribute über einen Konstruktor übergeben werden.

Dazu hier ein Beispiel aus der Java-Konfiguration von Spring Security:

http
    .httpBasic()
        .and()
    .authorizeRequests()
        .antMatchers(
                "/api/system/env/java.(runtime|vm).*",
                "/api/system/metrics/**"
        ).permitAll()
        .antMatchers("/api/system/env/**").denyAll()
        .antMatchers("/**").permitAll()
        .and()
    .sessionManagement()
        .sessionCreationPolicy(STATELESS);

Dies ist nur ein Ausschnitt der möglichen Konfiguration von Http Security. Man stelle sich einen dazugehörigen Konstruktor vor. Man kann anhand dieses Beispiels sehr schön sehen, dass sich das builder Pattern sehr gut zur Konfiguration von Dingen nutzen lässt.

Die ideale Implementierung eines Builder sieht für mich so aus, dass in einer flüssig lesbaren Art und Weise, unter Beachtung der korrekten Reihenfolge der notwendigen Schritte, ein unveränderliches Objekt erzeugt wird.

Hierzu habe ich folgendes Beispiel: Die Konfiguration eines belegten Brotes (nicht neu, bereits im Artikel Step builder Pattern von Marco Castigliego beschrieben aber nichtsdestotrotz immer noch relevant, gehört doch die Wurst zweifelsohne unter den Käse.

Die folgende Klasse soll konfiguriert werden:

import java.util.List;

public class Sandwich {
    private final String bread;

    private final String meat;
    
    private final String cheese;

    private final List<String> vegetables;
    
    public String getBread() {
        return bread;
    }
   
    public String getMeat() {
        return meat;
    }
    
    public String getCheese() {
        return cheese;
    }
    
    public List<String> getVegetables() {
        return vegetables;
    }
}

Der Kürze wegen verzichte ich auf Typen für Brot und Belag. Wir werden das Sandwich mithilfe dieser Schritte belegen:

public static interface ChooseBreadStep {
    public ChooseMeatStep withMeat(final String meat);

    public AddVeggiesStep vegan();
}

public static interface ChooseMeatStep {
    public ChooseCheeseStep withCheese(final String cheese);

    public AddVeggiesStep noCheese();
}

public static interface ChooseCheeseStep {
    public AddVeggiesStep addVeggie(final String vegetable);

    public CloseStep noVeggies();
}

public static interface AddVeggiesStep {
    public AddVeggiesStep addVeggie(final String vegetable);

    public CloseStep close();
}

public static interface CloseStep {
    public Sandwich create();
}

Nach der Auswahl von Brot folgt die Auswahl von Wurstwaren oder der Wahl eines veganen Belages. Käse fällt dann natürlich weg und es kann nur noch Gemüse konfiguriert werden. Zusätzlich zur Wurst kann Käse gewählt oder nach Gusto übersprungen werden. Gemüse selber kann mehrfach oder auch gar nicht hinzugefügt werden.

Die Implementierung des Builders sieht dann so aus:

private static class Builder implements ChooseBreadStep, ChooseMeatStep, ChooseCheeseStep, AddVeggiesStep, CloseStep {
    final String bread;

    String meat;

    String cheese;
    
    final List<String> vegetables = new ArrayList<>();
    
    Builder(String bread) {
        this.bread = bread;
    }
    
    @Override
    public ChooseMeatStep withMeat(String meat) {
        this.meat = meat;
        return this;
    }
    
    @Override
    public AddVeggiesStep vegan() {
        return this;
    }
    
    @Override
    public ChooseCheeseStep withCheese(String cheese) {
        this.cheese = cheese;
        return this;
    }
    
    @Override
    public AddVeggiesStep noCheese() {
        return this;
    }
    
    @Override
    public AddVeggiesStep addVeggie(String vegetable) {
        this.vegetables.add(vegetable);
        return this;
    }
    
    @Override
    public CloseStep noVeggies() {
        return this;
    }
    
    @Override
    public CloseStep close() {
        return this;
    }
        
    @Override
    public Sandwich create() {
        return new Sandwich(this);
    }
}

Die Instantiierung des Sandwiches:

    private Sandwich(Builder builder) {
        this.bread = builder.bread;
        this.meat = builder.meat;
        this.cheese = builder.cheese;
        this.vegetables = Collections.unmodifiableList(builder.vegetables);
    }

Ich ziehe es vor, den Builder beziehungsweise den letzten Schritt als Konstruktorparameter an einen privaten Konstruktor zu übergeben, da so zwei Aspekte behandelt werden: Der Teleskopkonstruktor wird genauso wie ein leerer, privater Konstruktor vermieden, der verhindert, dass das Sandwich unter Umgebung des Builders erzeugt wird.

Kein Java 8 bis jetzt. Aber: Der Builder wurde auch nicht initialisiert. Dies geschieht in einer statischen Hilfsmethode der zu erzeugenden Klasse. Diese Methode bekommt in meinem Implementierungen auch immer die obligatorischen Parameter für den ersten Schritt mit auf den Weg:

public static Sandwich make(String bread, Function<ChooseBreadStep, CloseStep> configuration) {
    return configuration.andThen(CloseStep::create).apply(new Builder(bread));
}

Der zweite Parameter ist interessant: Es ist eine Funktion, die den ChooseBreadStep auf den CloseStep abbildet. Durch die Verwendung einer solchen Funktion (ein Consumer tut es auch, wenn man ohne Schritte arbeitet) verhindert man eine Benutzung des Builders ohne die Absicht, etwas zu bauen (der Konstruktor des Builders ist privat) und man stellt klar, dass der Builder nicht weiter benutzt werden soll (er ist nach Erzeugung des Objektes nicht mehr zugänglich). Die Verwendung sieht dann so aus:

public static void main(final String...a) {        
    Sandwich.make("white", cfg -> cfg
            .withMeat("parma")
            .withCheese("cheedar")
            .addVeggie("salad")
            .close()
    );
    Sandwich.make("brown", cfg -> cfg
            .withMeat("salami")
            .noCheese()
            .close());
    Sandwich.make("spelt", cfg -> cfg
            .vegan()
            .addVeggie("salad")
            .addVeggie("gherkins")
            .addVeggie("tomatoes")
            .close()
    );
}

In diesem Fall tragen die aktuellen Sprachfeatures von Java 8 nicht zur Vereinfachung des Musters bei, wohl aber zu einer fehlerfreieren Verwendung.

Fazit

Funktionale Programmierung macht viele Entwurfsmuster sofort und ohne großes Klassendiagramm begreifbar. Lambdas sind weit mehr als nur syntaktischer Zucker für Java und lenken weit weniger stark als Konzepte des Objektorientierten Programmierens beim Lernen wirklich wichtiger Konzepte ab.

Als weiterführende Lektüre empfehle ich den Vortrag „Designing with Lambda Expressions in Java“ von Dr. Venkat Subramaniam, aus demviel Inspiration sowie das GitHub-Repository „From Gang Of Four Patterns to Lambdas“ von Mario Fusco.

Aufmacherbild: Digital Perspectives series. Composition of abstract grids von Shutterstock / Urheberrecht: agsandrew

Geschrieben von
Michael Simons
Michael Simons
Michael Simons arbeitet als Senior Consultant bei innoQ Deutschland. Er ist Mitglied des NetBeans Dream Teams und Gründer der Euregio JUG. Michael schreibt auf seinem Blog über Java, Spring und Softwarearchitektur. Auf Twitter ist er unterwegs als @rotnroll666, wo er sich unter anderem mit Java, Musik und den kleineren und größeren Problemen als Ehemann und Vater von zwei Kindern beschäftigt. Im Januar 2018 wird Michaels Buch "Spring Boot -- Moderne Softwareentwicklung im Spring-Ökosystem" im dpunkt.verlag erscheinen. Das Buch ist bereits heute unter springbootbuch.de vorbestellbar. Es behandelt Spring Boot 2 und das neue, reaktive Programmierparadigma von Spring 5 ebenso wie Spring-Grundlagen und spricht damit erfahrene Spring-Entwickler wie auch Spring-Neulinge an. Die Schaubilder in diesem Artikel stammen ebenso aus dem Buch, genau wie einige der gezeigten Beispiele. Der Code ist ist bereits jetzt auf GitHub verfügbar.
Kommentare
  1. Manuel Mauky2016-07-12 17:07:03

    Hey Michael,
    interessanter Artikel. Eine Frage dazu hätte ich aber:
    Immutable Objekte sind cool aber wirklich praktisch wird es ja erst, wenn man Abwandlungen davon erzeugen kann. Wenn ich also ausgehend von einem existierenden Objekt einen Klon erzeugen kann, der alle Eigenschaften teilt, bis auf eine bestimmte die ich verändert haben möchte.

    Beispielsweise erzeuge ich mir ein Sandwich mit Käse und Wurst, will aber anschließend, wenn keiner hinschaut, noch eine zusätzliche Scheibe Käse drauf legen.

    Wie würdest du das umsetzen?

  2. Michael Simons2016-07-13 09:41:11

    Hallo Manuel,
    schön, Dich hier wieder zu lesen.

    Deine Frage hat es - wie überall, wenn es um Anforderungen geht - in sich :)

    Da das Sandwich bereits so gestaltet ist, dass keine oder eine Scheibe Käse zulässig ist, könntest Du nur im Falle eines Sandwiches ohne oder eines veganen Sandwiches noch zusätzlich Käse ins Spiel bringen… Und damit Erwartungshaltungen verletzen.

    Und als ernsthaftere Antwort: Eine Idee wäre es zum Beispiel, wenn der Builder selber relativ kompakt ist und keinen allzu großen Speicherverbrauch hätte, ihn im erzeugten Objekt zu speichern, eine withXXX() Methode zu definieren und ihn dort neu benutzen… Man müsste den Builder allerdings cloneable machen und da fängt es für mich an, unangenehm zu werden.

    Wenn man beschränkt viele Konstruktorargumente hat, sind die Implementierungen in java.time.LocalDate etc. (withXXX()) vielleicht eine gute Möglichkeit, geänderte Kopien zu erzeugen.

    Mein dritter Vorschlag wäre es, dem Builder einen zweiten Konstruktor mit dem Sandwich als Template zu geben, die Builderattribute entsprechend korrekt zu füllen (https://gist.github.com/michael-simons/1f9a6ae42f78b2a024b84e0248dd74c6#file-sandwich-java-L100-L105) und diesen Konstruktor entsprechend aus dem Sandwich aufzurufen (https://gist.github.com/michael-simons/1f9a6ae42f78b2a024b84e0248dd74c6#file-sandwich-java-L41-L51).
    Am Beispiel des Gemüses ist das natürlich bei den gegebenen Anforderungen am einfachsten.

    Wenn es wirklich nur darum geht, ein Attribut zu ändern, würde ich an der Stelle keine weiteren Funktionen übergeben, aber dann darauf achten, dass die ändernden Methoden kein xxxStep sondern das fertige Objekt zurückgeben.

    Beste Grüße,
    Michael

Schreibe einen Kommentar

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