Fabrikmethode -- Factory Method

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Antworten
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: Fabrikmethode -- Factory Method

Beitrag von smurfer »

Wann immer du dich in der Situation befindest, dass du an einer Stelle ein Interface hast, mit dem der Code, der mit dem entsprechenden Objekt was tun sollte, nichts anfangen kann, zeigt dies einen Designfehler an. Ein Downcast, egal ob nun in Form eines static_cast oder dynamic_cast oder per selbst gebastelte, explizite Typabfrage oder was auch immer, löst niemals das eigentliche Problem, sondern lindert nur die Symptome, bis die Krankheit an anderer Stelle wieder ausbricht, in der Regel schlimmer als beim ersten Mal.
Hier möchte ich noch einmal direkt nachgefragen: Was ist denn konkret so schlimm daran? Es wird fast immer pauschal geschrieben, aber was macht es zu so einem schlechten Design, wo holt es mich wieder ein? Und, welche Alternative sollte man nutzen?

Mir ist bewusst, dass blindes Hin- und Hercasten nicht sonderlich sinnvoll ist, aber an wohl-definierten Stellen?!

Beispiel Kollisionsabfrage: Anfangs habe ich für die verschiedenen Objekttypen double-dispatching verwendet, was nach außen hin sicher recht elegant scheint und damals einfach "in" war. Es war mir aber eine zu starke Kopplung in die Objekte, ich wollte die Kollisionsabfrage in eine einzige Klasse kapseln, um sie leicht manipulieren oder ersetzen zu können. Ich kann natürlich die verschiedenen Objekte in unterschiedliche Listen* eintragen. Dies mache ich auch an den Stellen, wo das Vorgehen sich stark unterscheidet, wie bei den Partikeln und den Objekten. An anderen Stellen gibt es eine gemeinsame Liste und es wird der Typ explizit erfragt. Ich sehe kein Problem darin.
Ähnlich beim Speichern des Simulationsfortschritts. Das Emitter-Interface vereinfacht an vielen Stellen meinen Code. Beim Rausstreamen der Informationen wird die Emitterliste gestreamt. Ich könnte mehrere Listen anlegen und müsste nicht den Typ mitstreamen und beim Laden erfragen. Allerdings geschieht dies nicht während der Simulationszeit und ist von der Performance vernachlässigbar. Dafür muss ich nicht überall drei Listen pflegen.

Gibt es noch weitere Möglichkeiten, wie beispielsweise bei einer Kollisionsabfrage, verschiedene Typen zu testen, wenn nicht explizit typeigene Listen, double-dispatching oder Interfaces mit Typabfragen (sei es hausgemacht, dynamic_cast oder wie auch immer)?

* Liste als sinngemäßer Begriff, nicht zwangsweise std::list oder Ähnliche
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Fabrikmethode -- Factory Method

Beitrag von dot »

smurfer hat geschrieben:
Wann immer du dich in der Situation befindest, dass du an einer Stelle ein Interface hast, mit dem der Code, der mit dem entsprechenden Objekt was tun sollte, nichts anfangen kann, zeigt dies einen Designfehler an. Ein Downcast, egal ob nun in Form eines static_cast oder dynamic_cast oder per selbst gebastelte, explizite Typabfrage oder was auch immer, löst niemals das eigentliche Problem, sondern lindert nur die Symptome, bis die Krankheit an anderer Stelle wieder ausbricht, in der Regel schlimmer als beim ersten Mal.
Hier möchte ich noch einmal direkt nachgefragen: Was ist denn konkret so schlimm daran? Es wird fast immer pauschal geschrieben, aber was macht es zu so einem schlechten Design, wo holt es mich wieder ein?
In dem Moment, wo du einen Downcast brauchst, befindest du dich in einer Situation, in der dein Design sich selbst widerspricht. Der fragliche Code hat von seinem Aufrufer nur einen X Basisklassenzeiger bekommen, was bedeutet, dass das Interface des fraglichen Code sagt: Ich mache dieses und jenes mit jedem beliebigen X. Wenn du in der Implementierung dieses Code dann auf einmal ein konkretes Y brauchst, dann sagt uns das, dass der entsprechende Code offenbar doch nicht mit jedem beliebigen X funktioniert, sondern nur mit jenen X, die ein Y sind. Entweder hätte also das Interface bereits nach einem Y verlangen sollen, da die entsprechende Operation offenbar nur mit einem Y Sinn macht und nicht mit jedem X im Allgemeinen, oder irgendwas ist mit der Abstraktion X an sich verkehrt. Die Lösung ist in jeden Fall, das Problem im Design, das zu der entsprechenden Situation geführt hat, zu beheben und nicht – per Downcast – Code zu schreiben, der so tut, als würde er für jedes X das selbe machen, in Wirklichkeit aber, je nachdem was für ein konkretes Objekt sich hinter dem jeweiligen X befindet, etwas anderes macht...

