Laden...

Fluent interface

Erstellt von markus.bodlos vor 15 Jahren Letzter Beitrag vor 15 Jahren 4.897 Views
M
markus.bodlos Themenstarter:in
205 Beiträge seit 2008
vor 15 Jahren
Fluent interface

Hallo,

bin gerade auf das Pattern Fluent Interface auf Golo Roden's Blog gestoßen (Hier

Das ganze ist ja interessant und mächtig jedoch ist mir die Verwendung des Interfaces nicht ganz klar. Denn wenn meine Klasse mehrere Interfaces implemetiert welches nimm ich dann als Rückgabewert? Meine Frage ist nun wieso wird nicht die Klasse als solche als Rückgabewert benutzt?

Abwandlung von Golo's Beispiel ohne Interface:


    public class Rectangle
    {
        private int _height;
        private int _width;

        public Rectangle SetHeight(int height)
        {
            this._height = height;
            return this;
        }

        public Rectangle SetWidth(int width)
        {
            this._width = width;
            return this;
        }
    }

    public class Program
    {
        public static void Main()
        {
            Rectangle rectangle = new Rectangle().SetHeight(23).SetWidth(42);
        }
    }

mfg Markus

Gelöschter Account
vor 15 Jahren

das geht natürlich auch aber wenn man interfacebasierte architekruen hat, dann ist ein fluent interface manchmal hilfreich.

630 Beiträge seit 2007
vor 15 Jahren

Hallo,

Denn wenn meine Klasse mehrere Interfaces implemetiert welches nimm ich dann als Rückgabewert?

Ich vermute man nimmt das Interface als Rückgabe welches die betroffene Methode definiert.

Meine Frage ist nun wieso wird nicht die Klasse als solche als Rückgabewert benutzt?

Weil eine konkrete Klasse den Code spezifisch macht, während ein Interface universell verwendet werden kann.

Gruss
tscherno

PS: Ausserdem müsste das Pattern dann Fluent Classes heisen 😉

To understand recursion you must first understand recursion

http://www.ilja-neumann.com
C# Gruppe bei last.fm

5.941 Beiträge seit 2005
vor 15 Jahren

Hallo zusammen

Ich kann tscherno zustimmen.

Gruss Peter

--
Microsoft MVP - Visual Developer ASP / ASP.NET, Switzerland 2007 - 2011

M
markus.bodlos Themenstarter:in
205 Beiträge seit 2008
vor 15 Jahren

Danke, mfg

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo markus.bodlos,

Meine Frage ist nun wieso wird nicht die Klasse als solche als Rückgabewert benutzt?

die Methoden von String sind in vielen Fällen fluent, ganz ohne Interface:

str.Substring (5, 20).ToLower ().Replace ('a', 'b'):

Man kann genauso gut eine Klasse fluent machen, wie man ein Interface fluent machen kann, aber man braucht kein Interface, um eine Klasse fluent zu machen.

Wenn man ein Interface fluent macht, werden die Klassen, die es implementieren fluent. Das heißt aber wie gesagt nicht, dass man ein Interface braucht, um eine Klasse fluent zu machen.

Denn wenn meine Klasse mehrere Interfaces implemetiert welches nimm ich dann als Rückgabewert?

Da gibt es keine Regel. fluent heißt ja einfach nur flüssig. Das bedeutet, dass man das Objekt zurückliefert, das der Benutzer der Klasse in dieser Situation am ehesten brauchen wird. Das ist natürlich ein Problem, weil man nur raten kann, was der Benutzer später tatsächlich machen wollen wird.

Zum Beispiel liefert die Methode public virtual ListViewItem Add(string text) aus der Klasse ListViewItemCollection ein ListViewItem zurück und nicht die ListViewItemCollection.

In vielen Fällen wird man aber selbstreferentielle Rückgabewerte verwenden, also einfach return this und vom Typ her wie tscherno sagte, das Interfaces (bzw. wenn es kein Interface gibt, die Klasse), die die Methode definiert.

