Design Frage
-
Hallo,
ich habe eine kleine Designfrage zu C++.
Ich baue mir eine sehr große Klasse, die ein Problem löst. Das Problem kann sie intern auf 2 Arten lösen (einmal über eine Box und einmal über eine Sphere), wobei der Benutzer die Technik auswählen soll.
Box und Sphere Techniken haben viele Gemeinsamkeiten - die Klasse wird also viele private Methoden haben, die beide Techniken benutzen.Zur Realisierung schweben mir folgende Ideen vor:
- Die Klasse bietet als Schnittstelle einfach alle Methoden beider Techniken an, also z.B: Klasse::BoxMethode1() und Klasse::SphereMethode1() usw.
- Irgendwie intern einen Namespace eröffnen, so dass der User direkt die Techniken anspricht:
Ein Aufruf wäre dann in etwa so: Klasse::Box->Methode1() oder Klasse::Sphere->Methode1()
Allerdings weiß ich nicht, wie man das in C++ macht. (Innere Klasse?)
Oder gibt es irgend ein Pattern oder sonst ne Idee, wie man das am elegantesten machen könnte?
Danke

-
Mir fiele noch eine Möglichkeit ein, weiß aber nicht, wie gut sie bei dir reinpassen würde:
class Sonstwas { public: enum CalcModus { Box, Sphere } void DoWhateverYouHaveToDo( CalcModus modus ) { if ( modus == Box ) { } else if ( modus == Sphere ) { } else { cout << "Doofer Benutzer" << endl; } } }; Sonstwas bla; bla.WhateverYouHaveToDo( Sonstwas::Box );Ansonsten fiele mir noch Polymorphie ein, also gemeinsame Oberklasse und die Box- und Sphere-Lösung als erweiternde konkrete Subklassen.
edit2: Hänge halt sehr von den Aufgabe der Klasse(n) ab

-
Box und Sphere Techniken haben viele Gemeinsamkeiten - die Klasse wird also viele private Methoden haben, die beide Techniken benutzen.
Nimm Klasse Foo und pack alles da rein. Die Methoden, die nun eben entweder auf Sphere oder Box basieren, machst du pure virtual und implementierst später nochmal 2 Klassen, die eben von Foo ableiten und dann eben die Lösung beinhalten. So kann man sich für eine Methode entscheiden und Übergaben sind kein Problem, weil du ja immer einen Zeiger auf Foo übergeben kannst, auch wenn Methode abstrakt sind.
Beispiel:class Foo { void irgendeineMethode(); void neAndere(); virtual void SchlüsselMethode() = 0; }; class Box : public Foo { virtual void SchlüsselMethode() { SomeReallyImportantSourceCode; } }; class Sphere : public Foo { virtual void SchlüsselMethode() { SomeReallyImportantSourceCode; } };rya.
-
Das hilft mir nicht viel, weil wie gesagt jede Technik einige eigene Methoden besitzt, die für den Benutzer zugreifbar sein sollen.
Polymorphie möchte ich eigentlich nicht benutzen, da ich im Hauptprogramm nur ein einziges Objekt haben möchte. Bei Polymorphie müsste ich 2 Instanzen anlegen, da die Box und Sphere Techniken unterschiedliche Methoden haben.
-
Wann wird denn entschieden, welche Technik verwendet wird? Zur Kompilierzeit oder entscheidet das der User anhand von Programmoptionen?
rya.
-
Wenn zur Kompilierzeit entschieden wird helfen Policies.
-
EinGast schrieb:
...
Polymorphie möchte ich eigentlich nicht benutzen, da ich im Hauptprogramm nur ein einziges Objekt haben möchte. Bei Polymorphie müsste ich 2 Instanzen anlegen, da die Box und Sphere Techniken unterschiedliche Methoden haben.
Ähhh, dafür brauchst Du doch keine 2 Instanzen ? Nur 2 Klassen (ist ja klar; irgendwo musst Du ja die beiden "Techniken" implementieren) ....
Weder bei Compilezeit-(templates) noch Laufzeitpolymorphie (virtual functions).....Ich vermute, dass Du da etwas mißverstanden hast.
Gruß,
Simon2.
-
EinGast schrieb:
Das hilft mir nicht viel, weil wie gesagt jede Technik einige eigene Methoden besitzt, die für den Benutzer zugreifbar sein sollen.
Definiere bitte "fuer den Benutzer zugreifbar" - schwebt dir sowas wie ne kommandozeile vor wo der Benutzer je nach Anwendugn verschiedene Befehle eingeben kann?
-
würde auf das Strategy-Pattern zurückgreifen
oder Strategy gemixed mit Factory, kann man ganz gut kombinieren
-
EinGast schrieb:
...Ich baue mir eine sehr große Klasse, die ein Problem löst...
EinGast schrieb:
...Polymorphie möchte ich eigentlich nicht benutzen, da ich im Hauptprogramm nur ein einziges Objekt haben möchte...
Ich glaube eher das du dein Design gravierend ändern solltest. Zum einen sollten Klassen immer überschaubar bleiben (Ich kämpfe schon genug mit "Gottklassen" herum um zu wissen wovon ich rede und warum ich sie selbst vermeide). Zum anderen verstehe ich nicht warum du wirklich nur ein Objekt haben willst wenn es zwei Fälle zu geben scheint. Mit Objektorientierung hat das jedenfalls nichts mehr zu tun.
Vielleicht wäre es hilfreich mehr zu erfahren was du eigentlich vorhast.
Wenn ich nach deinen ersten Post gehe interessiert mich auch ob die Methoden (Klasse::Box->Methode1() / Klasse::Sphere->Methode1()...) im gleichen Anwendungsfall (nur mit unterschiedlichen Verhalten) oder gänzlich unterschiedlich verwendet werden.
Zudem gehe ich nach deinen bisherigen Beschreibungen davon aus das du die Objektorientierte Programmierung noch nicht wirklich verstanden hast, vielleicht kann man weiterhelfen wenn man mehr weiß.
cu André
-
Ich denke ich hab die OOP recht gut verstanden. Es geht hier auch nicht direkt um die OOP, sondern um Design mittels OOP. Trotzdem Danke der Nachfrage