Alternative, kurze Erklärung: Du verletzt in dem Moment, wo du einen Downcast machst, zwangsweise automatisch das Liskovsche Substitutionsprinzip...
smurfer hat geschrieben:Und, welche Alternative sollte man nutzen?
Dein konkreter Fall klingt nach einem Paradebeispiel für Double Dispatch... ;)
smurfer hat geschrieben:Mir ist bewusst, dass blindes Hin- und Hercasten nicht sonderlich sinnvoll ist, aber an wohl-definierten Stellen?!
Es geht hier nicht um Casts im Allgemeinen, sondern konkret um Downcasts (d.h. Cast von Basis runter auf abgeleitete Klasse). Ich bin absolut kein Fan von Absolutismen ;). Aber wenn es einen Fall gibt, wo man wirklich mal eine absolute Aussage machen kann, dann ist es: Ein Downcast ist immer Symptom eines Designfehlers. In all meinen Jahren ist mir noch nicht ein einziges Beispiel untergekommen, wo man imo auch nur beginnen hätte können, darüber nachzudenken, ob man vielleicht diskutieren könnte, dass ein Downcast unter gewissen Voraussetzungen nicht eventuell doch vertretbar sein könnte. Selbst nach vielmaligem, intensivem Nachdenken konnte ich bis jetzt noch nicht mal ein rein hypothetisches Beispiel für den potentiell akzeptablen Einsatz eines Downcast erfinden. Ich lasse mich natürlich immer gerne eines Besseren belehren, aber ich hab diese Diskussion schon wirklich oft geführt und der Downcast steht bei mir immer noch ganz weit oben auf der Liste der ganz groben Vergehen, gleich neben so Dingen wie dem Singleton Pattern...
smurfer hat geschrieben:Beispiel Kollisionsabfrage: Anfangs habe ich für die verschiedenen Objekttypen double-dispatching verwendet, was nach außen hin sicher recht elegant scheint und damals einfach "in" war. Es war mir aber eine zu starke Kopplung in die Objekte, ich wollte die Kollisionsabfrage in eine einzige Klasse kapseln, um sie leicht manipulieren oder ersetzen zu können.
Inwiefern war welche Kopplung zu stark? Dass die Kollisionsbestimmung zwischen verschiedenen Kombinationen verschiedener Objekttypen jeweils verschiedener Algorithmen bedarf, ist eine fundamentale Eigenschaft des Problems an sich; die Auswahl des Algorithmus ist rein prinzipiell an die Kombination der beteiligten Objekttypen gekoppelt, diese Kopplung ist ein notwendiger Umstand in jeder möglichen Lösung und kein unglücklicher Nebeneffekt eines bestimmten Lösungsansatzes. Double Dispatch liefert eine elegante Lösung für die Frage, wie man, gegeben eine beliebige Kombination von zwei Objekttypen, den passenden Algorithmus auswählen kann...
smurfer hat geschrieben:Nun zum Auslöser meiner Anfrage:
Ich habe einen WorldDataStorage, in dem vollwertig physikalische Objekte, eingeschränke physikalische Objekte wie Partikel und auch Emitter residieren.
Imo liegt da dein Problem. Sollten Emitter nicht viel eher Objekte in eine beliebige WorldDataStorage emittieren? Sollte es einer WorldDataStorage nicht absolut egal sein, was es alles für Arten von Emittern gibt oder vielleicht irgendwann mal geben wird? Abgesehen davon, würde ich mir die Frage stellen, ob es so eine gute Idee ist, einen solchen 'Emitter" wirklich ständig neue Objekte erzeugen und alte löschen zu lassen. Bei Partikelsystemen hat man in der Regel eine fixe, maximale Anzahl an Partikeln pro Emitter, die man einfach einmal alle erzeugen kann. Der Emitter führt dann nur noch das Updaten der Partikel durch. Meist will man in dem Moment, wo ein Partikel stirbt, direkt wieder ein neues emittieren. Anstatt das eine zu löschen und ein neues zu erzeugen, kann man einfach das bereits existierende resetten...
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Fabrikmethode -- Factory Method

Beitrag von Chromanoid »

Mal verschlafen am Rande:

