Wann kann man C # fließend sprechen?

78

In vielerlei Hinsicht mag ich die Idee von Fluent-Schnittstellen sehr, aber mit all den modernen Funktionen von C # (Initialisierer, Lambdas, benannte Parameter) denke ich: "Lohnt es sich?" Und "Ist dies das richtige Muster für" verwenden?". Könnte mir jemand, wenn nicht eine akzeptierte Praxis, zumindest seine eigene Erfahrung oder Entscheidungsgrundlage für die Verwendung des Fließenden Musters geben?

Fazit:

Einige gute Faustregeln aus den bisherigen Antworten:

  • Fließende Interfaces helfen sehr, wenn Sie mehr Aktionen als Setter haben, da Anrufe mehr vom Kontextdurchgang profitieren.
  • Fließende Interfaces sollten als eine Schicht über einer API betrachtet werden, nicht als das einzige Mittel zur Verwendung.
  • Die modernen Funktionen wie Lambdas, Initialisierer und benannte Parameter können Hand in Hand arbeiten, um eine flüssige Benutzeroberfläche noch benutzerfreundlicher zu gestalten.

Hier ist ein Beispiel dafür, was ich mit den modernen Funktionen meine, die dafür sorgen, dass sie sich weniger gebraucht anfühlen. Nehmen Sie zum Beispiel ein (vielleicht schlechtes Beispiel) Fluent-Interface, mit dem ich einen Mitarbeiter wie folgt erstellen kann:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Könnte leicht mit Initialisierern geschrieben werden wie:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

In diesem Beispiel hätte ich auch benannte Parameter in den Konstruktoren verwenden können.

Andrew Hanlon
quelle
1
Gute Frage, aber ich denke, es ist eher eine Wiki-Frage
Ivo
Ihre Frage ist getaggt mit "fließend". Versuchen Sie also zu entscheiden, ob Sie eine flüssige Benutzeroberfläche erstellen oder ob Sie eine flüssige Konfiguration von nhibernate vs. XML verwenden möchten?
Ilya Kogan
1
Gewählt, um zu Programmers.SE
Matt Ellen
@Ilya Kogan, ich denke, es ist tatsächlich als "Fluent-Interface" gekennzeichnet, was ein generisches Tag für das Fluent-Interface-Muster ist. Diese Frage bezieht sich nicht auf nhibernate, sondern, wie Sie sagten, nur darauf, ob eine flüssige Benutzeroberfläche erstellt werden soll. Vielen Dank.
1
Dieser Beitrag hat mich inspiriert , eine Art und Weise zu denken , dieses Muster zu verwenden , bei C. Mein Versuch , kann an der gefunden wird Code Review Schwester - Site .
Otto

Antworten:

28

Schreibe eine fließend Schnittstelle (ich habe dabbled mit ihm) braucht mehr Mühe, aber es hat einen Pay-off, denn wenn man es richtig macht, die Absicht des resultierenden Benutzercode mehr offensichtlich. Es ist im Wesentlichen eine Form der domänenspezifischen Sprache.

Mit anderen Worten, wenn Ihr Code viel mehr gelesen wird als geschrieben (und welcher Code nicht?), Sollten Sie in Betracht ziehen, eine flüssige Benutzeroberfläche zu erstellen.

