Ist es sinnvoll, Builder und flüssige Schnittstellen mit Objektinitialisierern zu verwenden?

10

In Java und C # können Sie ein Objekt mit Eigenschaften erstellen, die bei der Initialisierung festgelegt werden können, indem Sie entweder einen Konstruktor mit Parametern definieren, jede Eigenschaft nach dem Erstellen des Objekts definieren oder das Schnittstellenmuster Builder / Fluid verwenden. In C # 3 wurden jedoch Objekt- und Sammlungsinitialisierer eingeführt, was bedeutete, dass das Builder-Muster weitgehend nutzlos war. In einer Sprache ohne Initialisierer könnte man einen Builder implementieren und ihn dann folgendermaßen verwenden:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

Umgekehrt könnte man in C # schreiben:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

... ohne Builder, wie im vorherigen Beispiel gezeigt.

Sind Builder nach diesen Beispielen in C # noch nützlich oder wurden sie vollständig von Initialisierern abgelöst?

svbnet
quelle
are builders usefulWarum sehe ich diese Art von Fragen immer wieder? Ein Builder ist ein Entwurfsmuster - sicher, es kann als Sprachmerkmal verfügbar gemacht werden, aber am Ende des Tages ist es nur ein Entwurfsmuster. Ein Entwurfsmuster kann nicht "falsch" sein. Es kann Ihren Anwendungsfall erfüllen oder nicht, aber das macht nicht das gesamte Muster falsch, sondern nur die Anwendung. Denken Sie daran, dass Designmuster am Ende des Tages bestimmte Probleme lösen. Wenn Sie sich dem Problem nicht stellen, warum sollten Sie dann versuchen, es zu lösen?
VLAZ
@vlaz Ich sage nicht, dass Builder falsch sind - tatsächlich war meine Frage, ob es Anwendungsfälle für Builder gibt, da Initialisierer eine Implementierung des häufigsten Falls sind, in dem Sie Builder verwenden würden. Offensichtlich haben die Leute geantwortet, dass ein Builder immer noch nützlich ist, um private Felder festzulegen, was wiederum meine Frage beantwortet hat.
SVBnet
3
@vlaz Zur Verdeutlichung: Ich sage, dass Initialisierer das "traditionelle" Builder / Fluid-Schnittstellenmuster des Schreibens einer inneren Builder-Klasse weitgehend ersetzt haben und dann Verkettungs-Setter-Methoden innerhalb dieser Klasse geschrieben haben, die eine neue Instanz dieser übergeordneten Klasse erstellen. Ich sage nicht , dass Initialisierer ein Ersatz für dieses spezifische Builder-Muster sind. Ich sage , dass initialisers speichern Entwickler mit , dass bestimmte Erbauer zu implementieren, speichern für die Anwendungsfälle in den Antworten erwähnt, die nicht nicht die Erbauer nutzlos machen.
SVBnet
5
@vlaz Ich verstehe dein Anliegen nicht. "Sie sagten, dass ein Entwurfsmuster nicht nützlich ist" - nein, Joe fragte, ob ein Entwurfsmuster nützlich ist. Wie ist das eine schlechte, falsche oder falsche Frage? Denken Sie, dass die Antwort offensichtlich ist? Ich denke nicht, dass die Antwort offensichtlich ist; Das scheint mir eine gute Frage zu sein.
Tanner Swett
2
@vlaz, die Singleton- und Service Locator-Muster sind falsch. Ergo Designmuster können falsch sein.
David Arno

Antworten:

12

Wie von @ user248215 angesprochen, ist das eigentliche Problem die Unveränderlichkeit. Der Grund, warum Sie einen Builder in C # verwenden würden, besteht darin, die Aussagekraft eines Initialisierers beizubehalten, ohne einstellbare Eigenschaften verfügbar machen zu müssen. Es ist keine Frage der Kapselung, weshalb ich meine eigene Antwort geschrieben habe. Die Kapselung ist ziemlich orthogonal, da das Aufrufen eines Setters nicht impliziert, was der Setter tatsächlich tut, oder Sie an seine Implementierung bindet.

