it-swarm.com.de

Warum benötigen wir eine Builder-Klasse, wenn wir ein Builder-Muster implementieren?

Ich habe viele Implementierungen des Builder-Musters gesehen (hauptsächlich in Java). Alle von ihnen haben eine Entitätsklasse (sagen wir eine Person Klasse) und eine Builder-Klasse PersonBuilder. Der Builder "stapelt" eine Vielzahl von Feldern und gibt mit den übergebenen Argumenten ein new Person Zurück. Warum brauchen wir explizit eine Builder-Klasse, anstatt alle Builder-Methoden in die Person -Klasse selbst zu setzen?

Zum Beispiel:

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

Ich kann einfach Person john = new Person().withName("John"); sagen

Warum braucht man eine PersonBuilder Klasse?

Der einzige Vorteil, den ich sehe, ist, dass wir die Felder Person als final deklarieren können, um so die Unveränderlichkeit sicherzustellen.

33
Boyan Kushlev

Es ist so, dass Sie nveränderlich UND benannte Parameter simulieren gleichzeitig sein können.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Das hält Ihre Handschuhe von der Person fern, bis der Status festgelegt ist, und Sie können ihn nach dem Festlegen nicht mehr ändern, aber jedes Feld ist eindeutig gekennzeichnet. Sie können dies nicht mit nur einer Klasse in Java tun.

Es sieht so aus, als würden Sie über Josh Blochs Builder Pattern sprechen. Dies sollte nicht mit dem Gang of Four Builder Pattern verwechselt werden. Das sind verschiedene Tiere. Beide lösen Bauprobleme auf unterschiedliche Weise.

Natürlich können Sie Ihr Objekt ohne Verwendung einer anderen Klasse erstellen. Aber dann muss man sich entscheiden. Sie verlieren entweder die Fähigkeit, benannte Parameter in Sprachen zu simulieren, in denen sie nicht vorhanden sind (wie Java), oder Sie verlieren die Fähigkeit, während der gesamten Lebensdauer des Objekts unveränderlich zu bleiben.

Unveränderliches Beispiel, hat keine Namen für Parameter

Person p = new Person("Arthur Dent", 42);

Hier erstellen Sie alles mit einem einzigen einfachen Konstruktor. Dadurch bleiben Sie unveränderlich, verlieren jedoch die Simulation benannter Parameter. Das ist mit vielen Parametern schwer zu lesen. Computer kümmern sich nicht darum, aber es ist schwer für die Menschen.

Simuliertes Beispiel für benannte Parameter mit herkömmlichen Setzern. Nicht unveränderlich.

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

Hier bauen Sie alles mit Setzern und simulieren benannte Parameter, sind aber nicht mehr unveränderlich. Jede Verwendung eines Setters ändert den Objektstatus.

Wenn Sie also die Klasse hinzufügen, können Sie beides tun.

Die Validierung kann in build() durchgeführt werden, wenn ein Laufzeitfehler für ein fehlendes Altersfeld für Sie ausreicht. Sie können dies aktualisieren und erzwingen, dass age() mit einem Compilerfehler aufgerufen wird. Nur nicht mit dem Josh Bloch Builder-Muster.

Dafür benötigen Sie eine interne domänenspezifische Sprache (iDSL).

Auf diese Weise können Sie verlangen, dass sie age() und name() aufrufen, bevor Sie build() aufrufen. Sie können dies jedoch nicht einfach tun, indem Sie jedes Mal this zurückgeben. Jedes zurückgegebene Objekt gibt ein anderes Objekt zurück, das Sie dazu zwingt, das nächste Objekt aufzurufen.

Die Verwendung könnte folgendermaßen aussehen:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Aber dieses:

Person p = personBuilder
    .age(42)
    .build()
;

verursacht einen Compilerfehler, da age() nur für den von name() zurückgegebenen Typ gültig ist.

Diese iDSLs sind extrem leistungsfähig ( JOOQ oder Java8 Streams zum Beispiel) und sehr gut zu verwenden, insbesondere wenn Sie ein = verwenden IDE mit Code-Vervollständigung, aber das Einrichten ist ein gutes Stück Arbeit. Ich würde empfehlen, sie für Dinge zu speichern, gegen die ein gutes Stück Quellcode geschrieben wird.

28
candied_orange