Fließende Interfaces handeln mehr vom Kontext und sind so viel mehr als nur Möglichkeiten, Objekte zu konfigurieren. Wie Sie im obigen Link sehen können, habe ich eine flüssige API verwendet, um Folgendes zu erreichen:

  1. Kontext (wenn Sie also in der Regel viele Aktionen in einer Sequenz mit der gleichen Sache ausführen, können Sie die Aktionen verketten, ohne Ihren Kontext immer wieder neu deklarieren zu müssen).
  2. objectA.Erkennbarkeit (wenn Sie dann zu Intellisense gehen, erhalten Sie viele Hinweise. In meinem obigen Fall erhalten plm.Led.Sie alle Optionen für die Steuerung der eingebauten LED und erhalten plm.Network.Informationen zu den Funktionen, die Sie mit der Netzwerkschnittstelle ausführen können. plm.Network.X10.Sie erhalten eine Teilmenge von Netzwerkaktionen für X10-Geräte: Dies erhalten Sie nicht mit Konstruktorinitialisierern (es sei denn, Sie möchten für jeden Aktionstyp ein Objekt erstellen, was nicht idiomatisch ist).
  3. Reflection (im obigen Beispiel nicht verwendet) - Die Fähigkeit, einen übergebenen LINQ-Ausdruck zu übernehmen und zu bearbeiten, ist ein sehr leistungsfähiges Werkzeug, insbesondere in einigen Hilfs-APIs, die ich für Komponententests erstellt habe. Ich kann einen Property-Getter-Ausdruck übergeben, eine ganze Reihe nützlicher Ausdrücke erstellen, kompilieren und ausführen oder sogar den Property-Getter verwenden, um meinen Kontext einzurichten.

Eine Sache, die ich normalerweise mache, ist:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

Ich verstehe nicht, wie man so etwas auch ohne eine flüssige Benutzeroberfläche machen kann.

Edit 2 : Sie können auch sehr interessante Verbesserungen der Lesbarkeit vornehmen, wie zum Beispiel:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
Scott Whitlock
quelle
Vielen Dank für die Antwort, mir ist jedoch der Grund für die Verwendung von Fluent bekannt, ich suche jedoch nach einem konkreteren Grund für die Verwendung über so etwas wie mein neues Beispiel oben.
Andrew Hanlon
Danke für die ausführliche Antwort. Ich denke, Sie haben zwei gute Faustregeln aufgestellt: 1) Verwenden Sie Fluent, wenn Sie viele Anrufe haben, die vom "Durchreichen" des Kontexts profitieren. 2) Denken Sie an Fluent, wenn Sie mehr Anrufe als Setter haben.
Andrew Hanlon
2
@ach, ich sehe in dieser Antwort nichts über "mehr Anrufe als Setter". Sind Sie verwirrt von seiner Aussage über "Code [der] wird viel mehr gelesen als geschrieben"? Das ist nicht etwa Eigenschaft Getter / Setter, es geht um den Menschen den Code gegen Menschen Lesen des Codes zu schreiben - über die Herstellung der Code einfach für Menschen zu lesen, weil wir in der Regel einen bestimmten Codezeile lesen viel häufiger als wir es ändern.
Joe White
@ Joe White, ich sollte vielleicht meinen Begriff "Handlungsaufforderung" umformulieren. Die Idee bleibt aber bestehen. Vielen Dank.
Andrew Hanlon
Reflexion zum Testen ist böse!
Adronius
24

Scott Hanselman spricht darüber in Episode 260 seines Podcasts Hanselminutes mit Jonathan Carter. Sie erklären, dass eine flüssige Benutzeroberfläche eher einer Benutzeroberfläche auf einer API gleicht. Sie sollten kein flüssiges Interface als einzigen Zugangspunkt bereitstellen, sondern es als eine Art Code-UI über das "reguläre API-Interface" stellen.

Jonathan Carter spricht in seinem Blog auch ein wenig über API-Design .

Kristof Claes
quelle
Vielen Dank für die Info-Links und die Benutzeroberfläche oben auf der API ist eine schöne Sichtweise.
Andrew Hanlon
14

Fließende Schnittstellen sind sehr leistungsstarke Funktionen, die Sie im Kontext Ihres Codes bereitstellen können, wenn Sie die "richtige" Argumentation verwenden.