In der nächsten Version von C #, 8.0, wird wahrscheinlich ein withSchlüsselwort eingeführt, mit dem unveränderliche Objekte klar und präzise initialisiert werden können, ohne dass Builder geschrieben werden müssen.

Eine andere interessante Sache, die Sie mit Buildern im Gegensatz zu Initialisierern tun können, ist, dass sie abhängig von der Reihenfolge der aufgerufenen Methoden zu unterschiedlichen Objekttypen führen können.

Beispielsweise

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

Um das obige Beispiel zu verdeutlichen, handelt es sich um eine Mustervergleichsbibliothek, die das Builder-Muster verwendet, um eine "Übereinstimmung" durch Angabe von Fällen aufzubauen. Fälle werden angehängt, indem die CaseMethode aufgerufen wird, die eine Funktion übergibt. Wenn valuees dem Parametertyp der Funktion zugewiesen werden kann, wird es aufgerufen. Sie finden den vollständigen Quellcode auf GitHub . Da XML-Kommentare im Klartext schwer zu lesen sind, finden Sie hier einen Link zur von SandCastle erstellten Dokumentation (siehe Abschnitt " Hinweise ").

Aluan Haddad
quelle
Ich sehe nicht, wie dies mit einem Objektinitialisierer unter Verwendung eines IEnumerable<Func<T, TResult>>Mitglieds nicht möglich war.
Caleth
@Caleth Das war eigentlich etwas, mit dem ich experimentiert habe, aber es gibt einige Probleme mit diesem Ansatz. Es erlaubt keine Bedingungsklauseln (nicht gezeigt, aber in der verknüpften Dokumentation verwendet und demonstriert) und es erlaubt keine Typinferenz von TResult. Letztendlich hatte Typinferenz viel damit zu tun. Ich wollte auch, dass es wie eine Kontrollstruktur "aussieht". Außerdem wollte ich keinen Initialisierer verwenden, da dies eine Mutation ermöglichen würde.
Aluan Haddad
12

Objektinitialisierer erfordern, dass der aufrufende Code auf die Eigenschaften zugreifen kann. Verschachtelte Builder können auf private Mitglieder der Klasse zugreifen.

Wenn Sie Vehicleunveränderlich machen möchten (indem Sie alle Setter privat machen), können Sie die privaten Variablen mit dem verschachtelten Builder festlegen.

user248215
quelle
0

Sie alle dienen unterschiedlichen Zwecken !!!

Konstruktoren können markierte Felder readonlysowie private und geschützte Mitglieder initialisieren . Sie sind jedoch etwas eingeschränkt in dem, was Sie in einem Konstruktor tun können. Sie sollten beispielsweise vermeiden, thisan externe Methoden zu übergeben, und Sie sollten vermeiden, virtuelle Mitglieder aufzurufen, da diese möglicherweise im Kontext einer abgeleiteten Klasse ausgeführt werden, die sich noch nicht selbst erstellt hat. Außerdem wird die Ausführung von Konstruktoren garantiert (es sei denn, der Aufrufer führt etwas sehr Ungewöhnliches aus). Wenn Sie also Felder im Konstruktor festlegen, kann der Code in der restlichen Klasse davon ausgehen, dass diese Felder nicht null sind.

Initialisierer laufen nach dem Bau. Sie können nur öffentliche Objekte aufrufen. Private und / oder schreibgeschützte Felder können nicht festgelegt werden. Konventionell sollte das Festlegen einer Eigenschaft ziemlich begrenzt sein, z. B. sollte es idempotent sein und begrenzte Nebenwirkungen haben.

Builder- Methoden sind tatsächliche Methoden und erlauben daher mehr als ein Argument. Sie können konventionell Nebenwirkungen haben, einschließlich der Objekterstellung. Während sie keine schreibgeschützten Felder festlegen können, können sie so ziemlich alles andere tun. Methoden können auch als Erweiterungsmethoden implementiert werden (wie so ziemlich die gesamte LINQ-Funktionalität).

John Wu
quelle