Warum eine Builder-Klasse verwenden/bereitstellen:

  • Unveränderliche Objekte herstellen - der Vorteil, den Sie bereits identifiziert haben. Nützlich, wenn die Konstruktion mehrere Schritte umfasst. FWIW, Unveränderlichkeit sollte als wichtiges Werkzeug bei unserer Suche nach wartbaren und fehlerfreien Programmen angesehen werden.
  • Wenn die Laufzeitdarstellung des endgültigen (möglicherweise unveränderlichen) Objekts für das Lesen und/oder die Speicherplatznutzung optimiert ist, jedoch nicht für die Aktualisierung. String und StringBuilder sind hier gute Beispiele. Das wiederholte Verketten von Zeichenfolgen ist nicht sehr effizient, daher verwendet der StringBuilder eine andere interne Darstellung, die zum Anhängen geeignet ist - jedoch nicht so gut für die Speicherplatznutzung und nicht so gut zum Lesen und Verwenden wie die reguläre Zeichenfolgenklasse.
  • Konstruierte Objekte klar von Objekten im Bau trennen. Dieser Ansatz erfordert einen klaren Übergang vom Bau zum Bau. Für den Verbraucher gibt es keine Möglichkeit, ein im Bau befindliches Objekt mit einem konstruierten Objekt zu verwechseln: Das Typsystem erzwingt dies. Das bedeutet, dass wir diesen Ansatz manchmal verwenden können, um sozusagen "in die Grube des Erfolgs zu fallen", und wenn wir eine Abstraktion für andere (oder uns selbst) vornehmen (wie eine API oder eine Ebene), kann dies sehr gut sein Ding.
57
Erik Eidt

Ein Grund wäre, sicherzustellen, dass alle übergebenen Daten den Geschäftsregeln entsprechen.

In Ihrem Beispiel wird dies nicht berücksichtigt, aber nehmen wir an, dass jemand eine leere Zeichenfolge oder eine Zeichenfolge aus Sonderzeichen übergeben hat. Sie möchten eine Art Logik erstellen, die darauf basiert, sicherzustellen, dass der Name tatsächlich ein gültiger Name ist (was eigentlich eine sehr schwierige Aufgabe ist).

Sie könnten das alles in Ihre Personenklasse aufnehmen, insbesondere wenn die Logik sehr klein ist (z. B. nur sicherstellen, dass ein Alter nicht negativ ist), aber wenn die Logik wächst, ist es sinnvoll, sie zu trennen.

21
Deacon

Ein etwas anderer Blickwinkel als in anderen Antworten.

Der Ansatz withFoo ist hier problematisch, da sie sich wie Setter verhalten, aber so definiert sind, dass die Klasse die Unveränderlichkeit unterstützt. In Java Klassen, wenn eine Methode eine Eigenschaft ändert, ist es üblich, die Methode mit 'set' zu starten. Ich habe es nie als Standard geliebt, aber wenn Sie etwas anderes tun, wird es - Überraschung Leute und das ist nicht gut. Es gibt eine andere Möglichkeit, die Unveränderlichkeit mit der Basis-API zu unterstützen, die Sie hier haben. Zum Beispiel:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

Es bietet nicht viel, um nicht ordnungsgemäß erstellte teilweise erstellte Objekte zu verhindern, verhindert jedoch Änderungen an vorhandenen Objekten. Es ist wahrscheinlich albern für so etwas (und der JB Builder auch). Ja, Sie werden mehr Objekte erstellen, aber dies ist nicht so teuer , wie Sie vielleicht denken.

Diese Vorgehensweise wird meistens bei gleichzeitigen Datenstrukturen wie CopyOnWriteArrayList verwendet. Und dies deutet darauf hin, warum Unveränderlichkeit wichtig ist. Wenn Sie Ihren Code threadsicher machen möchten, sollte die Unveränderlichkeit fast immer berücksichtigt werden. In Java darf jeder Thread einen lokalen Cache mit variablem Status behalten. Damit ein Thread die in anderen Threads vorgenommenen Änderungen sehen kann, muss ein synchronisierter Block oder eine andere Parallelitätsfunktion verwendet werden. All dies erhöht den Aufwand für den Code. Aber wenn Ihre Variablen endgültig sind, gibt es nichts zu tun. Der Wert ist immer der, auf den er initialisiert wurde, und daher sehen alle Threads unabhängig davon, was passiert.

6
JimmyJames

Builder-Objekt wiederverwenden

Wie bereits erwähnt, sind die Unveränderlichkeit und die Überprüfung der Geschäftslogik aller Felder zur Validierung des Objekts die Hauptgründe für ein separates Builder-Objekt.