Im Moment habe ich 2 Klassen (Box und Sphere), die nahezu identisch sind, jedoch teilweise unterschiedliche Methoden haben (Sphere hat sowas wie getRadius() etc, was bei ner Box keinen Sinn macht und umgekehrt).
Im Hauptprogramm hab ich also nun von jeder Klasse eine Instanz (Box *b und Sphere* s) und über ein GUI kann der Benutzer die Render Technik (Box oder Sphere) wechseln.
Pseudomäßig sieht es im Hauptprogramm also so aus:if(technique == box) b->render(); else if(technique == sphere) s->render();und wenn ich irgend ein Attribut änderen will, das für beide Techniken gilt, mach ich das im Moment immer doppelt:
if(neueFarbe) { b->setFarbe(farbe); s->setFarbe(farbe); }Nur finde ich diese ständige "Doppelbehandlung" im Code unschön und ich möchte das Kapseln.
Wenn ich jedoch 2 eigene Klassen habe, dann brauch ich im Programm ja wieder 2 Instanzen und gewinne nichts:
class Base { ... }; class Box : Base { } class Sphere : Base { } // Im Hauptprogramm dann: Box* b = new Box; Sphere* s = new Sphere;Wenn ich eine Boxspezifische Methode aufruf, brauch ich ja einen Box Zeiger und dito für Sphere.
Am einfachsten wäre wohl wirklich eine Superklasse und dann innerhalb von Superklasse::Render(): if(box) boxrender() else sphererender()
Das einzig unschöne wäre eben, dass die Klasse sowohl BoxMethoden als auch SphereMethoden hätte.
-
Naja, dann machs doch so, wie ichs dir beschrieben habe...
Mach ein Interface für beide und mach die Funtionen des Interfaces allgemeiner, damit beim Aufruf nicht mehr zwischen Radius und Fläche oder so unterschieden werden muss. Schreib daraus 2 Klassen.
Und in deinem Programm reicht dann ein simples:IBasisKlasse *Foo; onChangeTechnique(int technique) { if (Foo) delete Foo; if (technique == BOX) Foo = new Box; else if (technique == SPHERE) Foo = new Sphere; else error; }Resultat: Du musst nur einen Zeiger auf das Interface behalten und kannst den Rest kicken. Die 2x-Behandlung fällt damit weg.
rya.
-
Hm ja, also eigentlich ist die Lösung wirklich elegant. Das Problem ist nur, dass Box und Sphere eine sehr aufwändige Initialisierung haben (sehr rechenintensiver Algorithmus). Die Arbeit damit sieht also in etwa so aus:
Box* b = new Box;
b->setGanzVieleParameter()
...
b->calculate() // aufwändige Methode, die paar Sekunden benötigt
// ab jetzt kann ich damit arbeiten// dito für Sphere
Ich kann also leider nicht beim Wechseln ständig die Instanzen löschen und neue anlegen.
Aber auch dann wäre die Lösung noch nicht perfekt, da ich ja beim Setzen einer (zB) Boxspezifischen Methode einen Downcast bräuchte:
(dynamic_cast<Box*>(Foo))->setzeKantenLänge();Auch nicht so das Wahre.