Als Java-Entwickler ist man ja ziemlich verseucht mit Downcast-Lösungen. Ich glaube nicht, dass das zwangsläufig ein Designfehler ist. Ich bin auch nicht davon überzeugt, dass das Liskovsche Substitutionsprinzip dadurch zwangsweise verletzt wird.

Wenn man eine (in vielen Programmiersprachen ermittelbare Eigenschaft) wie den Typ oder eben eine entsprechende Typ-ID eines Objekts als Teil des Objekts betrachtet, dann ist es keine Verletzung des Prinzips, wenn sich das Verhalten des Programms verändert, je nach dem welcher Objekt-Typ gerade in eine Methode reinflattert. Meiner Meinung nach sind dokumentierte Verträge für Schnittstellen/Klassen und ihre Methoden/Eigenschaften die "beweisbaren Eigenschaften" aus dem Liskovschen Substitutionsprinzip. Nur weil man in einer Methode auf den Subtyp eines Typs anders reagiert als auf den Typ selbst, heißt das noch lange nicht, dass diese Verträge verletzt werden. Wenn man einen Typ ableitbar bzw. die Methoden eines Typs überschreibbar macht, müssen die entsprechenden Verträge sowieso genau definiert sein. Nur wenn man dann in einem Subtyp gegen die Verträge verstößt, nur dann verstößt man auch gegen das Liskovsche Substitutionsprinzip...

Cool finde ich in diesem Zusammenhang das hier: http://ceylon-lang.org/blog/2012/01/25/enumerated-types / http://ceylon-lang.org/documentation/1.2/tour/types/
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: Fabrikmethode -- Factory Method

Beitrag von smurfer »

Chromanoid, sehe ich ähnlich, wenn ich es richtig verstanden habe, siehe meine Antwort im Folgenden.
dot hat geschrieben:In dem Moment, wo du einen Downcast brauchst, befindest du dich in einer Situation, in der dein Design sich selbst widerspricht. Der fragliche Code hat von seinem Aufrufer nur einen X Basisklassenzeiger bekommen, was bedeutet, dass das Interface des fraglichen Code sagt: Ich mache dieses und jenes mit jedem beliebigen X. Wenn du in der Implementierung dieses Code dann auf einmal ein konkretes Y brauchst, dann sagt uns das, dass der entsprechende Code offenbar doch nicht mit jedem beliebigen X funktioniert, sondern nur mit jenen X, die ein Y sind. Entweder hätte also das Interface bereits nach einem Y verlangen sollen, da die entsprechende Operation offenbar nur mit einem Y Sinn macht und nicht mit jedem X im Allgemeinen, oder irgendwas ist mit der Abstraktion X an sich verkehrt. Die Lösung ist in jeden Fall, das Problem im Design, das zu der entsprechenden Situation geführt hat, zu beheben und nicht – per Downcast – Code zu schreiben, der so tut, als würde er für jedes X das selbe machen, in Wirklichkeit aber, je nachdem was für ein konkretes Objekt sich hinter dem jeweiligen X befindet, etwas anderes macht...
Das ist die Art von Antwort, die ich meinte: Das Design ist schlecht, weil ein solches Design schlecht ist. Werd doch mal konkreter :)

Im Prinzip habe ich eine Kollisionsmethode, die nur X benötigt und nach außen hin mit jedem X funktioniert. Die Information welches Y das X ist, ist auch vorhanden, nicht in Form der sprachtechnischen Ausprägung als Basisklasse, sondern gewissermaßen als Member, die jedes X besitzt. Ich denke das geht in die Richtung, die Chromanoid angesprochen hatte.
dot hat geschrieben: Dein konkreter Fall klingt nach einem Paradebeispiel für Double Dispatch... ;)
dot hat geschrieben:Es geht hier nicht um Casts im Allgemeinen, sondern konkret um Downcasts (d.h. Cast von Basis runter auf abgeleitete Klasse). Ich bin absolut kein Fan von Absolutismen ;). Aber wenn es einen Fall gibt, wo man wirklich mal eine absolute Aussage machen kann, dann ist es: Ein Downcast ist immer Symptom eines Designfehlers. In all meinen Jahren ist mir noch nicht ein einziges Beispiel untergekommen, wo man imo auch nur beginnen hätte können, darüber nachzudenken, ob man vielleicht diskutieren könnte, dass ein Downcast unter gewissen Voraussetzungen nicht eventuell doch vertretbar sein könnte. Selbst nach vielmaligem, intensivem Nachdenken konnte ich bis jetzt noch nicht mal ein rein hypothetisches Beispiel für den potentiell akzeptablen Einsatz eines Downcast erfinden. Ich lasse mich natürlich immer gerne eines Besseren belehren, aber ich hab diese Diskussion schon wirklich oft geführt und der Downcast steht bei mir immer noch ganz weit oben auf der Liste der ganz groben Vergehen, gleich neben so Dingen wie dem Singleton Pattern...
Siehe oben, werd doch mal konkret :) . Was ist am obigen Beispiel der Kollisionsabfrage so schlimm, welche Fallstricke erwarten mich? Ich habe eine definierte Stelle an der ich weiß, dass Objekte ankommen, die auf Kollision geprüft werden und alle ihren Typ mit sich bringen. Nochmal darauf speziell:
dot hat geschrieben:Selbst nach vielmaligem, intensivem Nachdenken konnte ich bis jetzt noch nicht mal ein rein hypothetisches Beispiel für den potentiell akzeptablen Einsatz eines Downcast erfinden.
Deine Kernaussage es sei grundsätzlich schlecht, weil es schlecht ist, klingt für mich viel hypothetischer und diskutiert habe ich auch schon oft. ;)

