Design Patterns - manchmal auch nicht ganz so gutes Design?
-
Hallo,
ich lese zur Zeit "Design Patterns" von der GoF und ich glaube, dass mir einige Unstimmigkeiten bei den Design Patterns aufgefallen sind, die gegen die Grundsätze der OO-Programmierung verstoßen.
1. Beim Kapitel über das Strategy-Pattern ist mir aufgefallen, dass meinem "Algorithmus-Objekt" für seine Arbeit wohl häufig Werte übergeben muss. Wenn diese Werte dann Membervariablen des benutzenden Objekts sind, ist damit doch Data Hiding etc. passé, oder? Ich verteile ja hier meine Membervariablen auf zwei Klassen. Wo bleibt da die Datenkapselung? Außerdem ist meine Algorithmus-Klasse (in den meisten Fällen) wohl gar keine echte Klasse, weil ihr eben eigene Variablen fehlen. Sie ist eigentlich nur eine Funktion als Klasse verkleidet.
2. Gilt für das Bridge-Pattern nicht Ähnliches wie für Strategy? Auch hier wird ja Interface und Implementierung auseinander gerissen.
3. Das Composite-Pattern will eine einheitliche Schnittstelle für eine Klasse und Composites (~ Container) von Objekten dieser Klasse schaffen. Dazu wird eine abstrakte Klasse geschaffen, die natürlich alle Funktion der jeweiligen Klasse beinhaltet; dazu kommen jetzt aber auch noch Methoden für die Composite-Funktionalität, also bspw. CreateIterator() oder Add/Remove(). Dafür muss man dann nur noch eine geeignete Default-Implementierung finden und schon kann man auch die Klasse davon erben lassen, bei der eigentlich ein CreateIterator() herzlich wenig Sinn macht. Damit ist gemeinsame Schnittstelle da, aber ist das auch gut so?
Kann mich einer vielleicht aufklären
-
zu 1. Private Felder verstoßen doch nicht gegen den Data-Hiding Grundsatz, denn sie gehören nicht zur Schnittstelle der Klasse. Alternativ kannst Du aber auch immer alle Parameter über die Methoden-Signaturen mitschleifen und somit auf Felder und somit einen Objektzustand verzichten. Das könnte manchmal sinnvoll sein, z.B. bei Multithreading, weil Du dann nichts synchronisieren musst.
zu 2. Wie gesagt: Private Felder = Implementierungsdetails. Hat nix mit Interface der Klasse zu tun.
zu 3. Composite Pattern ist in der Tat nicht perfekt, da man entweder zwischen Composite und Child unterscheiden muss oder das Interface von Child unsauber ist, da sinnlose Methoden enthalten (getChildren() obwohl Child per Definition keine Children besitzt).
Trotzdem ist es eine vernünftige Möglichkeit, eine gemeinsame Schnittstelle zwischen Objekten zu finden, um sie somit gleich zu behandeln.
-
(getChildren() obwohl Child per Definition keine Children besitzt).
wobei getChildren nicht wirklich Problematisch ist. Das liefert einfach immer null zurückt. addChildren und removeChildren sind da eher das Problem. Diese müssen im prinzip ein Fehlercode zurückliefern.
-
Dieses Strategiemuster hat den Vorteil das du später weitere Strategien hinzufügen kannst ohne das das Hauptobjekt davon etwas wissen musst. Du entkoppelst beide Seiten, was nicht passieren würde wenn du weitere Methoden in deinem Hauptobjekt anfügen würdest welche die alternativen Algorithmen abbilden.
Dieses Problem mit dem Bekanntmachen/Delegieren von Implementierungsdetails gibt es auch für andere Muster (State, Facade) aber glücklicherweise gibt es ja hierfür das Schlüsselwort friend.
Das bei einem Composite die Gefahr eines fat interface besteht wird m.E. aber auch in dem Buch diskutiert. Man muß halt das Beste daraus machen. Ich kombiniere das Pattern gerne mit dem Visitor-Pattern, dann bleibt die Schnittstelle auch recht aufgeräumt.
-
ad 1 & 2: ich seher hier kein problem. nicht alles was "class" heisst muss im OOP sinn ein first class objekt sein. patterns können auch mal implementierungs detail sein, auch spricht nichts dagegen dass ein paar wenige klassen eng gekoppelt sind, so lange man dort entkoppelt wo es wichtig ist.
beispiel: wenn ich std::sort() aufrufe, klar sieht std::sort() dann meine "privaten" daten. und natürlich hab ich dann eine enge koppelung zu std::sort. und wenn es sinn macht packe ich auch std::sort() in ein objekt, falls ich mal - konfigurierbarerweise - auf foo::sort_faster() oder bar::sort_mighty_smart() umschalten will.
und? wo ist das problem?
ad 3: das composite pattern ist ein sinnvoller kompromiss, da es eben viele dinge vereinfacht. das ist der sinn. und deswegen ist es auch gut. WENN es angebracht ist. steht aber IIRC alles recht schön im GoF buch beschrieben.
-
man sollte design patters nicht ganz so ernst nehmen und 1 zu 1 umsetzen, sondern die ideen verstehen und mit neuen ideen programme entwerfen
-
Danke erstmal an alle für die zahlreichen Antworten.
Private Felder verstoßen doch nicht gegen den Data-Hiding Grundsatz, denn sie gehören nicht zur Schnittstelle der Klasse.
Das hab ich so auch nicht behauptet. Mein Einwand war eher, dass so Daten und Methoden, die mit ihnen arbeiten, getrennt werden. Das Strategy-Objekt wird immer von einer anderen Klasse gefüttert, "leiht" sich also dessen Daten aus. Ich hatte nur so 'nen Satz wie "Keep data and related behavior in one place." im Kopf.
@witte: Danke für deinen Kombinationstipp. Ich denke, die Übergabe per Parameter ist der friend-Variante vorzuziehen, oder?
@hustbaer: Ich denke, Containerklassen kann man da ausnehmen, die sind ein Extremfall.
-
moagnus schrieb:
Das Strategy-Objekt wird immer von einer anderen Klasse gefüttert, "leiht" sich also dessen Daten aus. Ich hatte nur so 'nen Satz wie "Keep data and related behavior in one place." im Kopf.
Ne, ist falsch.
Du rufst ja auch
std::sort auf um deinen vector zu sortieren.algorithmen und daten muessen nicht zusammen sein...
-
Shade Of Mine schrieb:
moagnus schrieb:
Das Strategy-Objekt wird immer von einer anderen Klasse gefüttert, "leiht" sich also dessen Daten aus. Ich hatte nur so 'nen Satz wie "Keep data and related behavior in one place." im Kopf.
Ne, ist falsch.
Du rufst ja auch
std::sort auf um deinen vector zu sortieren.
algorithmen und daten muessen nicht zusammen sein...Recht habt ihr beide. Allgemeine Algorithmen arbeiten mit (idealerweise) beliebigen Daten. Spezielle Algorithmen nicht.
-
moagnus schrieb:
@hustbaer: Ich denke, Containerklassen kann man da ausnehmen, die sind ein Extremfall.
Hab ich was von Containerklassen geschrieben?
-
Ja, aber std::sort ist ja auch keine Memberfunktion. Ich weiß nicht, aber ich hab das Gefühl, dass bei Strategy zu viel Daten von außen eingegeben werden. Wenn ich mir dann vorstelle, dass ich mal meiner Strategy nen Zeiger auf ne Membervariable der Klasse, die die Strategy benutzt, übergebe und mittels des Zeigers dann irgendein Wert während des Algorithmuses (in einer Strategy-Methode) verändert wird, dann wird der Wert der Variable nicht mehr nur von seiner Klasse bestimmt, sondern von der Strategy-Methode.
Ist das ein berechtigter Einwand oder ist das Beispiel eher zu konstruiert und sollte ich mir hustbaers ersten Absatz bzgl. manchmal enge Kopplung ist okay zu Herzen nehmen?
-
moagnus schrieb:
Ich weiß nicht, aber ich hab das Gefühl, dass bei Strategy zu viel Daten von außen eingegeben werden.
Das ist halt ein Kompromiss. Dass jede Verantwortlichkeit bei dem Objekt liegt, das die dazugehörigen Daten enthält, ist nur ein Designprinzip von mehreren. Ein anderes wäre, dass man veränderliches Verhalten durch Polymorphie ausdrücken soll. Das beides geht nicht unter einen Hut zu bringen, also muss man sich überlegen, wo man was aufweicht.
-
@moagnus:
Du kannst dir ja aussuchen wie die "Strategy" mit dem "eigentlichen" Objekt kommuniziert. Ein Extrem wäre "friend", das andere Extrem wäre ne totale Entkopplung mittels SOAP Interface oder etwas in der Art. Nochmal eine andere Möglichkeit ist ein generisches Template-basiertes Interface, wie es oft in der Standard-Library anzutreffen ist (z.B. eben bei sort, partition etc.).Es gibt sicher Fälle wo es Sinn macht alles schön zu entkoppelt, und keinen "direkten" Zugriff auf die Eingeweide einer Klasse zu erlauben.
Es gibt aber auch Fälle, wo es gänzlich egal ist.
Wichtig ist doch hauptsächlich, dass ein Funktionsblock/Modul sauber von anderen Funktionsblöcken/Modulen getrennt ist. Das heisst aber nicht, dass ein Funktionsblock nicht aus 2, 3 oder auch mal 10 eng gekoppelten Klassen bestehen dürfte.
-
Das Strategie Pattern sagt doch überhaupt nichts darüber aus, ob und wie Daten vom Algorithmus manipuliert werden. Das ist eine Frage, wie Du den Algorithmus implementierst. Wenn Du Seiteneffekt vermeiden willst, dann manipulier halt nicht das übergebene Objekt sondern erzeuge ein neues und liefer das zurück.
Das Strategie Pattern besagt lediglich, dass man eine einheitliche Schnittstelle schafft, um Algorithmen polymorph austauschen zu können.
-
moagnus schrieb:
Ja, aber std::sort ist ja auch keine Memberfunktion.
Ich sehe den Unterschied nicht.
Die Strategy kann ja zB eine funktion sein.
class Context { private: boost::function<void(*)(int)> strategy; public: void setStrategy(boost::function<void(*)(int)> strategy) { this->strategy = strategy; } void execute(int data) const { strategy(data); } };
Wie du die Strategy Objekte mit Context und den Daten koppelst, ist dir selbst überlassen...
-
moagnus schrieb:
Ich denke, die Übergabe per Parameter ist der friend-Variante vorzuziehen, oder?
Kommt drauf an. Beispiel: Du hast ein Form/Webpage mit 30 Controls und die unterschiedlichen Bearbeitungsmodi dieses Forms sollen in ein Strategiemuster ausgelagert werden. Also ein Objekt setzt das Form im Nur-Lesen-Modus, eins in den Bearbeitungsmodus und eins in den Anfügemodus. Also ICH werde da bestimmt keine 30 Controlreferenzen übergeben.
-
witte schrieb:
moagnus schrieb:
Ich denke, die Übergabe per Parameter ist der friend-Variante vorzuziehen, oder?
Kommt drauf an. Beispiel: Du hast ein Form/Webpage mit 30 Controls und die unterschiedlichen Bearbeitungsmodi dieses Forms sollen in ein Strategiemuster ausgelagert werden. Also ein Objekt setzt das Form im Nur-Lesen-Modus, eins in den Bearbeitungsmodus und eins in den Anfügemodus. Also ICH werde da bestimmt keine 30 Controlreferenzen übergeben.
Du übergibts eine Referenz auf die Form... Die Form kennt ja die eigenen Controls. Oder übersehe ich da was? Schliesslich ist dem Strategy ja der Rest der Seite komplett egal...
-
Er wollte die Member (hier die Controls des Form) nicht public machen um die Kapselung nicht aufzubrechen, wenn ich das richtig verstanden habe. Also müssen doch entweder die Member übergeben werden oder die Strategieklassen sind friends und manipulieren den Client direkt.
-
Wobei C# ja leider kein "friend" kann... und "internal" ist halt etwas grob.
-
@Bashar: Ok, ich glaube "aufweichen" ist hier ein gutes Schlüsselwort.
@hustbaer: Ok, ich hatte da vielleicht eine zu idealistische Ansicht. Vollkommen voneinander entkoppelte Klassen sind wohl auch nicht immer möglich.
@byto: Ja, du hast recht. Ich schätze mal, mein Beispiel war zu überzogen. Ich versteh das Strategy-Pattern jetzt mal als hilfreichen Kompromiss.
@witte: Die Member public zu setzen, würde mir sowieso nicht einfallen. Die drei Möglichkeiten, die ich sehe - und die auch schon genannt wurden - sind:
1. Übergabe der notwendigen Parameter. Wenn sich das in Grenzen hält, scheint das mir die beste Variante zu sein.
2. Die Anfreundung der zwischen Klasse und Strategy-Klasse über "friend".
3. "Callback" vom Strategy-Objekt aus mittels Getter-Methoden. Das halte ich für die schlechteste Variante, da es das Interface der benutzenden Klasse aufbläht, und das in einer eigentlich unerwünschten Weise.Als Resümee versuch ich mal die engere Kopplung positiver zu sehn, sozusagen.