herbivore

J
57 Beiträge seit 2008
vor 15 Jahren

Das genannte Beispiel kann man mit C# 3 auch per Object Initializers erzeugen:

new Rectangle() { Height = 23, Width = 42 };

Hier wird meiner Meinung nach viel klarer, was passiert. Geht natürlich nur bei der Initialisierung.

Außer zum mehr oder weniger bequemen Erstellen des Objektes sehe ich nicht viel Sinn darin.

Rectangle rectangle = new Rectangle().SetHeight(23).SetWidth(42);

rectangle.SetWidth(rectangle.Width + 1).SetHeight(rectangle.Height + 1);

So etwa? Nein danke. Und was ist mit den Properties die ich verwendet habe? GetXxx? Private Setter?
Das Pattern passt überhaupt nicht zu C#, vielleicht zu Java oder C++.

630 Beiträge seit 2007
vor 15 Jahren

So etwa? Nein danke. Und was ist mit den Properties die ich verwendet habe? GetXxx?

Ja, das ist mir auch sofort ins Auge gesprungen. Deshalb würde ich Fluent interfaces eher sparsam einsetzen da man sonst ins GetXXX/SetXXX-Pattern "zurückfällt".

Gruss
tscherno

To understand recursion you must first understand recursion

http://www.ilja-neumann.com
C# Gruppe bei last.fm

M
markus.bodlos Themenstarter:in
205 Beiträge seit 2008
vor 15 Jahren

Hallo,

@Herivore:
Danke für die ausführliche Ergänzung

@Jabe:
Bei diesem Pattern geht es nicht um eine Initialisierung als solche es war halt einfach nur das Beispiel. Viel mehr geht es um die Weiterreichung eines Objektes von einer Methode zur nächsten um einen Fluss zu erzeugen und eben keine methoden definieren welche keinen Rückgabewert besitzen.
Wie in Herbivors String Code Beispiel gut zu sehen ist würde ohne diesen Pattern der Code mehrere Zeilen benötigen. Es ist halt einfach schöner

mfg Markus

630 Beiträge seit 2007
vor 15 Jahren

Hallo markus.bodlos,

ich würde aber eher ein paar Zeilen mehr schreiben als auf Getter und Setter zu verzichten.

Gruss
tscherno

To understand recursion you must first understand recursion

http://www.ilja-neumann.com
C# Gruppe bei last.fm

M
markus.bodlos Themenstarter:in
205 Beiträge seit 2008
vor 15 Jahren

Du hast natürlich recht wenn es sich um Zugriffe auf Daten handelt ohne jegliche offensichtliche logik. Aber bei sachen wie String Methoden a la replace, trim & co würde es keinen Sinn machen eine Eigenschaft zu verwenden und genau bei solchen dingen findet das pattern anwendung.

mfg Markus

PS: Nochmal die Initialisierung war nur ein Beispiel welches das Prinzip nahelegen sollte und nicht ein praktische Anwendung

C
980 Beiträge seit 2003
vor 15 Jahren

Wenn eine Methode ein Objekt gleichen Typs zurückgibt, dann nehme ich an dass der Methodenaufruf keine Nebenwirkungen hat, also die ursprüngliche Instanz nicht geändert wurde (sondern die Änderungen in der zurückgegebenen Kopie reflektiert werden).

Die string-Methoden geben das Resultat zurück weil String (so-gut-wie) immutable ist, die Methoden haben entsprechend auch keine Nebenwirkungen.

Meiner Meinung nach sollte dieses Fluent-Pattern bei Methoden mit Nebenwirkungen generell vermieden werden, es macht den Code unleserlich und bringt auch kaum Vorteile.

J
57 Beiträge seit 2008
vor 15 Jahren

Ich würde das Pattern nur dort einsetzen, wo es einen logischen Sinn macht. Siehe StringBuilder, dort kann ich Append().Append().Append() machen. Oder aber, wenn die Implementierung explizit darauf abzielt, wie LINQ.