Zur Kopplung:
dot hat geschrieben: Inwiefern war welche Kopplung zu stark? Dass die Kollisionsbestimmung zwischen verschiedenen Kombinationen verschiedener Objekttypen jeweils verschiedener Algorithmen bedarf, ist eine fundamentale Eigenschaft des Problems an sich; die Auswahl des Algorithmus ist rein prinzipiell an die Kombination der beteiligten Objekttypen gekoppelt, diese Kopplung ist ein notwendiger Umstand in jeder möglichen Lösung und kein unglücklicher Nebeneffekt eines bestimmten Lösungsansatzes. Double Dispatch liefert eine elegante Lösung für die Frage, wie man, gegeben eine beliebige Kombination von zwei Objekttypen, den passenden Algorithmus auswählen kann...
Diese Kopplung ist natürlich gegeben, aber das hat doch nichts damit zu tun, an welcher Stelle des Codes (örtlich gesehen) ich auf diese eingehe.

Angenommen ich möchte grundlegende Dinge an der Kollisonsabfrage ändern(1), neue Objekte hinzufügen(2) oder grundlegend verschiedene Kollisionsvarianten vorhalten(3), dann bedeutet das für die jeweilige Vorgehensweise:
Double Dispatch
  • Ermitteln welche Objekte (Klassen -> Dateien) betroffen sind und Code in den jeweiligen Objekten ändern (1)
  • Neue Kollisionsmethoden an einer Stelle für die neue Klasse erstellen und im Minimalfall nur den Rückruf in den anderen Klassen (quasi triple dispatch ;) ), ansonsten in allen beteiligten Klassen (2)
  • Varianten in den Objekten vorhalten oder Varianten der Objekte vorhalten (3)
Kapselung mit Downcast
  • Sämtlichen Code innerhalb des Kollisionsmanagers ändern (1)
  • Neue Kollisionsmethoden an einer Stelle im Kollisionsmanager erstellen (2)
  • Varianten der Kollisionsmanager vorhalten (3)
Auch ich finde Double-Dispatch elegant und habe es auch nicht grundlos anfangs implementiert. Aus rein praktischen Gründen empfand ich es aber zu verstreut im Code und wollte mich in allen drei oben genannten Fällen nur um eine Klasse/Datei kümmern. Ich finde Double-Dispatch mit Sicherheit nicht verkehrt, es ist eine persönliche Vorliebe gewesen, die gezeigte Alternative zu wählen. Was mir immer noch fehlt, ist ein wirklich praktischer Grund, was mir dabei früher oder später so sehr um die Ohren fliegt, dass es das Design schlecht macht.
dot hat geschrieben:Imo liegt da dein Problem. Sollten Emitter nicht viel eher Objekte in eine beliebige WorldDataStorage emittieren? Sollte es einer WorldDataStorage nicht absolut egal sein, was es alles für Arten von Emittern gibt oder vielleicht irgendwann mal geben wird? Abgesehen davon, würde ich mir die Frage stellen, ob es so eine gute Idee ist, einen solchen 'Emitter" wirklich ständig neue Objekte erzeugen und alte löschen zu lassen. Bei Partikelsystemen hat man in der Regel eine fixe, maximale Anzahl an Partikeln pro Emitter, die man einfach einmal alle erzeugen kann. Der Emitter führt dann nur noch das Updaten der Partikel durch. Meist will man in dem Moment, wo ein Partikel stirbt, direkt wieder ein neues emittieren. Anstatt das eine zu löschen und ein neues zu erzeugen, kann man einfach das bereits existierende resetten...
Ja, mein Fehler, siehe Edit in dem entsprechenden Post, wahrscheinlich hast Du ihn nicht mehr rechtzeitig gelesen. Die Emitter sind in einem eigenen "Storage" und emittieren in den WorldDataStorage. Partikel als solche sind in einem Ringpuffer und dieser hat eine fixe Größe. Es können aber auch physikalisch vollwertige Objekte erzeugt werden, die (undefiniert) länger leben, sie werden tatsächlich an den WorldDataStorage übergeben und sind dann als vollwertige Objekte in seiner Obhut. Und genau weil ich nicht ständig neue Objekte (im Speicher) erzeugen möchte, sollte der WorldDataStorage das Erzeugen der Objekte im Speicher übernehmen, wobei ich wieder beim Grund meines ursprünglichen Anliegens angekommen bin.

