Warum sollte ein Typ mit seinem Builder gekoppelt werden?

20

Ich habe kürzlich eine Antwort von mir auf Code Review gelöscht , die so begann:

private Person(PersonBuilder builder) {

Halt. Rote Flagge. Ein PersonBuilder würde eine Person erstellen. es kennt eine Person. Die Person-Klasse sollte nichts über einen PersonBuilder wissen - es ist nur ein unveränderlicher Typ. Sie haben hier eine kreisförmige Kopplung erstellt, wobei A von B abhängt, was von A abhängt.

Die Person sollte nur ihre Parameter aufnehmen; Ein Kunde, der bereit ist, eine Person zu erstellen, ohne sie zu erstellen, sollte dazu in der Lage sein.

Ich wurde mit einer Gegenstimme geschlagen und sagte, dass (unter Berufung auf) Red Flag, warum? Die Implementierung hier hat dieselbe Form, die Joshua Bloch in seinem Buch "Effective Java" (Punkt 2) demonstriert hat.

Es sieht also so aus, als ob die einzig richtige Methode zum Implementieren eines Builder-Musters in Java darin besteht , den Builder in einen verschachtelten Typ zu verwandeln (es handelt sich jedoch nicht um diese Frage) und dann das Produkt (die Klasse des zu erstellenden Objekts) zu erstellen ) nimm eine Abhängigkeit vom Builder , wie folgt:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

Dieselbe Wikipedia-Seite für dasselbe Builder-Muster hat eine völlig andere (und viel flexiblere) Implementierung für C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Wie Sie sehen, weiß das Produkt hier nichts über eine BuilderKlasse, und wenn es auch wichtig ist, könnte es durch einen direkten Konstruktoraufruf, eine abstrakte Factory, ... oder einen Builder instanziiert werden - soweit ich das verstehe Produkt eines Schöpfungsmusters muss nie etwas darüber wissen, was es schafft.

Mir wurde das Gegenargument zugestellt (das anscheinend ausdrücklich in Blochs Buch verteidigt wird), dass ein Builder-Muster verwendet werden könnte, um einen Typ zu überarbeiten, bei dem ein Konstruktor mit Dutzenden optionaler Argumente aufgebläht wäre. Anstatt mich an das zu halten, von dem ich dachte, dass ich es wüsste, habe ich ein bisschen auf dieser Seite recherchiert und festgestellt, dass dieses Argument , wie ich vermutete, kein Wasser hält .

Also, was ist der Deal? Warum überarbeitete Lösungen für ein Problem finden, das es eigentlich gar nicht geben sollte? Wenn wir Joshua Bloch für eine Minute von seinem Podest nehmen, können wir einen einzigen guten, gültigen Grund finden, zwei konkrete Typen zu koppeln und es als Best Practice zu bezeichnen?

Das alles riecht für mich nach Frachtkult-Programmierung .

Mathieu Guindon
quelle
12
Und diese "Frage" stinkt nur nach Schimpfen ...
Philip Kendall
2
@ PhilipKendall vielleicht ein wenig. Aber ich bin wirklich daran interessiert zu verstehen, warum jede Java Builder-Implementierung diese enge Kopplung aufweist.
Mathieu Guindon
19
@PhilipKendall Es stinkt nach Neugier für mich.
Mast
8
@PhilipKendall Es liest sich wie ein Scherz, ja, aber im Kern gibt es eine gültige Frage zum Thema. Vielleicht könnte das Geschwätz herausgeschnitten werden?
Andres F.
Das Argument "zu viele Konstruktorargumente" beeindruckt mich nicht. Aber ich bin noch weniger beeindruckt von diesen beiden Builder-Beispielen. Vielleicht liegt es daran, dass die Beispiele zu einfach sind, um nicht-triviales Verhalten zu demonstrieren, aber das Java-Beispiel liest sich wie eine verschlungene, fließende Schnittstelle, und das C # -Beispiel ist nur ein Umweg, um Eigenschaften zu implementieren. Keines der beiden Beispiele führt irgendeine Form der Parameterüberprüfung durch. Ein Konstruktor würde mindestens den Typ und die Anzahl der bereitgestellten Argumente validieren, was bedeutet, dass der Konstruktor mit zu vielen Argumenten gewinnt.
Robert Harvey