Wenn Sie einfach massive einzeilige Code-Ketten als eine Art Pseudo-Black-Box erstellen möchten, bellen Sie wahrscheinlich den falschen Baum an. Wenn Sie es andererseits verwenden, um der API-Schnittstelle einen Mehrwert zu verleihen, indem Sie Methodenaufrufe verketten und die Lesbarkeit des Codes verbessern, dann lohnt sich der Aufwand meiner Meinung nach mit viel Planung und Aufwand.

Ich würde vermeiden, dem zu folgen, was bei der Erstellung von fließenden Schnittstellen zu einem gängigen "Muster" zu werden scheint, bei dem Sie alle Ihre fließenden Methoden mit "-etwas" benennen, da es einer potenziell guten API-Schnittstelle den Kontext und damit den inneren Wert nimmt .

Der Schlüssel ist, sich die flüssige Syntax als eine spezifische Implementierung einer domänenspezifischen Sprache vorzustellen. Als ein wirklich gutes Beispiel für das, wovon ich spreche, werfen Sie einen Blick auf StoryQ, mit dessen Hilfe sich fließend eine DSL auf sehr wertvolle und flexible Weise ausdrücken lässt.

S.Robins
quelle
Vielen Dank für die Antwort, für eine durchdachte Antwort ist es nie zu spät.
Andrew Hanlon
Ich habe nichts gegen das "mit" -Präfix für Methoden. Es unterscheidet sie von anderen Methoden, die kein Objekt zum Verketten zurückgeben. ZB position.withX(5)gegenposition.getDistanceToOrigin()
LegendLength
5

Vorbemerkung: Ich gehe von einer Annahme in der Frage aus und ziehe daraus (am Ende dieses Beitrags) meine spezifischen Schlussfolgerungen. Da dies wahrscheinlich keine vollständige und umfassende Antwort ergibt, bezeichne ich dies als CW.

Employees.CreateNew().WithFirstName("Peter")…

Könnte leicht mit Initialisierern geschrieben werden wie:

Employees.Add(new Employee() { FirstName = "Peter",  });

In meinen Augen sollten diese beiden Versionen verschiedene Dinge bedeuten und tun.

  • Im Gegensatz zur nicht fließenden Version verbirgt die fließende Version die Tatsache, dass das Neue Employeeebenfalls Addin die Sammlung aufgenommen wird Employees- dies deutet lediglich darauf hin, dass ein neues Objekt Created ist.

  • Die Bedeutung von ….WithX(…)ist mehrdeutig, insbesondere für Personen, die aus F # kommen, das ein withSchlüsselwort für Objektausdrücke hat : Sie können obj.WithX(x)als ein neues Objekt interpretiert werden, von objdem objabgesehen von seiner XEigenschaft, deren Wert gleich ist x. Andererseits ist bei der zweiten Version klar, dass keine abgeleiteten Objekte erstellt werden und dass alle Eigenschaften für das ursprüngliche Objekt angegeben sind.

….WithManager().With
  • Dies ….With…hat noch eine andere Bedeutung: das Umschalten des "Fokus" der Eigenschaftsinitialisierung auf ein anderes Objekt. Die Tatsache, dass Ihre fließende API zwei unterschiedliche Bedeutungen Withhat, erschwert die korrekte Interpretation des Geschehens. Vielleicht haben Sie in Ihrem Beispiel deshalb Einrückungen verwendet, um die beabsichtigte Bedeutung dieses Codes zu demonstrieren. Es wäre klarer wie folgt:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 