Edit: Copy-Paste-Fehler bei Punkt (3) der Kapselung-mit-Downcast-Liste korrigiert.
Zuletzt geändert von smurfer am 07.02.2016, 11:47, insgesamt 1-mal geändert.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Fabrikmethode -- Factory Method

Beitrag von Chromanoid »

Ich bin hier voll auf Deiner Seite. Ich glaube sogar, dass wenn eine Sprache keine oder nur "widerwillig" Mittel zur Ermittlung des Objekt-Typs bereitstellt, das automatisch zu einer starken Behinderung des Zusammenspiels von Bibliotheken aus unterschiedlicher Hand führt. Und ich habe auch tatsächlich das Gefühl, dass sich die Entwicklungen von Bibliotheken z.B. in C++ immer sehr stark um sich selbst drehen.

Ich glaube genau Dein Usecase ist ein Grund für das Einführen von Reflection und ähnlicher Ansätze. Möchte man zum Beispiel einen Editor für ein Objektmodell entwerfen, ist man bei Sprachen ohne Reflection sehr darauf angewiesen, dass das Objektmodell diese Möglichkeit (und am besten auch noch viele andere) antizipiert. Die Komplexität, die bei dieser Antizipation in das Objektmodell wandern würde, ist bei vielen Sprachen einfach verallgemeinert worden und in eine Relfection-API gewandert.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Fabrikmethode -- Factory Method

Beitrag von Alexander Kornrumpf »

smurfer hat geschrieben: ... WorldDataStorage ... Physikmanager ... Einen ähnlichen Datenverwalter gibt es für Visuals ... WorldDataStorageManager
Vielleicht bin ich naiv, aber ich denke, wenn du die o. g. Parasitenklassen wegwirfst werden sich deine Designprobleme wahrscheinlich von alleine lösen.
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: Fabrikmethode -- Factory Method

Beitrag von smurfer »

Hm, das finde ich nicht naiv, sondern eher ein wenig provokant und auch pauschal formuliert -- die Begriffe hast Du schon sehr plakativ rausgezogen. Nenn es nicht Manager und belass es in irgendeiner einzelnen Methode/Funktion und schon ist es nur noch eine Schleife, die durchlaufen wird -- irgendwer muss es machen. Und die Daten an bestimmten Stellen von der Logik zu trennen, empfinde ich auch nicht grundsätzlich als parasitär.

Abgesehen davon mal ganz direkt: Welche Designprobleme? Ich sehe ja gar kein großartiges Designproblem, das ich loswerden müsste. Ich wollte nur verschiedene Möglichkeiten ausloten und schauen, ob ich etwas übersehen habe.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Fabrikmethode -- Factory Method

Beitrag von Spiele Programmierer »

Und die Daten an bestimmten Stellen von der Logik zu trennen
Das widerspricht meines Erachtens der Idee der Objektorientierung und Kapselung.
Schließlich soll die Datenrepräsentation und Funktionsweise in der Kapselung von außen nicht erkennbar sein.
Klassen im Sinne der Objektorientierung sind mehr als Strukturen in C.