Die Wiederverwendbarkeit ist jedoch ein weiterer Vorteil. Wenn ich viele Objekte instanziieren möchte, die sehr ähnlich sind, kann ich kleine Änderungen am Builder-Objekt vornehmen und mit der Instanziierung fortfahren. Das Builder-Objekt muss nicht neu erstellt werden. Durch diese Wiederverwendung kann der Builder als Vorlage für die Erstellung vieler unveränderlicher Objekte fungieren. Es ist ein kleiner Vorteil, aber es könnte nützlich sein.

3
yitzih

Ein Builder kann auch so definiert werden, dass er eine Schnittstelle oder eine abstrakte Klasse zurückgibt. Sie können den Builder verwenden, um das Objekt zu definieren, und der Builder kann bestimmen, welche konkrete Unterklasse zurückgegeben werden soll, basierend beispielsweise darauf, welche Eigenschaften festgelegt sind oder auf welche sie festgelegt sind.

2
Ed Marty

Das Builder-Muster wird zum Erstellen/Erstellen des Objekts verwendet Schritt für Schritt durch Festlegen der Eigenschaften. Wenn alle erforderlichen Felder festgelegt sind, wird das endgültige Objekt mithilfe der Build-Methode zurückgegeben. Das neu erstellte Objekt ist unveränderlich. Hierbei ist vor allem zu beachten, dass das Objekt nur zurückgegeben wird, wenn die endgültige Erstellungsmethode aufgerufen wird. Dies stellt sicher, dass alle Eigenschaften auf das Objekt festgelegt sind und sich das Objekt daher nicht in inkonsistenter Zustand befindet, wenn es von der Builder-Klasse zurückgegeben wird.

Wenn wir keine Builder-Klasse verwenden und alle Builder-Klassenmethoden direkt in die Person-Klasse selbst einfügen, müssen wir zuerst das Objekt erstellen und dann die Setter-Methoden für das erstellte Objekt aufrufen, was zu einem inkonsistenten Zustand des Objekts zwischen der Erstellung führt von Objekt und Festlegen der Eigenschaften.

Durch die Verwendung der Builder-Klasse (d. H. Einer anderen externen Entität als der Person-Klasse selbst) stellen wir sicher, dass sich das Objekt niemals im inkonsistenten Zustand befindet.

2

Tatsächlich können Sie die Builder-Methoden für Ihre Klasse selbst haben und sind dennoch unveränderlich. Dies bedeutet lediglich, dass die Builder-Methoden neue Objekte zurückgeben, anstatt die vorhandenen zu ändern.

Dies funktioniert nur, wenn es eine Möglichkeit gibt, ein anfängliches (gültiges/nützliches) Objekt abzurufen (z. B. von einem Konstruktor, der alle erforderlichen Felder festlegt, oder von einer Factory-Methode, die Standardwerte festlegt), und die zusätzlichen Builder-Methoden dann modifizierte Objekte basierend zurückgeben auf dem bestehenden. Diese Builder-Methoden müssen sicherstellen, dass Sie unterwegs keine ungültigen/inkonsistenten Objekte erhalten.

Dies bedeutet natürlich, dass Sie viele neue Objekte haben, und Sie sollten dies nicht tun, wenn die Erstellung Ihrer Objekte teuer ist.

Ich habe das im Testcode verwendet, um Hamcrest-Matcher für eines meiner Geschäftsobjekte zu erstellen. Ich erinnere mich nicht an den genauen Code, aber er sah ungefähr so ​​aus (vereinfacht):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

Ich würde es dann so in meinen Unit-Tests verwenden (mit geeigneten statischen Importen):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));
2
Paŭlo Ebermann

Ein weiterer Grund, der hier noch nicht explizit erwähnt wurde, ist, dass die Methode build() überprüfen kann, ob alle Felder gültige Werte enthalten (entweder direkt festgelegt oder von anderen Werten anderer Felder abgeleitet) ist wahrscheinlich der wahrscheinlichste Fehlermodus, der sonst auftreten würde.

Ein weiterer Vorteil ist, dass Ihr Person -Objekt eine einfachere Lebensdauer und eine einfache Menge von Invarianten hat. Sie wissen, dass Sie, sobald Sie einen Person p Haben, einen p.name Und einen gültigen p.age Haben. Keine Ihrer Methoden muss für Situationen wie "Nun, was ist, wenn das Alter festgelegt ist, aber nicht der Name, oder was ist, wenn der Name festgelegt ist, aber nicht das Alter?" Dies reduziert die Gesamtkomplexität der Klasse.