Im Übrigen bin ich kein Fan von Verkettungen, sie führen zu unübersichtlichen Zeilen und sind unhandlich im Debug und Testing.

PS:
Das mit dem Ändern erinnert mich an Ruby. Dort gibt es String-Methoden wie z.B. upcase(), die eine Kopie des Strings liefert und upcase!(), die das aktuelle Objekt ändert. Schöne Konvention. 🙂

M
198 Beiträge seit 2007
vor 15 Jahren

ich hab einfach mal um zu testen wie es ist, ein Fluent Interface gebaut für Menüs in einem Modularen Projekt, das ganze würde dann etwa so ausschauen:


_shell.Menu.AddGroup("Stammdaten")
                     .AddItem("Neuen Kunden anlegen")
                         .Executes(Commands.CreateKunde)
                         .AllowedRole("Administrator")
                     .AddItem("Kundenübersicht anzeigen")
                         .Executes(Commands.ShowKundenÜbersicht)
                     .AddItem("Neuen Artikel anlegen")
                         .AddSeparator
                         .Executes(Commands.CreateArtikel)
                         .AllowedRole("Administrator")
                     .AddItem("Artikelübersicht anzeigen")
                         .Executes(Commands.ShowArtikelÜbersicht)
                     .AddItem("Neuen Lieferanten anlegen")
                         .AddSeparator
                         .Executes(Commands.CreateLieferant)
                         .AllowedRole("Administrator")
                     .AddItem("Lieferantenübersicht anzeigen")
                         .Executes(Commands.ShowLieferantenÜbersicht)
                 .EndGroup();

(EndGroup ist natürlich auf der gleichen höhe wie AddGroup und die restliche Einrückung ist ebenfalls stück weiter links)

Dieser Code befüllt das obere Menü sowie eine Sidebar mit Punkten, entsprechend gibts noch Properties wie ExcludeFromMenu / Sidebar und falls in der Sidebar eine andere Caption verwendet werden soll SideBarCaptionDiffers(string s) und halt noch AddSubGroup() / EndSubGroup() (so oft schachtelbar wie man möchte)

Dazu wollte ich fragen, was ihr generell von solchen Ansätzen haltet Fluent Interfaces zur besseren Leserlichkeit / Wartbarkeit zu verwenden

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo MaXeM,

hm, ich verstehe deinen Code noch nicht ganz. Es wäre hilfreich, wenn du zu jeder Methode angeben würdest, von welcher Klasse sie ist und welchen Rückgabetyp sie hat.

AddGroup wird ja wohl die Gruppe liefern bzw. ein Item, das die Gruppe repräsentirt. AddItem wird dann dieser Gruppe ein Item hinzufügen und sich selbst liefern, do dass Executes und AllowedRole auf diesem Item arbeiten können und es auch wieder liefern. Aber wie passt dann das nächste AddItem in die Reihe. Es bekommt ja dann ein Item und keine Gruppe bzw. ein Item das ein Item in der Gruppe repräsentiert und nicht das Item, dass Gruppe repräsentiert.

Bis auf diese Unklarheit/Schwäche finde ich deine Ansatz sehr gelungen, weil er die Erzeugung eines Menüs in kompakter Weise erlauft.

herbivore

M
198 Beiträge seit 2007
vor 15 Jahren

Diese ganzen Befehle geben ein MenuItem zurück(eigene Klasse, muss ich noch umbennen wegen dem Konflikt mit dem WindowsForms MenuItem), dies liegt daran, dass man auch einer Gruppe ein Execute bzw. ein Allowed Role etc. zuweisen kann (sofern man dies macht bevor man ein SubItem angelegt hat). Die Ausnahme ist EndGroup(), dabei bekommt man wieder das Menu zurück um weitere Hauptmenupunkte anzulegen.
Das Problem dabei ist natürlich das man theoretisch ein .EndSubGroup() direkt nach .AddGroup / AddItem machen könnte ohne eine SubGroup vorher zu eröffnen.

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo MaXeM,