Antworten:

22

Ich bin mit Ihrer Behauptung nicht einverstanden, dass es eine rote Fahne ist. Ich bin auch nicht einverstanden mit Ihrer Darstellung des Kontrapunkts, dass es einen richtigen Weg gibt, ein Builder-Muster darzustellen.

Für mich gibt es eine Frage: Ist der Builder ein notwendiger Bestandteil der API des Typs? Hier ist die PersonBuilder ein notwendiger Teil der Person API?

Es ist durchaus sinnvoll, dass eine validitätsgeprüfte, unveränderliche und / oder eng gekapselte Personenklasse notwendigerweise über einen von ihr bereitgestellten Builder erstellt wird (unabhängig davon, ob dieser Builder verschachtelt oder benachbart ist). Auf diese Weise kann die Person alle ihre Felder privat oder paketprivat und endgültig lassen und die Klasse für Änderungen offen lassen, wenn sie in Bearbeitung ist. (Möglicherweise wird es für Änderungen geschlossen und für Erweiterungen geöffnet, aber das ist derzeit nicht wichtig und für unveränderliche Datenobjekte besonders umstritten.)

Wenn eine Person notwendigerweise durch einen bereitgestellten PersonBuilder als Teil eines API-Entwurfs auf Paketebene erstellt wird, ist eine zirkuläre Kopplung in Ordnung und eine rote Flagge ist hier nicht garantiert. Insbesondere haben Sie dies als eine unbestreitbare Tatsache und nicht als eine API-Entwurfsmeinung oder -entscheidung angegeben, sodass dies möglicherweise zu einer schlechten Reaktion beigetragen hat. Ich hätte Sie nicht herabgestimmt, aber ich beschuldige niemanden dafür, und ich überlasse den Rest der Diskussion darüber, "was eine Herabstufung rechtfertigt", der Coderevisions- Hilfe und dem Meta . Abstimmungen passieren; Es ist kein "Schlag".

Wenn Sie viele Möglichkeiten zum Erstellen eines Person-Objekts offen lassen möchten, kann der PersonBuilder natürlich zum Consumer oder Utility werdender Personenklasse, von der die Person nicht direkt abhängen soll. Diese Wahl scheint flexibler zu sein - wer möchte nicht mehr Optionen zur Objekterstellung? -, aber um Garantien (wie Unveränderlichkeit oder Serialisierbarkeit) zu gewährleisten, muss die Person plötzlich eine andere Art von öffentlicher Erstellungstechnik verfügbar machen, z. B. einen sehr langen Konstruktor . Wenn der Builder einen Konstruktor durch viele optionale Felder darstellen oder ersetzen soll oder ein Konstruktor, der geändert werden kann, ist der lange Konstruktor der Klasse möglicherweise ein Implementierungsdetail, das besser verborgen bleibt. (Bedenken Sie auch, dass ein offizieller und notwendiger Builder Sie nicht daran hindert, Ihr eigenes Konstruktionsdienstprogramm zu schreiben, sondern dass Ihr Konstruktionsdienstprogramm den Builder wie in meiner ursprünglichen Frage oben als API verwenden kann.)


ps: Ich stelle fest, dass das von Ihnen aufgeführte Code-Review-Beispiel eine unveränderliche Person hat, aber das Gegenbeispiel von Wikipedia listet ein veränderliches Auto mit Gettern und Setzern auf. Wenn Sie die Sprache und die Invarianten zwischen ihnen konsistent halten, ist es möglicherweise einfacher zu erkennen, dass die erforderlichen Maschinen im Auto fehlen.