-
Bleibt immernoch die Frage, ob der User von aussen vor dem Bildschirm Zugriff auf die spezifischen Methoden haben muss (z.B. Box::getRadius() ).
Normalerweise macht man es wie folgt:class BaseRenderer{ public: virtual void render() = 0; //abstrakte Methode, muss jede Kindklasse selbst wissen wie sie das macht void setFarbe(Farbe f) {farbe = f}; //naja, das ist ueberall gleich private: Farbe farbe; }; class Sphere : public BaseRenderer { public: void render() { /*hier steht wie die Sphere rendert*/ int v = getRadius(); /*und rechnet mit ihren speziellen Eigenschaften*/ }; private: int getRadius(); }; class Box : public BaseRenderer { public: void render() {/*hier steht wie die Box rendert*/}; }; int main() { BaseRenderer* pbr; if (User_Will_Box) pbr = new Box(); else pbr = new Sphere(); pbr->setFarbe(GRUEN); //das ist einfach pbr->render(); //klappt auch super //Wenn man denn doch unbedingt mal wissen will was man eigentlich hat Sphere* ps = dynamic_cast<Sphere*>(pbr); if(ps) {cout << "Es ist eine Sphere, sie hat den Radius " << ps->getRadius() << endl; else {cout << "Es ist eine Box, sowas hat keinen Radius!" << endl; } }Soweit doch ganz einfach. Wenn du OOP so gut verstanden hast, dann sollte Polymorphie doch eigentlich kein Fremdwort mehr sein oder?
/edit ich seh schon, war mal wieder zu spaet dran.
-
Und wenn du beide Objekte beim Start anlegst und einen globalen Zeiger (oder innerhalb der Klasse is ja egal) nimmst?
Und beim Wechsel wechselst du nur den Zeiger...
Zur Verdeutlichung:IBase* Foo = NULL; Box *g_Box = new Box(); Sphere *g_Sphere = new Sphere(); void onChange() { Foo = g_Box; // oder Foo = g_Sphere; }Damit entfällt das initialisieren auf den Programmstart und du kannst jederzeit wechseln.
Es wird zwar der Speicher für beide Objekte verbraucht, aber du solltest diese doppelten Aufrufe nicht mehr brauchen.edit:
Aber auch dann wäre die Lösung noch nicht perfekt, da ich ja beim Setzen einer (zB) Boxspezifischen Methode einen Downcast bräuchte:
(dynamic_cast<Box*>(Foo))->setzeKantenLänge();Es spricht ja nix dagegen, dass die Klasse Box so eine Methode enthält. Schliesslich implementierst du ja nur eine abstrakte Klasse. Die kann ja noch mehr Methoden enthalten.
rya.
-
1. Klar braucht der Benutzer Zugriff auf spezifische Methoden
2. Gleiches Problem wie oben. Wenn ich während die Anwendung läuft die Technik wechseln will, müsste ich das alte Objekt löschen und eine Instanz der jeweils anderen Klasse erstellen. Das geht jedoch aufgrund der aufwändigen Initialisierung nicht.
-
Wenn du wirklich Box und Sphere parallel haben willst, kommst du wohl um zwei Klassen nicht herum, denn eine Monolithische Superklasse ist so gut wie immer Mist. Ich wuerde sogar ganze 4 (!) Klassen vorschlagen:
- Eine Interfaceklasse ("RenderInterface"), die gemeinsame Methoden (wie render()) deklariert und von der deine Box und deine Sphere ableiten.
- Eine Klasse, ("RenderData") die Daten enthaelt, die unabhaengig von der Art der Rendererklasse sind (z.B. die Farbe udn damit auch die Methode SetFarbe())
- Die Box leitet von RenderInterface ab und haelt einen Zeiger auf RenderData
- Dito fuer Sphere.class RenderInterface{ public: virtual void render() = 0; //abstrakte Methode, muss jede Kindklasse selbst wissen wie sie das macht RenderData* getData() {return myData;} private: RenderData* myData; }; class RenderData { void setFarbe(Farbe f) {farbe = f}; //naja, das ist ueberall gleich private: Farbe farbe; }; class Sphere : public BaseRenderer { public: void render() { /*hier steht wie die Sphere rendert*/ int v = getRadius(); /*und rechnet mit ihren speziellen Eigenschaften*/ }; private: int getRadius(); }; class Box : public BaseRenderer { public: void render() {/*hier steht wie die Box rendert*/}; }; int main() { RenderData rd; Box b(&rd); Sphere s(&rd); Sphere.getData()->setColor(GRUEN); //alles gruen Box.getData()->setColor(ROT); //na gut dann eben rot rd.setColor(ROT); //war doch schon rot! if (technique == SPHERE) s.render(); else b.render(); int v = b.getRadius(); }Wenn mans ganz gewitzt machen moechte, implementiert man fuer RenderData referenzzaehlung, und fuer die Renderklassen einen standardkonstruktor, der ein neues Datenobjekt erzeugt und einen konstruktor, der von einem anderen Renderobjekt dessen Datenobjekt uebernimmt.
Box b; //legt sich ein eigenes Datenobjekt an Sphere s(b); //hat jetzt das selbe Datenobjekt wie b Sphere s2; //legt ein zweites Datenobjekt an Box b2(s2); //teilt sich das Datenobjekt mit s2
-
Hehe, vielen Dank für eure Lösungstipps.

Ich werde es jetzt wohl so machen wie Scorcher24 es vorgeschlagen hat.Aber da ich nach wie vor (wie jetzt) 2 Instanzen habe, ändere ich um Grunde garnicht so viel. Im Grunde habe ich dann bei BaseRenderer->Render() nach wie vor ein if(), nur dass es eben von der vtable ausgeführt wird;)
Aber alleine die Vermeidung von doppeltem Code ist es wert es so zu machen.
-
Na, das "if durch die vtable" ist doch grade der witz an der vtable bzw. an Polymorphie allgemein!