Method Chaining / Fluent Interfaces

    • Allgemein

    Es gibt 14 Antworten in diesem Thema. Der letzte Beitrag () ist von nikeee13.

      Method Chaining / Fluent Interfaces

      Hi,

      ich arbeite mal wieder an nem Projekt bei dem mir aufgefallen ist, dass ein relativ nützlicher Tipp hier meines Wissens nach noch nie gefallen ist: Method Chaining.
      An dieser Stelle würde ich gerne mal Wikipedia zitieren:

      Fluent Interfaces [...] sind ein Konzept für Programmierschnittstellen in der Software-Entwicklung, bei dessen Befolgung man beinahe in Form von Sätzen natürlicher Sprache programmieren kann.

      Heißt: Alle Operationen einer Klasse können aneinandergehängt (-> "gechained") werden.
      Nehmen wir als Beispiel eine Klasse User. Ein User hat Name, Wohnort und Alter. Dazu implementiere ich hier noch eine Funktion, die alles kombiniert.


      1. Normale Klasse "User"

      C#-Quellcode

      1. public class NormalUser
      2. {
      3. public string Name { get; set; }
      4. public string Residence { get; set; }
      5. public int Age { get; set; }
      6. public void GetInformation()
      7. {
      8. Console.WriteLine(String.Format("{0} is {1} years old and currently living in {2}.", this.Name, this.Age.ToString(), this.Residence));
      9. }
      10. }

      Sieht gut aus. Eine Instanz dieser Funktion kann erzeugt werden und nachdem die entsprechenden Eigenschaften gesetzt sind können Informationen angezeigt werden.

      C#-Quellcode

      1. static void Main(string[] args)
      2. {
      3. NormalUser User = new NormalUser();
      4. User.Name = "Nikx";
      5. User.Residence = "Germany";
      6. User.Age = 18;
      7. User.GetInformation();
      8. Console.ReadLine();
      9. }

      Trotzdem ist es doch ziemlich wenig Information auf ziemlich vielen Zeilen, die alle mit User. beginnen. Und hier kommen wir wieder zum Wikipedia-Artikel, der Method Chaining als "Programmieren in Sätzen" beschreibt. Was wäre, wenn man einfach direkt mit dem Objekt weiterarbeiten könnte, nachdem man die Property gesetzt hat? Keine schlechte Idee. Aber mit Propertys ist das so leider nicht machbar. Also implementieren wir unsere Propertys als Funktionen mit Rückgabetyp der eigenen Klasse und geben dann this zurück.


      2. Chainable Klasse "User"

      C#-Quellcode

      1. public class ChainUser
      2. {
      3. private string _name;
      4. public ChainUser Name(string name)
      5. {
      6. this._name = name;
      7. return this;
      8. }
      9. private string _residence;
      10. public ChainUser Residence(string residence)
      11. {
      12. this._residence = residence;
      13. return this;
      14. }
      15. private int _age;
      16. public ChainUser Age(int age)
      17. {
      18. this._age = age;
      19. return this;
      20. }
      21. public ChainUser GetInformation()
      22. {
      23. Console.WriteLine(String.Format("{0} is {1} years old and currently living in {2}.", this._name, this._age.ToString(), this._residence));
      24. return this;
      25. }
      26. }

      Die Funktionen sind nun chainable, was im Endcode eine starke Vereinfachung zur Folge hat:

      C#-Quellcode

      1. static void Main(string[] args)
      2. {
      3. ChainUser User = new ChainUser();
      4. User.Name("Nikx").Residence("Germany").Age(18).GetInformation();
      5. Console.ReadLine();
      6. }

      Es gibt ebenfalls die Möglichkeit, die normale Userklasse als internen Datenspeicher für die Chainables zu verwenden, was den Code für die Chainable-Klasse nochmal vereinfachen würde. Bei diesem kleinen Beispiel wirkt das Prinzip erstmal nicht sehr gut, es kann bei größeren Projekten aber extrem nützlich werden.

      Wers auf die Spitze treiben will, muss nicht mal eine Variable für die Instanz erstellen, sondern kann direkt mit new ClassName().Chain1().Chain2() beginnen.


      3. Instanzierung als Teil der Chain

      @'SplittyDev' hat noch nen schönen Beitrag zu dem Thema. Hat man eine Klasse mit der man arbeitet ist es möglicherweise schöner, die Instanzierung als Teil der Chain vorzunehmen. Für diesen Fall hat er ne schöne Klasse namens IGrabbable bereitgestellt:

      C#-Quellcode

      1. public class IGrabbable<T> where T : new()
      2. {
      3. public static T GrabNew()
      4. {
      5. return new T();
      6. }
      7. }

      Lässt man andere Klassen davon erben, kann man .GrabNew() an den Anfang setzen und erhält damit eine Chain, die so aufgebaut ist:

      C#-Quellcode

      1. ClassName.GrabNew().Chain1().Chain2()

      Das scheint im ersten Moment unnötig, macht bei längeren Chains aber Sinn.


      Beispielprojekt befindet sich im Anhang.
      Habe dort auch gerade noch eine dritte Klasse reingehauen, bei der mit einer normalen Userklasse als Speicher für die Daten gearbeitet wird und mit der
      man auch Werte abrufen kann.

      Grüße
      Dateien
      "Life isn't about winning the race. Life is about finishing the race and how many people we can help finish the race." ~Marc Mero

      Nun bin ich also auch soweit: Keine VB-Fragen per PM! Es gibt hier ein Forum, verdammt!

      Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „Nikx“ ()

      Anmerkung: ein praktisches Beispiel ist auch in den Erläuterungen des 4. Listings von DBExtensions erläutert.
      (Allerdings wusste ich nicht den richtigen Begriff dafür ;) )

      Man kann den Ansatz auch noch extremer treiben, dass man ein Fluent-Interface wie als Property auffasst. Also zusammengehörige Methoden können denselben Namen haben, anhand der Signaturen unterscheiden sich Setter und Getter (wie bei normalen Properties auch):

      C#-Quellcode

      1. public string Residence() { return _user.Residence; } //pseudo-getter
      2. public ChainUserExt Residence(string residence) { //pseudo-setter
      3. _user.Residence = residence;
      4. return this;
      5. }
      6. public string Name() { return _user.Name; } //pseudo-getter
      7. public ChainUserExt Name(string name) { //pseudo-setter
      8. _user.Name = name;
      9. return this;
      10. }
      11. public ChainUserExt NameAndAge(string name,int age) { //pseudo-multi-setter
      12. _user.Name = name;
      13. _user.Age = age;
      14. return this;
      15. }
      Beachte die letzte Methode, ein "multi-setter". Damit geht eine Fluent-Interface-Pseudo-Property nämlich ein Stück über das hinaus, was mit konventionellen Properties möglich ist.

      Edit: Auch ein Fluent-Konstruktor ist möglich, als statische Methode, und sähe so aus:

      C#-Quellcode

      1. public static ChainUserExt Create(string name) {
      2. var u = new ChainUserExt();
      3. u._user.Name = name;
      4. return u;
      5. }
      (Es beschleicht mich das ketzerische Gefühl, die MS-Sprach-Architekten hätten vlt. besser ganz auf das Sprach-Element "Property" verzichtet, und stattdessen eine schöne Fluent-Interface-Unterstützung konzipiert :evil: )
      jdfs hier nochmal die Variante incl. Fluent-Konstruktor, Multi-Setter und TestAufruf:
      Spoiler anzeigen

      C#-Quellcode

      1. class ChainUserExt {
      2. private NormalUser _user = new NormalUser();
      3. public string Name() { return _user.Name; } //pseudo-getter
      4. public ChainUserExt Name(string name) { //pseudo-setter
      5. _user.Name = name;
      6. return this;
      7. }
      8. public string Residence() { return _user.Residence; } //pseudo-getter
      9. public ChainUserExt Residence(string residence) { //pseudo-setter
      10. _user.Residence = residence;
      11. return this;
      12. }
      13. public ChainUserExt ResidenceAndAge(string residence, int age) { //pseudo-multi-setter
      14. return Residence(residence).Age(age);
      15. }
      16. public int Age() { return _user.Age; }
      17. public ChainUserExt Age(int age) {
      18. _user.Age = age;
      19. return this;
      20. }
      21. public static ChainUserExt Create(string name) { //pseudo-Konstruktor
      22. return new ChainUserExt().Name(name);
      23. }
      24. public string GetInformation() {
      25. return String.Format("{0} is {1} years old and currently living in {2}.", Name(), Age(), Residence());
      26. }
      27. public static void TestCall() {
      28. var ux=ChainUserExt.Create("EDR").ResidenceAndAge("Erde", 66);
      29. Console.WriteLine(ux.GetInformation());
      30. }
      31. }

      Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Das kann oft extrem praktisch sein. Aus C++ kennt man bspw. bei Streams, die Bitshift-Operatoren. "<<" schreibt in den Stream ">>" liest aus dem Stream. Die 2 Operatoren sind stumpf überladen und geben eine Referenz des Streams zurück, so lässt sich munter weiter schreiben. stream << "Hallo" << " hallo" << " hallo" << endl;. Ich glaube Operatorüberladungen waren in .Net aber nur sehr beschränkt möglich, deswegen dürfte sowas wohl nicht gehen. :/

      ErfinderDesRades schrieb:


      Edit: Auch ein Fluent-Konstruktor ist möglich, als statische Methode, und sähe so aus: [...]


      Der Ansatz ist gut, das lässt sich jedoch meiner Meinung nach besser als generic lösen:

      C#-Quellcode

      1. public class Chainable<T> where T : new() {
      2. public static T Create () {
      3. return new T ();
      4. }
      5. }


      Man müsste dann nur von Chainable erben:

      C#-Quellcode

      1. class MyChainable : Chainable<MyChainable> { }
      Hi
      das Prinzip ist zwar teilweise elegant, aber, wenn man's überstark benutzt, einfach nur hässlich.
      Ich würde es tatsächlich nur dann verwenden, wenn bspw. ein Fluss beschrieben werden soll (siehe System.Text.StringBuilder). Code soll nicht in Prosa dastehen, sondern logisch korrekt und nachvollziehbar sein.

      Viele Grüße
      ~blaze~

      Nikx schrieb:

      C#-Quellcode

      1. public void GetInformation(){ ... }
      gettet ja keine Information, besser wäre

      C#-Quellcode

      1. public override string ToString()
      2. {
      3. return string.Format("{0} is {1} years old and currently living in {2}.", this.Name, this.Age, this.Residence);
      4. }
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!

      ~blaze~ schrieb:

      Hi
      das Prinzip ist zwar teilweise elegant, aber, wenn man's überstark benutzt, einfach nur hässlich.


      Ist denke ich Ansichtssache.

      Ich persönlich denke, dass es einige Abläufe stark erleichtert. Ich habe zB vor wenigen Tagen einen Compiler+Interpreter+Runtime für eine fiktive Sprache geschrieben und dort fand ich es sehr angebracht, solche fluid methods zu nutzen.

      Am Ende muss man für sich entscheiden, ob es einem gefällt und sich einfach integrieren lässt.

      Der Code bleibt ja im Endeffekt für jeden Entwickler der sich den Source ansieht lesbar.
      Kommt sich meiner Meinung nach auch drauf an für was. Würde niemals alles über Fluent basierende APIs machen. Sehe z.B. keinen Grund alle Properties etc. mit entsprechenden Methoden abzubilden. Teilweise macht das aber sehr wohl Sinn. .NET verwendet ja (gewollt oder ungewollt) ähnliches bei z.B. den string methoden. Dort kann ich auch schreiben "abc".Replace(...).Split(...).
      Ebenfalls verwende ich das z.B. bei CSCore um Filterketten aufzubauen. Baue dabei auf generische Erweiterungs-Methoden auf welche zutreffenden interfaces passen. Spart beim verwenden teilweise haufenweise nicht benötigter variabeln, verbessert erheblich die Lesbarkeit und den Wartungsaufwand. Ist halt meiner Meinung immer eine Frage wo man das einsetzt.


      Opensource Audio-Bibliothek auf github: KLICK, im Showroom oder auf NuGet.
      Bei string-Methoden handelt es sich um Funktionen, bei denen jeweils neue Instanzen generiert werden. Das hat mit Method Chaining an sich wenig zu tun. Was anderes ist es beim StringBuilder. Dort wird immer this zurückgegeben.
      Für die Verkettung bzw. zur Signalisierung eines Flusses ist es durchaus elegant.

      Viele Grüße
      ~blaze~
      Das hatte ich befürchtet (deshalb auch das "gewollt oder ungewollt" ;) ). Trotzdem merkt der Anwender keinen Unterschied. Wobei der StringBuilder als Referenztyp natürlich das wesentlich bessere Beispiel gewesen wäre ....


      Opensource Audio-Bibliothek auf github: KLICK, im Showroom oder auf NuGet.
      Um nochmal den Sinn zu erläutern hab ich heute schnell eine GraphicChain-Klasse geschrieben, die Bildbearbeitung als Chain ermöglicht.
      Dabei hab ich aus Langeweile fast alle Funktionen aus e.Graphics.* implementiert (ist ja auch kein Aufwand).
      Dies ermöglicht, ein Bild in einer Chain übersichtlich und als "Einzeiler" beliebig lang zu modifizieren.

      Grüße
      Bilder
      • graphicchain.png

        23,29 kB, 950×468, 170 mal angesehen
      "Life isn't about winning the race. Life is about finishing the race and how many people we can help finish the race." ~Marc Mero

      Nun bin ich also auch soweit: Keine VB-Fragen per PM! Es gibt hier ein Forum, verdammt!
      Du entfernst dich mit dieser Notation halt vom in .Net allgemein geltenden Prinzip, Code möglichst nahe an der englischen Sprache zu formulieren. Vor allem fehlen in diesen Konstrukten einige Verben, was dem Code einen deklarativen Charakter verleiht im Gegensatz zum üblichen imperativen.

      Die etwas längere Variante halte ich daher für intuitiver.

      Nikx schrieb:

      Die Funktionen sind nun chainable, was im Endcode eine starke Vereinfachung zur Folge hat:

      C#-Quellcode

      1. static void Main(string[] args)
      2. {
      3. ChainUser User = new ChainUser();
      4. User.Name("Nikx").Residence("Germany").Age(18).GetInformation();
      5. Console.ReadLine();
      6. }

      Wenn es nur um das setzen von Propertys geht, sehe ich gegenüber einem normalen Initializer-Block keinen wirklichen Vorteil:

      C#-Quellcode

      1. NormalUser user = new NormalUser {
      2. Name = "Nikx",
      3. Residence = "Germany",
      4. Age = 18
      5. };
      6. user.GetInformation();


      RodFromGermany schrieb:

      gettet ja keine Information, besser wäre

      C#-Quellcode

      1. public override string ToString()
      2. {
      3. return string.Format("{0} is {1} years old and currently living in {2}.", this.Name, this.Age, this.Residence);
      4. }

      Wer schon VS 2015 hat, kann das hier auch noch etwas "kürzer" mit C# 6 formulieren:

      C#-Quellcode

      1. public override string ToString() => $"{Name} is {Age} years old and currently living in {Residence}.";

      Ich bin übrigens der Ansicht, dass ein ToString mit so einer Rückgabe nicht angebracht ist. Dafür würde ich eine extra Methode schreiben.
      Von meinem iPhone gesendet

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nikeee13“ ()

      @nikeee13 Es geht aber nicht nur um das Setzen von Propertys. Ich war eigentlich der Meinung, dass "GetInformation()" dafür ausreicht.
      ToString zu überschreiben ist normalerweise eine Möglichkeit, hier allerdings keine, weil es ja Teil der Chain sein soll. Außerdem halte ich eine
      solche Ausgabe bei ToString für ungeeignet, aber das ist Geschmackssache.

      Zu C# 6 - Lust, den Code schnell zu erklären?

      Grüße
      "Life isn't about winning the race. Life is about finishing the race and how many people we can help finish the race." ~Marc Mero

      Nun bin ich also auch soweit: Keine VB-Fragen per PM! Es gibt hier ein Forum, verdammt!

      Nikx schrieb:

      Zu C# 6 - Lust, den Code schnell zu erklären?
      Sicher:
      Für Methoden, die nur aus einer Expression bestehen, lässt sich die Schreibweise wähnlich zu Lambdas abkürzen:

      C#-Quellcode

      1. public int Foo()
      2. {
      3. return bar;
      4. }
      5. // Auch zu schreiben als:
      6. public int Foo() => bar;


      Das geht auch bei Getter-Only-Properys, bei denen ein Wert berechnet werden muss:

      C#-Quellcode

      1. public string Name { get { return First + " " + Last; }}
      2. // auch zu schreiben als:
      3. public string Name => First + " " + Last;


      Für Strings gibt es eine neuen Literal-Prefix, das $ (angelehnt an das @). Das ist einfach nur eine schönere Schreibweise für string.Format.

      C#-Quellcode

      1. var a = string.Format("{0} is {1} years old and currently living in {2}.", <ausdruck-0>, <ausdruck-1>, <ausdruck-2>);
      2. // kann man schreiben als:
      3. var a = $"{<ausdruck-0>} is {<ausdruck-1>} years old and currently living in {<ausdruck-2>}."


      C# 6 hat noch mehr Zcuker zu bieten. Einfach mal hier vorbeischauen:
      github.com/dotnet/roslyn/wiki/…nguage-Features-in-C%23-6
      Von meinem iPhone gesendet