Jeff Bowman unterstützt Monica
quelle
Ich denke, ich sehe Ihren Standpunkt darin, den Konstruktor als Implementierungsdetail zu behandeln. Es scheint einfach nicht so, als würde Effective Java diese Botschaft nicht richtig vermitteln (ich habe dieses Buch nicht gelesen / nicht gelesen), was eine Menge jüngerer (?) Java-Entwickler davon ausgehen lässt, dass "das ist, wie es gemacht wird", unabhängig vom gesunden Menschenverstand . Ich bin damit einverstanden, dass die Wikipedia-Beispiele zum Vergleich schlecht sind. Aber hey, es ist Wikipedia. Die gelöschte Antwort hat ohnehin eine positive Netto-Punktzahl (und es besteht die Chance, dass ich endlich ein [Abzeichen: Gruppenzwang] bekomme).
Mathieu Guindon
1
Dennoch kann ich die Idee, dass ein Typ mit zu vielen Konstruktorargumenten ein Designproblem darstellt (es riecht, als würde er die SRP brechen), nicht abschütteln , dass sich ein Builder-Muster schnell unter einen Teppich schiebt.
Mathieu Guindon
3
@ Mat'sMug Mein Beitrag oben geht davon aus, dass die Klasse so viele Konstruktorargumente benötigt . Ich würde es auch als Designgeruch behandeln, wenn wir über eine beliebige Geschäftslogikkomponente sprechen, aber für ein Datenobjekt ist es nicht fraglich, ob ein Typ zehn oder zwanzig direkte Attribute hat. Es ist keine Verletzung der SRP, wenn ein Objekt beispielsweise einen einzelnen stark typisierten Datensatz in einem Protokoll darstellt .
Jeff Bowman unterstützt Monica
1
Meinetwegen. Ich verstehe den String(StringBuilder)Konstruktor jedoch immer noch nicht , was diesem "Prinzip" zu gehorchen scheint, bei dem es für einen Typ normal ist, eine Abhängigkeit von seinem Builder zu haben. Ich war verblüfft, als ich sah, dass der Konstruktor überladen war. Es ist nicht so, als hätte a String42 Konstruktorparameter ...
Mathieu Guindon
3
@Luaan Odd. Ich habe es buchstäblich nie gebraucht gesehen und wusste nicht, dass es existiert; toString()ist universell.
chrylis -on strike-
7

Ich verstehe, wie ärgerlich es sein kann, wenn in einer Debatte "Appell an die Autorität" verwendet wird. Argumente sollten auf ihrer eigenen IMO stehen, und obwohl es nicht falsch ist, auf den Rat einer so angesehenen Person hinzuweisen, kann es nicht als vollständiges Argument an sich betrachtet werden, da wir wissen, dass die Sonne um die Erde reist, weil Aristoteles dies sagte .

Abgesehen davon halte ich die Prämisse Ihrer Argumentation für ähnlich. Wir sollten niemals zwei konkrete Typen koppeln und es als Best Practice bezeichnen, weil ... jemand das gesagt hat?

Damit das Argument, dass die Kopplung problematisch ist, Gültigkeit hat, müssen bestimmte Probleme auftreten, die dadurch verursacht werden. Wenn dieser Ansatz nicht diesen Problemen unterliegt, können Sie logischerweise nicht argumentieren, dass diese Form der Kopplung problematisch ist. Wenn Sie also hier auf etwas eingehen möchten, müssen Sie zeigen, wie dieser Ansatz diesen Problemen unterliegt und / oder wie das Entkoppeln des Builders einen Entwurf verbessert.

Kurz gesagt, ich kämpfe darum, wie der gekoppelte Ansatz zu Problemen führen wird. Die Klassen sind zwar vollständig gekoppelt, aber der Builder dient nur zum Erstellen von Instanzen der Klasse. Eine andere Sichtweise wäre eine Erweiterung des Konstruktors.