Sowohl "WorldDataStorage" als auch einen "WorldDataStorageManager" zu haben wirkt für mich sehr seltsam.
Warum muss man den "Storage" "managen". Warum kann sie das nicht selbst tun. Wenn es eine "World" gibt, kann die doch außerdem einfach die Daten direkt besitzen. Es klingt weder so, als ob man die Klasse irgendwo anders brauchen kann, noch so, als ob sie selbst sinnvoll Aufgaben übernehmen kann.
Das ist die Art von Antwort, die ich meinte: Das Design ist schlecht, weil ein solches Design schlecht ist. Werd doch mal konkreter :)
Down Casts halte ich auch für gefährlich und zu vermeiden. Dot hat es schon schön dargelegt. Ich würde zwar nicht so weit gehen und sagen, dass man sie immer (auf eine sinnvolle Weise) vermeiden kann, aber doch sehr häufig. Das sollte man deswegen, weil der Typ die "Kompetenzen" angibt sollte, die das Objekt hat. Wenn an einer Stelle mehr "Kompetenzen" benötigt werden als eigentlich angegeben, ist das ein Problem - ein Designproblem. Ganz konkret kann das dazu führen, dass Laufzeitfehler bzw. Undefined Behaviour ausgelöst werden, die andernfalls zur Compile-Zeit aufgefallen wäre. Außerdem verschlechtert es die Verständlichkeit und Dokumenation des Codes an der Schnittstelle selbst, wenn ein Objekt gecastet werden muss, damit es wie notwendig verwendet werden kann. Das es in einem anderen Typ gedowncastet werden muss bzw. kann steht schließlich nicht im Code der Schnittstelle.

Andernfalls könnte man doch auch einfach void* verwenden? Hmm? Warum denn nicht, ist schließlich auch eine Möglichkeit, oder?
Designprobleme? Achwas - bestimmt hypothetische Bedenken. ;)
Und ich habe auch tatsächlich das Gefühl, dass sich die Entwicklungen von Bibliotheken z.B. in C++ immer sehr stark um sich selbst drehen.
Ich weiß gerade nicht, was du damit meinst.
Hört sich an, als ob in Java mittels Reflection fremde Bibliotheken automatisch erkannt und verwendet würden???
Zuletzt geändert von Spiele Programmierer am 07.02.2016, 21:01, insgesamt 1-mal geändert.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Fabrikmethode -- Factory Method

Beitrag von Chromanoid »

Spiele Programmierer hat geschrieben:
Und ich habe auch tatsächlich das Gefühl, dass sich die Entwicklungen von Bibliotheken z.B. in C++ immer sehr stark um sich selbst drehen.
Ich weiß gerade nicht, was du damit meinst. Hört sich an, als ob in Java werden mittels Reflection fremde Bibliotheken automatisch erkannt und verwendet würden???
Ja, so in der Art kommt das häufig vor. Insbesondere bei Serversystemen ist das sehr praktisch. Stell Dir vor, Du programmierst ein paar Klassen steckst sie in ein Modul (JAR/DLL) und schiebst das Modul in einen Application Server. Der Server lädt Deine Server-Klassen automatisch, exponiert sie, schiebt Dir per Reflection die richtige extern konfigurierte Datenbankverbindung in die Request-Behandlungsmethode usw. Genauso gibt es zahlreiche Bibliotheken, die z.B. dein POJO anhand der Eigenschaften in ein Template zur Ausgabe füllen usw. MVVM und XAML lassen grüßen. Auch Bibliotheken, die Objekte serialisieren/deserialisieren, haben dank Reflection trotz dynamischer Verlinkung ein leichtes Spiel.

Das lässt sich alles auch mit C++ erledigen, aber eben nicht so einfach und schon gar nicht, wenn man das ganze auch noch dynamisch linken will.

Eine noch viel größere Rolle spielt bei der Integrierbarkeit von Bibliotheken in Java und Co. sicher das dynamische Laden des Codes und die damit einhergehende Einfachheit Bibliotheken mit ihrem gesamten Objektmodell ohne zusätzliches Wissen zu verknüpfen. Aber trotzdem glaube ich, dass das ganze erst so richtig Fahrt aufnimmt, wenn zur Laufzeit die Datenströme aus den Bibliotheken anhand von Reflection gezielter verarbeitet werden können. Reflection und dynamisches Laden führt, wenn man nicht aufpasst, natürlich auch zu äußerst gruseligen und bedrohlichen Deserialisierungs-Exploits. Alles hat eben sein Für und Wider.

