Builder:Motivation: Unterschied zwischen den Versionen
(Die Seite wurde neu angelegt: „Kategorie:Java Kategorie:Builder = Motivation = Ber Begriff "Builder" tritt wohl zum ersten Mal im "Design Patterns"-Buch der Gang of Four auf. Die Absicht des Builder Pattern wird dort definiert durch: :Separate the construction of a complex object from its representation<br> :so that the same construction process can create different representations Es geht dort also um die Trennung von Konstruktion und Repräsentation. Ein wesentlicher Aspekt…“) |
|||
Zeile 91: | Zeile 91: | ||
{{java|code= | {{java|code= | ||
Statement s = new Select() // | Statement s = new Select() // | ||
.fields("x, y, z") // | .fields("x", "y", "z") // | ||
.from("t") // | .from(new Table("t")) // | ||
.where("x | .where(Condition.equals("x", "z")); | ||
}} | }} | ||
Anstelle der von einander unabhängigen Setter definiert die {{java|Select}}-Klasse Methoden, die immer wieder das gleiche | Anstelle der von einander unabhängigen Setter definiert die {{java|Select}}-Klasse Methoden, die immer wieder das gleiche |
Version vom 26. Februar 2025, 08:26 Uhr
Motivation
Ber Begriff "Builder" tritt wohl zum ersten Mal im "Design Patterns"-Buch der Gang of Four auf. Die Absicht des Builder Pattern wird dort definiert durch:
- Separate the construction of a complex object from its representation
- so that the same construction process can create different representations
Es geht dort also um die Trennung von Konstruktion und Repräsentation. Ein wesentlicher Aspekt dabei ist die Anforderung verschiedene Repräsentationen aus dem Ergebnis des selben Konstruktionsprozesses zu erzeugen. Als Beispiel führt das Buch die Konstruktion eines Textes an, der dann in unterschiedliehen Formaten ausgegeben werden kann.
Auch wenn solche Probleme in der Praxis durchaus vorkommen, hat sich der Builder-Begriff davon mittlerweile losgelöst. Die meisten Beschreibungen verzichten auf den zweiten Aspekt -- die Erzeugung der Repräsentationen -- und beschränken sich auf die Konstruktion. In diesem Sinne solle der Builder auch in diesem Pamphlet verstanden werden. Es geht also um die Trennung der Konstruktion von der Verarbeitung des erzeugten Objekts. Dem Builder geht es dabei ausschließlich um die Konstruktion und die Manipulation der Objekt; was -- insbesondere fachlich -- weiter mit dem Objektgeschieht, interessiert den Builder nicht.
Paradigmen
Sucht man im Web nach dem Begriff "objektorientiert" so findet man allerhand, nur keine Definition; nicht einmal ein Konsens darüber was OO eigentlich sein soll findet man. Eine der ersten Sprachen die "Objekte" eingeführt hat, war SIMULA-67. Wie der Name suggeriert, ist SIMULA im Umfeld von software-gestützten Simulationen entstanden in denen mehr oder weniger selbständige "Dinge" mit einander interagieren. Die Definition von Grady Booth für das objektorienterte Design enstammt der realen Welt, nicht der Programmierung; die Eigenschaften treffen jedoch auch beide zu. Demnach hat eine Objekt
- eine Identität
- einen (inneren) Zustand
- eine definiertes Verhalten
Das Objekt tackert damit Daten und Funktionen die damit arbeiten zusammen. Objekte "leben" über einen längeren Zeitraum und verändern sich dabei. Seit dem Aufkommen in den 1990ern ist "Objektorientierung" das führende Paradigma, tatsächlich zeichnet sich seit einiger Zeit eine Änderung des Trends ab. Massive Parallelität in allen Bereichen -- vom Prozessor bis zum globalen Web führen immer wieder zu Problemen, weil die Daten im Objekt nicht für den parallelen Zugriff vorgesehen sind. Auch wenn SOA kein moderner Begriff mehr ist, ist der Service zum Mantra der Gegenwart geworden. Die Vorstellung eines Ablaufs, der gestartet und beendet wird, Eingaben aufnimmt und daraus ein Ergebnis berechnet; unabhängig vom Rest der Welt, mit einer Lebensdauer von Millisekunden -- das ist das Gegenteil eines Objekts.
Auch der Builder bricht mit dem OO-Paradigma indem er die Daten von ihrer Manipulation trennt. Im Grunde genommen läßt sich die Verarbeitung von Daten nicht zufriedenstellend in der OO-Welt abbilden. Eine Überweisung führt sich nicht selbst aus. Sie ist ein Auftrag, der im Rahmen eines Prozesses ausgeführt wird. Die Überweisungs-Daten müssen also vom inneren Zustand der Überweisungsmaschine gertrennt werden. Die Maschine mag ein Objekt sein, die Überweisungsdaten bilden keines; für sie ist das Datenstruktur- oder Datentyp-Modell angemessener.
Folgt man diesem Gedankenm, führt das zum Konzept das typisierten funktionalen Programmiersprachen zugrunde liegt.
Die integrierte Factory
Im Pattern-Buch taucht die Factory als Pattern nicht auf. Statt dessen giebt es zwei speziellere Pattern "Abstract Factory" und "Factory Method" deren Definition hier nicht wiederholt weren soll. Stellen wir uns statt dessen die Frage: Was möchten wir unter einer Factory verstehen?
Die kürzestmögliche Definition ist:
- Eine Factory ist ein Ding, das Objekte erzeugt.
Meistens handelt es sich da um ein Objekt, es kann aber auch eine Funktion oder eine Methode sein.
Die Parametrisierung bzw. Konfigurierbarkeit zur Erzeugung von Objekten unterschiedlicher Klassen ist zwar möglich, aber nicht wesentlich. Dieser Factory-Begriff kann also getrost auf den Builder angewandt werden: Ein Builder erzeugt Objekte. Die Klasse des Generats kann für einen Builder variieren, muß -- und tut es im allgemeinen -- aber nicht.
Was den Builder von der landläufigen Vorstellung der Factory -- und der Gof'schen factory method -- unterscheidet, ist die Tatsache daß beim Builder zur Erzeugung von Objekten mehrere Methoden aufgerufen werden müssen.
Fluent Interfaces
Geprägt wurde der Begriff wohl von Eric Evens; Martin Fowler schreibt darüber 2005 in seinem Blog. Die Idee dahinter ist, Folgen von Methoden-Aufrufen auf Objekte -- die so typisch sind für imperative Sprachen wie Java -- zu ersetzen durch verkettete Methoden-Aufrufe, die die Struktur einer domain specific languange aufweisen. Fowlers Beispiel ähnelt nicht zufällig den Buildern wie sie hier beschrieben werden. Im folgenden soll nicht der Artikel nacherzählt werden, vielmehr geht es darum einen Eindruck vom "fluent interface"-Konzept zu geben.
Manchmal werden fluent interfaces auch als Pattern bezeichnet. Das ist -- im ursprünglichen Sinne -- nicht korrekt. Ihnen liegt keine fachliche Motivation zugrunde und sie dienen nicht der Lösung irgendeiner Problem-Kategorie. Sie sind ein Design-Konzept für Programmier-Interfaces.
In Java könnte man sich folgenden Code vorstellen:
Statement s = new select(); Fields f = new Fields("x, y, z"); s.setFields(f); Tables t = new Tables("t"); s.setTables(t); Condition c = new Condition("x = z"); s.setConditions(c)
Man kann -- anstelle der Verwendung lokaler Variablen -- die Objekt-Erzeugung inline durchführen, aber das macht den Code nicht schöner. Man kommt beim Lesen nicht umhin, den Code sequentiell zu analysieren: Erzeugt wird ein Statement, dann wird ein "Field"-Objekt erzeugt, das wird dem Statement hinzugefügt, und so fort. Der Eingeweihte erkennt schnell das SQL-Statement das hier konstruiert wird.
Man kann das -- anlehnend an die SQL-Syntax -- auch in Java anders formulieren:
Statement s = new Select() // .fields("x", "y", "z") // .from(new Table("t")) // .where(Condition.equals("x", "z"));
Anstelle der von einander unabhängigen Setter definiert die Select
-Klasse Methoden, die immer wieder das gleiche
Select
-Objekt als Ergebnis liefern. Die DSL die hier definiert wird, entspricht der SQL-Syntax und man kann sich
vorstellen, daß -- wenn die where
-Methode entsprechend modelliert wird -- auch die Definition geschachtelter
Select-Statements möglich sind.
Das sind denn auch die beiden Elemente der fluent interfaces, die hier wesentlich sind. Zunächst einmal das "method chaining": Man versteht darunter, daß die Methoden-Aufrufe nicht in einzelnene Befehlen geschehen -- wie das bei Verwendung von Settern und lokalen Variablen der Fall ist -- sondern in einer (mehr oder weniger langen) Kette von aufeinander angewandten Methoden, so daß für die komplette Folge nur ein einziger Befehl (gewissermaßen ein einziges Semikolon) benötigt wird.
Das zweite Element ist die Schachtelung. Statt Unter-Objekte in lokalen Variablen abzulegen und dort zu manipulieren, werden sie -- wieder in einer einzigen Methoden-Kette erzeugt und dann direkt als Argumente an Methoden übergeben. Das ganze Konstrukt erhält dadurch einen eher funktionalen Charakter. Das ist für den "prozeduralen" Java-Hacker ein eher ungewohntes und damit befremdliches Konzept.
Ich stand der "fluent interface"-Idee von Anfang an skeptisch gegenüber. Solange die dadurch definierte DSL den Charakter einer formalen Sprache hat -- wie das bei SQL der Fall ist -- die einer eindeutig definierten inneren Logik folgt, giebt es eigentlich kein Problem. Wenn man aber versucht -- und so wird es vielfach verkauft -- natürliche Sprache darüber nachzubilden wird die Sache schwierig. Computer-Programme sind formal und lassen keine Unschärfe zu; das Gegenteil ist ein wesentliches Merkmal natürlicher Sprachen.
Wie kann man nun zusammenfassen inwiefern fluent interfaces bei der Builder-Konstruktion bedeutsam sind?
Der Builder verwendet Method Chaining und Schachtelung, um klassische Anweisungsfolgen durch eine DSL zu ersetzen.
Dadurch wird einerseits eine bessere Lesbarkeit und eine bessere Verständlichkeit der zugrundeliegenden Semantik erreicht und andererseits boiler plate code gekapselt, der sonst die die Aufmerksamkeit absaugen würde.
Separation of Concerns
Wie im Abschnitt "Zugriffs-Kontolle" ausführlicher beschrieben wird, bietet der Builder eine Möglichkeit die Zuständigkeiten über Entities so trennen. Das klingt zunächst einmal etwas merkwürdig; welche Zuständigkeiten sollte man bei einem Entity -- das selbst keine Logik enthält -- trennen?
Es geht hier um die Trennung von Erzeugung (bzw. Manipulation) des Objekts und dem Zugriff auf den Inhalt. Die Idee des immutable Objekts ist Java grundsätzlich fremd, sie widerspricht geradezu der eigentlichen Objekt-Idee. Aber tatsächlich haben unveränderliche Objekte in einer multithreading-Umgebung große Vorteile, weil sie die Synchronisation stark vereinfachen. Nicht umsonst ist das funktionale Paradigma weiterhin auf dem Vormarsch.
Konsequent durchgezogen, kann das Entity vollständig auf Setter verzichten indem es die Manipulation dem Builder überträgt und sich darauf beschränken Daten zu halten und herauszugeben.
Statisch oder dynamisch?
Das ist für jede Programmiersprache die -- oder zumindest eine -- Gretchenfrage. Was ist damit gemeint? Statisch heißt "zur Compile-Zeit", dynamisch -- oder nicht-statisch -- bezeichnet das Gegenstück, nämlich "zur Laufzeit".
Wenn Tony Hoare die Einführung von "null" in ALGOL als "billion Dollar mistake" bezeichnet hat, meinte er damit, daß die null- oder Ungültigkeitsprüfung optimalerweise zur Compile-Zeit stattfinden sollte. Es giebt mittlerweile Sprachen, die Konzepte dafür implementieren -- in Java sind sie nur unzulänglich ausgeführt.
Inwiefern betrifft das den Builder? Bei jeder Prüfung muß man sich fragen, wann diese stattfinden soll. Was ist mit Muß-Werten? Kann man das Vorhandensein zur Compile-Zeit prüfen -- was sicherer wäre -- oder soll man die Prüfung auf die Laufzeit -- was einfacher wäre -- verlagern? Auf diese spezielle Frage wird später noch eingegangen, betrachten wir die Frage etwas allgemeiner: "wann statisch, wann dynamisch"?
Die Antwort lautet: -- wie überraschend -- kommt drauf an...
Grundsätzlich kann man sagen: statische Prüfung ist immer aufwändiger als die dynamische. Definiert an einen einfachen Builder mit mehrereren Konfigurations-Methoden, kann man die Methoden in beliebiger Reihenfolge aufrufen; im einfachsten Falle sogar mehrfach. Möchte man eine Reihenfolge festlegen, die bereits zur Compile-Zeit eingehalten werden muß ist das prinzipiell möglich. es erfordert allerdings zusätzliche Klassen und verursacht Aufwand.
Möchte man zur Compile-Zeit sicher sein, daß Muß-Felder gefüllt sind, geht das nur über (häßliche) Konstruktoren und selbst dann hat man nur eingeschränkte Kontrolle darüber, ob die Felder mit brauchbaren Werten gefüllt sind.
Es bleibt also eine Abschätzung: Lohnt sich der Aufwand -- wenn es denn überhaupt geht?
Der Aldi-Tip: Mach's dynamisch und schreib' viele Unit-Tests.
Migration/Versionierung
Die Separation von Datenspeicherung -- im Entity -- und Manipulation -- im Builder -- macht es einfacher, zwischen unterschiedlichen Versionen von Entities zu unterscheiden, diese zu erzeugen und Migration von Daten zwischen Versionen durchzuführen.