Schlussfolgerungen: Das "Ausblenden" einer ausreichend einfachen Sprachfunktion new T { X = x }mit einer fließenden API ( Ts.CreateNew().WithX(x)) ist natürlich möglich, aber:

  1. Es muss darauf geachtet werden, dass die Leser des resultierenden fließenden Codes immer noch verstehen, was es genau tut. Das heißt, die flüssige API sollte eine transparente und eindeutige Bedeutung haben. Das Entwerfen einer solchen API ist möglicherweise aufwändiger als erwartet (es muss möglicherweise auf Benutzerfreundlichkeit und Akzeptanz getestet werden) und / oder

  2. Das Entwerfen ist möglicherweise aufwändiger als erforderlich: In diesem Beispiel bietet die fließende API nur einen geringen "Benutzerkomfort" gegenüber der zugrunde liegenden API (einer Sprachfunktion). Man könnte sagen, dass eine flüssige API die zugrunde liegende API / Sprachfunktion "benutzerfreundlicher" machen sollte. das heißt, es sollte dem Programmierer einen beträchtlichen Aufwand ersparen. Wenn es nur eine andere Art ist, dasselbe zu schreiben, lohnt es sich wahrscheinlich nicht, da dies das Leben des Programmierers nicht erleichtert, sondern nur die Arbeit des Designers erschwert (siehe Fazit 1 rechts oben).

  3. In beiden obigen Punkten wird stillschweigend davon ausgegangen, dass die flüssige API eine Schicht über einer vorhandenen API oder einem vorhandenen Sprachfeature ist. Diese Annahme könnte eine weitere gute Richtlinie sein: Eine flüssige API kann eine zusätzliche Möglichkeit sein, etwas zu tun, nicht die einzige. Das heißt, es könnte eine gute Idee sein, eine flüssige API als Option anzubieten.

stakx
quelle
1
Vielen Dank, dass Sie sich die Zeit genommen haben, meine Frage zu ergänzen. Ich gebe zu, dass mein gewähltes Beispiel schlecht durchdacht war. Zu der Zeit wollte ich eigentlich eine flüssige Schnittstelle für eine von mir entwickelte Abfrage-API verwenden. Ich habe zu stark vereinfacht. Vielen Dank für den Hinweis auf die Fehler und für die guten Schlussfolgerungen.
Andrew Hanlon
2

Ich mag den fließenden Stil, er drückt Absichten sehr deutlich aus. Mit dem Objektinitalisierer-Beispiel, das Sie nachher haben, müssen Sie Setter für öffentliche Eigenschaften haben, um diese Syntax zu verwenden, und nicht mit dem fließenden Stil. Wenn Sie das sagen, mit Ihrem Beispiel gewinnen Sie nicht viel gegenüber den öffentlichen Setzern, weil Sie sich fast für eine java-artige Methode entschieden haben.

Was mich zum zweiten Punkt bringt, ich bin mir nicht sicher, ob ich den fließenden Stil so verwenden würde, wie Sie es getan haben, mit vielen Eigenschaftssetzern. Ich würde wahrscheinlich die zweite Version dafür verwenden. Ich finde es besser, wenn Sie Sie müssen viele Verben miteinander verketten oder zumindest viele Dinge tun, anstatt Einstellungen vorzunehmen.

Ian
quelle
Vielen Dank für Ihre Antwort, ich glaube, Sie haben eine gute Faustregel formuliert: Fließend ist besser mit vielen Anrufen über viele Setter.
Andrew Hanlon
1

Ich kannte den Begriff der fließenden Benutzeroberfläche nicht , aber er erinnert mich an einige APIs, die ich verwendet habe, einschließlich LINQ .

Persönlich sehe ich nicht, wie moderne Funktionen von C # den Nutzen eines solchen Ansatzes verhindern würden. Ich würde eher sagen, dass sie Hand in Hand gehen. ZB ist es noch einfacher, eine solche Schnittstelle mit Hilfe von Erweiterungsmethoden zu erreichen .

Vielleicht verdeutlichen Sie Ihre Antwort mit einem konkreten Beispiel, wie eine flüssige Benutzeroberfläche durch eine der von Ihnen erwähnten modernen Funktionen ersetzt werden kann.

Steven Jeuris
quelle
1
Vielen Dank für die Antwort. Ich habe ein einfaches Beispiel hinzugefügt, um meine Frage zu klären.
Andrew Hanlon