JimmyJames
quelle
Also ... höhere Kopplung, geringerer Zusammenhalt => Happy End? hmm ... Ich sehe den Punkt über den Builder als "Erweiterung des Konstruktors", aber um ein anderes Beispiel zu nennen, ich verstehe nicht, was Stringmit einer Konstruktorüberladung geschieht, die einen StringBuilderParameter übernimmt (obwohl es fraglich ist, ob a StringBuilderein Builder ist , aber das ist es) eine andere Geschichte).
Mathieu Guindon
4
@ Mat'sMug Um es klar auszudrücken, ich habe in beiden Fällen kein starkes Gefühl. Ich neige nicht dazu, das Builder-Muster zu verwenden, weil ich keine Klassen erstelle, die viele Statusangaben enthalten. Ich würde eine solche Klasse im Allgemeinen aus Gründen zerlegen, auf die Sie an anderer Stelle verweisen. Ich sage nur, wenn Sie zeigen können, wie dieser Ansatz zu einem Problem führt, spielt es keine Rolle, ob Gott herabkam und Moses dieses Baumuster auf einer Steintafel überreichte. Du gewinnst'. Auf der anderen Seite, wenn Sie nicht ...
JimmyJames
Eine legitime Verwendung des Builder-Musters, das ich in meiner eigenen Codebasis habe, ist das Verspotten der VBIDE-API. Im Grunde genommen geben Sie den Zeichenfolgeninhalt eines Codemoduls an, und der Builder gibt Ihnen ein VBEModell mit Fenstern, einem aktiven Codebereich, einem Projekt und einer Komponente mit einem Modul, das die von Ihnen angegebene Zeichenfolge enthält. Ohne sie weiß ich nicht, wie ich 80% der Tests in Rubberduck schreiben könnte , zumindest ohne mir jedes Mal in die Augen zu stechen, wenn ich sie anschaue.
Mathieu Guindon
@ Mat'sMug Es kann wirklich nützlich sein, um die Zuordnung von Daten zu unveränderlichen Objekten aufzuheben. Diese Art von Objekten sind in der Regel Säcke mit Eigenschaften, dh nicht wirklich OO. Dieser ganze Problembereich ist so ein PITA, dass alles, was dazu beiträgt, genutzt werden kann, unabhängig davon, ob gute Praktiken befolgt werden oder nicht.
JimmyJames
4

Um zu beantworten, warum ein Typ mit einem Builder gekoppelt ist, muss verstanden werden, warum jeder überhaupt einen Builder verwenden würde. Insbesondere: Bloch empfiehlt die Verwendung eines Builders, wenn Sie über eine große Anzahl von Konstruktorparametern verfügen. Das ist nicht der einzige Grund, einen Builder zu verwenden, aber ich spekuliere, dass dies der häufigste ist. Kurz gesagt, der Builder ersetzt diese Konstruktorparameter, und häufig wird der Builder in derselben Klasse deklariert, die er erstellt. Die einschließende Klasse hat also bereits Kenntnisse über den Builder, und die Übergabe als Konstruktorargument ändert daran nichts. Deshalb würde ein Typ mit seinem Builder gekoppelt.

Warum bedeutet eine große Anzahl von Parametern, dass Sie einen Builder verwenden sollten? Eine große Anzahl von Parametern zu haben, ist in der realen Welt keine Seltenheit und verstößt auch nicht gegen das Prinzip der Einzelverantwortung. Es ist auch schlecht, wenn Sie zehn String-Parameter haben und sich merken möchten, welche welche sind und ob es der fünfte oder der sechste String ist, der null sein soll. Ein Builder erleichtert das Verwalten der Parameter und kann auch andere coole Dinge tun, die Sie im Buch nachlesen sollten, um mehr darüber zu erfahren.

Wann verwenden Sie einen Builder, der nicht mit seinem Typ gekoppelt ist? Im Allgemeinen, wenn die Komponenten eines Objekts nicht sofort verfügbar sind und die Objekte mit diesen Teilen nicht den Typ kennen müssen, den Sie erstellen möchten, oder wenn Sie einen Builder für eine Klasse hinzufügen, über die Sie keine Kontrolle haben .