Naja aber das ist wohl alles offtopic. Ganz interessant finde ich trotzdem, dass man wahrscheinlich in den meisten Spiele-Engines Ansätze für Reflection finden kann. Sobald fremde Entwickler Funktionalität hinzufügen sollen, die nicht Teil eines großen Ganzen ist, und man die Resultate dieser Entwicklung trotzdem so verarbeiten möchte, kommt es natürlicherweise zu Implementierungen von Reflection oder Reflection-artigen Systemen. In der Unreal Engine gibt es z.B. das UObject von dem AFAIK alle Gameplay-Sachen erben, das bringt natürlich gleich auch Reflection mit. Der Editor kann so die Eigenschaften anzeigen, der Netzwerkcode die richtigen Felder in der richtigen Häufigkeit an die anderen Mitspieler übermitteln usw.
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: Fabrikmethode -- Factory Method

Beitrag von smurfer »

Spiele Programmierer hat geschrieben:
Und die Daten an bestimmten Stellen von der Logik zu trennen
Das widerspricht meines Erachtens der Idee der Objektorientierung und Kapselung.
Schließlich soll die Datenrepräsentation und Funktionsweise in der Kapselung von außen nicht erkennbar sein.
Vom Gefühl her sehe ich grundsätzlich keinen Widerspruch, bzw. kann mir durchaus vorstellen, an geeigneten Stellen stärker objektorientiert und an anderen modularer vorzugehen. Vielleicht passt hier auch das Wort Kapselung nicht. Allerdings möchte ich Dir auch nicht grundsätzlich widersprechen, da ich nicht genau weiß, ob ich dich richtig verstanden habe und mit auch mit den Begrifflichkeiten nicht ganz sicher bin.
Spiele Programmierer hat geschrieben:Klassen im Sinne der Objektorientierung sind mehr als Strukturen in C.
Ja, das ist wohl wahr. Siehe oben, so ganz verstehe ich nicht, worauf du mit der Anmerkung hinaus möchtest. Gebe ich dir das Gefühl, eine Klasse sei für mich nur eine C-Struktur?
Spiele Programmierer hat geschrieben:Sowohl "WorldDataStorage" als auch einen "WorldDataStorageManager" zu haben wirkt für mich sehr seltsam.
Einverstanden, den Manager braucht es nicht.
Spiele Programmierer hat geschrieben:Down Casts halte ich auch für gefährlich und zu vermeiden. Dot hat es schon schön dargelegt. Ich würde zwar nicht so weit gehen und sagen, dass man sie immer (auf eine sinnvolle Weise) vermeiden kann, aber doch sehr häufig. Das sollte man deswegen, weil der Typ die "Kompetenzen" angibt sollte, die das Objekt hat. Wenn an einer Stelle mehr "Kompetenzen" benötigt werden als eigentlich angegeben, ist das ein Problem - ein Designproblem. Ganz konkret kann das dazu führen, dass Laufzeitfehler bzw. Undefined Behaviour ausgelöst werden, die andernfalls zur Compile-Zeit aufgefallen wäre. Außerdem verschlechtert es die Verständlichkeit und Dokumenation des Codes an der Schnittstelle selbst, wenn ein Objekt gecastet werden muss, damit es wie notwendig verwendet werden kann. Das es in einem anderen Typ gedowncastet werden muss bzw. kann steht schließlich nicht im Code der Schnittstelle.
Auch das ist ja grundsätzlich nicht verkehrt, es ist gut wenn man nicht downcasten muss. Es kann zu Problemen führen wenn wahrlos eingesetzt. Aber nochmal zum konkreten Beispiel der Kollisionsabfrage. Es handelt sich um einen klar definierten, in sich geschlossenen Bereich, in dem jegliches Vorwissen gegben ist. Die "Kompetenz" ist nun nicht c++-sprachlich durch die Klasse also solche gegeben, sondern über das Interface und den mitgegebenen Typ. Wo ist das Problem?
Ob die Lesbarkeit bei double dispatch nun größer ist als bei einer einzelnen Kollisionsklasse mit wohldefinierten Downcasts wage ich zu bezweifeln.
Spiele Programmierer hat geschrieben:Andernfalls könnte man doch auch einfach void* verwenden? Hmm? Warum denn nicht, ist schließlich auch eine Möglichkeit, oder?
Designprobleme? Achwas - bestimmt hypothetische Bedenken. ;)
Warum ins Lächerliche ziehen? Ich spreche niemandem ab, dass double dispatch und Vermeiden von Downcasts ein gutes Design ist. Ich sehe nur ernsthaft in klar abgesteckten Szenarien wie den angesprochenen nicht den dogmatisch erwähnten Designfehler, wenn es mir an anderer Stelle Vorteile verschafft und die Handhabung vereinfacht.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Fabrikmethode -- Factory Method

Beitrag von Alexander Kornrumpf »