meine Frage sehe ich damit noch nicht wirklich beantwortet.

herbivore

Gelöschter Account
vor 15 Jahren

wir machen sowas mit xml, da man da eindeutig herauslesen kann, was wohin gehört und welche eigenschaften es hat.

aus deinem code z.b. kannn ich nciht die struktur des menus eindeutig entziffern.

hast du einen screen zum code? oder zeig mal das interface.

3.511 Beiträge seit 2005
vor 15 Jahren

Mach ich ebenfalls. Gerade die neuen c# 3.5 Klassen um XML (XElement, XDocument, ...) bieten diese fluent Möglichkeit um XML Dokumente zu erstellen. Das geht wesentlich schneller von der Hand als über die "alten" XML Klassen (XmlNode, XmlDocument, ...). Gerade wenn man feste XML Dateien hat, die einem festen Schema folgen.

"Jedes Ding hat drei Seiten, eine positive, eine negative und eine komische." (Karl Valentin)

M
198 Beiträge seit 2007
vor 15 Jahren

AddGroup() Gehört zu meiner MenüKlasse und gibt ein MenuItem zurück.
AddItem, Executes, AllowedRole, AllowedRoles, AddSeparator, ExcludeFromMenu, ExcludeFromSidebar, SideBarCaptionDiffers, AddSubGroup, EndSubGroup gehören zu meiner MenuItem-Klasse und haben ein MenuItem als Rückgabewert.
EndGroup gehört ebenfalls zu MenuItem, gibt aber das Menu zurück.

Wie schon geschrieben muss man ein bisschen die Reihenfolge wissen, wobei sich eigentlich ergibt wann welche Methode wie angewand werden könnte (ausnahme AddSeparator, dies liegt jedoch an den DevExpress Menüs welche ich verwende, dort wird ein Separator nicht als einzelnes Item dargestellt sondern durch einen Boolwert bei dem Item was als nächstes folgt)

Schlüsselwörter die ein aktuelles Item in eine Itemliste legen sind AddItem, AddSubGroup, EndSubGroup, EndGroup.

Jetzt klarer?

JAck30lena:
Das liegt daran das ich mit dem Befehl direkt 2 Menüs befülle welche recht unterschiedlich sind (Outlookbar + Normales oberes Menü)
Könnte aber auch daran liegen, dass dies mein erster Versuch war, ich wollte einfach mal sehen was für einen Aufwand es ist eine solche Schreibweise zu ermöglichen

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo MaXeM,

wenn alle Klassen immer den gleichen MenuItem liefern, dann beziehen sich alle Methoden auch auf den. Ich habe doch konkret geschrieben, was mir unklar ist. Wie du das mit dem Ebenen hinbekommst. Insbesondere im Vergleich des ersten AddItems zu weiteren AddItems.

Anderseits geht es mir nicht nur um das hinbekommen, sondern auch um die Verständlichkeit deines Codes, der an dieser Stelle so m.E. noch nicht gegeben ist. Durch ein BeginAddItem/EndAddItem würde es klarer werden.

herbivore

Gelöschter Account
vor 15 Jahren

ok ich denke ich habe es jetzt verstanden. das menu sieht dann so aus:

-("Neuen Kunden anlegen")
-("Kundenübersicht anzeigen")
-("Neuen Artikel anlegen")
-("Artikelübersicht anzeigen")
-("Neuen Lieferanten anlegen")
-("Lieferantenübersicht anzeigen")

das ist aber recht verwirrend da:

_shell.Menu.AddGroup("Stammdaten") // das ist ja soweit klar
                     .AddItem("Neuen Kunden anlegen")//fügt ein neues element zum menu hinzu und liefert das eben angelegte item?
                         .Executes(Commands.CreateKunde)//macht was am eben angelegten item
                         .AllowedRole("Administrator")//macht noch was am eben angelegtem item
                     .AddItem("Kundenübersicht anzeigen")//und hier hört dann das verständniss auf bzw begehst du hier einen oop bruch, da du hier über das menuitem an das menu gehst und dort ein neues menuitem anlegst um dann dieses zurückzugeben (*)
                         .Executes(Commands.ShowKundenÜbersicht)//und das arbeitet am neuen menuitem

wenn ich damit richtig liege hast du dann folgenden weg beim (*)
Child-> Parent.AddItem -> return Parent.AddedItem

hier gilt aber dennoch auch was für forms gilt:
childs sollen am parent nichts machen.

liege ich richtig oder falsch mit meiner vermutung?

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo JAck30lena,

//und hier hört dann das verständniss auf bzw begehst du hier einen oop bruch, da du hier über das menuitem an das menu gehst und dort ein neues menuitem anlegst um dann dieses zurückzugeben (*)

das ist auch der Punkt, den ich die ganze Zeit meine.

herbivore

M
198 Beiträge seit 2007
vor 15 Jahren

JAck30lena:
Ja, deine Vermutung ist richtig. AddItem, EndGroup, EndSubGroup geben den jeweiligen Parent(Menu oder MenuItem) zurück.

Bzw. jein, eigentlich arbeite ich immer eine Ebene zu hoch. Ich habe eine private Variable namens CurrentItem, dort ist das aktuelle Item drin und arbeite vom Parent aus an diesem, jedoch gebe ich halt das Parent zurück ohne vom Child aus per Parent.AddItem o. Ä. was zu machen sondern eher (bsp AllowedRole()):

if(CurrentItem == null)
Roles.Add(role);
else
CurrentItem.Roles.Add(role)

beim AddSubGroup erzeuge ich ein neues MenuItem, Adde dieses der Itemliste und gebe das zurück, bei EndSubGroup returne ich den Parent. (AddItem macht nur CurrentItem = new MenuItem, AddSubGroup aber:
MenuItem subGroup = new MenuItem(caption, this);
subGroup.IsGroup = true;
AddMenuItem(subGroup);
return subGroup;

Gelöschter Account
vor 15 Jahren

das ist auch der Punkt, den ich die ganze Zeit meine.

ich weiß. ich wollte es nur von MaXeM explizit bestätigt haben.

ich persönlich finde diese lösung nicht ratsam. sie funktioniert, keine frage aber sie verwirrt.
wenn man an oop denkt, dann müsste das menu eigendlich laut code so aussehen:

-("Neuen Kunden anlegen")
--("Kundenübersicht anzeigen")
---("Neuen Artikel anlegen")
----("Artikelübersicht anzeigen")
-----("Neuen Lieferanten anlegen")
------("Lieferantenübersicht anzeigen")

edit: ah jetzt sehe ich dein edit^^

das problem ist, das man bei .additem().Machnochwas().additem()
annimmt, das man beim 2. additem auf dem ersten hinzugefügtem item arbeitet. und das verwirrt dann. (also zumindest mich verwirrt es)

M
198 Beiträge seit 2007
vor 15 Jahren

Naja ich fand es halt komfortabler auf ein EndItem zu verzichten, jedoch wäre es kein Problem dies hinzuzufügen

Frage: verstößt mein Ansatz dann immer noch gegen oop?

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo MaXeM,

und das verwirrt dann.

mehr als das. Es ist einfach unsauber. So solltest du das nicht lassen. Eine mögliche Alternative habe ich ja schon vorgeschlagen. Oder du verwendest AddSibbling. Auch dann wird es klarer. Der erste Aufruf müsste dann natürlich AddChild sein.

Frage: verstößt mein Ansatz dann immer noch gegen oop?

Nein, dann wäre es ok.

Wenn du mit Fluent Arbeitest, sollten die Methoden wirklich nur auf dem Rückgabewert arbeiten und nicht auf irgendwelchen Objekten in Hilfsvariablen.

herbivore