Alter fetter Ned
quelle
Seltsamerweise brauchte ich nur einen Builder, wenn ich einen Typ hatte, der keine definitive Form hat, wie diesen MockVbeBuilder , der ein Mock der API einer IDE erstellt. ein Test könnte braucht nur einen einfachen Code - Modul, ein weiterer Test zwei Formen und ein Klassenmodul benötigen könnte - IMO , das , was Erbauer des GoF ist für. Das heißt, ich könnte anfangen, es für eine andere Klasse mit einem verrückten Konstruktor zu verwenden ... aber die Kopplung fühlt sich überhaupt nicht richtig an.
Mathieu Guindon
1
@ Mat's Mug Die Klasse, die Sie vorgestellt haben, scheint repräsentativer für das Builder-Entwurfsmuster zu sein (nach Design Patterns von Gamma et al.), Nicht unbedingt für eine einfache Builder-Klasse. Sie bauen beide Dinge, aber sie sind nicht dasselbe. Außerdem "braucht" man nie einen Builder, es macht einfach andere Dinge einfacher.
Old Fat Ned
1

Das Produkt eines Schöpfungsmusters muss nie etwas darüber wissen, was es erzeugt.

Die PersonKlasse weiß nicht, was es schafft. Es gibt nur einen Konstruktor mit einem Parameter, na und? Der Name des Parametertyps endet mit ... Builder, der eigentlich nichts erzwingt. Es ist doch nur ein Name.

Der Builder ist ein verherrlichtes 1 - Konfigurationsobjekt. Und ein Konfigurationsobjekt gruppiert eigentlich nur Eigenschaften, die zueinander gehören. Wenn sie nur zu dem Zweck zusammengehören, an den Konstruktor einer Klasse weitergegeben zu werden, dann sei es auch! Beideorigin und destinationdas StreetMapBeispiel sind vom Typ Point. Wäre es möglich, die einzelnen Koordinaten jedes Punktes auf die Karte zu übertragen? Klar, aber die Eigenschaften von a Pointgehören zusammen, und Sie können alle Arten von Methoden verwenden, die bei ihrer Konstruktion helfen:

1 Der Unterschied besteht darin, dass der Builder nicht nur ein einfaches Datenobjekt ist, sondern diese verketteten Setter-Aufrufe zulässt. Das Builderist meistens damit beschäftigt, wie man sich selbst baut.

Jetzt fügen wir ein bisschen hinzu der Mischung Befehlsmuster hinzu , denn wenn Sie genau hinschauen, ist der Builder tatsächlich ein Befehl:

  1. Es kapselt eine Aktion: Aufrufen einer Methode einer anderen Klasse
  2. Es enthält die erforderlichen Informationen zum Ausführen dieser Aktion: die Werte für die Parameter der Methode

Das Besondere für den Bauherrn ist, dass:

  1. Diese aufzurufende Methode ist eigentlich der Konstruktor einer Klasse. Nennen Sie es besser a jetzt Schöpfungsmuster .
  2. Der Wert für den Parameter ist der Builder selbst

Was an den PersonKonstruktor übergeben wird, kann ihn erstellen, muss es aber nicht. Es kann sich um ein einfaches altes Konfigurationsobjekt oder einen Builder handeln.

Null
quelle
0

Nein, sie liegen völlig falsch und Sie haben absolut Recht. Dieses Baumodell ist einfach albern. Es geht die Person nichts an, woher die Konstruktorargumente kommen. Der Builder ist nur eine Annehmlichkeit für Benutzer und nichts weiter.

DeadMG
quelle
4
Danke ... Ich bin mir sicher, dass diese Antwort mehr Stimmen sammeln würde, wenn sie ein bisschen erweitert würde. Es sieht aus wie eine unvollendete Antwort =)
Mathieu Guindon
Ja, ich würde +1 geben, eine alternative Herangehensweise an den Bauherrn, die die gleichen Sicherheiten und Garantien ohne die Kupplung bietet
Matt Freak
3
Einen Gegenpol dazu finden Sie in der akzeptierten Antwort , in der Fälle erwähnt werden, in denen dies PersonBuildertatsächlich ein wesentlicher Bestandteil der PersonAPI ist. Wie es aussieht, argumentiert diese Antwort nicht ihren Fall.
Andres F.