smurfer hat geschrieben:Hm, das finde ich nicht naiv, sondern eher ein wenig provokant und auch pauschal formuliert -- die Begriffe hast Du schon sehr plakativ rausgezogen.
Provozieren wollte ich wenn überhaupt nur dazu es mal ganz anders zu betrachten.
Nenn es nicht Manager und belass es in irgendeiner einzelnen Methode/Funktion und schon ist es nur noch eine Schleife, die durchlaufen wird -- irgendwer muss es machen. Und die Daten an bestimmten Stellen von der Logik zu trennen, empfinde ich auch nicht grundsätzlich als parasitär.
Klingt sinnvoll. Aber wieso muss innerhalb der Schleife der Typ unterschieden werden? Wenn verschiedene Typen verschieden behandelt werden wollen, täten es auch mehrere Schleifen über jeweils homogene Daten?!
Abgesehen davon mal ganz direkt: Welche Designprobleme? Ich sehe ja gar kein großartiges Designproblem, das ich loswerden müsste. Ich wollte nur verschiedene Möglichkeiten ausloten und schauen, ob ich etwas übersehen habe.
Ich bin nicht ganz sicher, worüber wir dann hier diskutieren. Wenn du Code hast, der für dich funktioniert, würde ich den nicht anfassen, bevor nicht irgendein Problem auftaucht, was das nötig machen würde.
Spiele Programmierer
Establishment
Beiträge: 426
Registriert: 23.01.2013, 15:55

Re: Fabrikmethode -- Factory Method

Beitrag von Spiele Programmierer »

Ja, das ist wohl wahr. Siehe oben, so ganz verstehe ich nicht, worauf du mit der Anmerkung hinaus möchtest. Gebe ich dir das Gefühl, eine Klasse sei für mich nur eine C-Struktur?
Die Klasse "WorldDataStorage" schien irgendetwas zu speichern ("Storage") also Daten zu halten ohne selbst für die Verwaltung zuständig zu sein. Deshalb gab es ja den "Manager". Analog zu einer C-Struktur: Variablen hier, freie Funktionen dort.
Aber scheinbar sind wir uns ja eh einig man die "Manager"-Klasse nicht braucht.
smurfer
Establishment
Beiträge: 195
Registriert: 25.02.2002, 14:55

Re: Fabrikmethode -- Factory Method

Beitrag von smurfer »

Nur in aller Kürze:
Spiele Programmierer hat geschrieben:Die Klasse "WorldDataStorage" schien irgendetwas zu speichern ("Storage") also Daten zu halten ohne selbst für die Verwaltung zuständig zu sein. Deshalb gab es ja den "Manager". Analog zu einer C-Struktur: Variablen hier, freie Funktionen dort. Aber scheinbar sind wir uns ja eh einig man die "Manager"-Klasse nicht braucht.
Ah verstehe, dann ist für mich der Hinweis nachvollziehbar. Und ja, wie vorgeschlagen sollte WorldDataStorage Daten halten und auch verwalten.
Alexander hat geschrieben:Wenn verschiedene Typen verschieden behandelt werden wollen, täten es auch mehrere Schleifen über jeweils homogene Daten?!
Ja, genau. Es ist für mich einfach wieder ein Abwägen des Für und Widers, da sowieso ein gemeinsames Interface besteht und ich es so an manchen Stellen für mich vereinfachen kann. Wie schon weiter vorne im Thread beschrieben, trenne ich aber genauso in mehrere Schleifen auf, wenn die Unterschiede zu groß sind oder ein gemeinsames, womöglich umständliches Interface erst erstellt werden müsste. Darauf läuft es im Kern für mich auch beim Kollisionsbeispiel hinaus: ein Abwägen von elegantem Design gegen die für mich praktikablere Lösung mit dem diskutierten Risiko des vermeintlich schlechten oder als widersprüchlich empfundenen Designs.
Alexander hat geschrieben:Ich bin nicht ganz sicher, worüber wir dann hier diskutieren. Wenn du Code hast, der für dich funktioniert, würde ich den nicht anfassen, bevor nicht irgendein Problem auftaucht, was das nötig machen würde.
Keine Widerrede, der Thread ist ja letztlich in zwei Richtungen verlaufen. Zum einen die konkrete Diskussion über den Charakter von Downcasts bzgl. des Architekturdesigns, zum anderen die ursprüngliche, für mich eigentlich schon beantwortete Frage nach dem sinnvollsten Vorgehen und etwaigen weiteren Optionen, wenn ein WorldDataStorage nicht nur Daten verwalten, sondern auch erzeugen und nach außen bringen soll.
Antworten