Akzeptanz unterschiedlicher Proxys für Klasse
-
Hi,
okay, das gehört irgendwie in einen eigenen Thread, finde ich. Mittlerweile bin ich ja brav und spar mir das Vererben eines Proxys und seiner Bezugsklasse von einem gemeinsamen Interface.
Trotzdem bleibt das Problem, dass ich eine UI-Klasse habe, die mit beidem umgehen können soll. Die würde damit zum Template werden, jedoch soll man bei einer bestehenden Instanz das jeweilige Objekt auswechseln können.
class SomeClass { public: void doThis(); void doThat(); }; class SomeClassProxy { public: SomeClassProxy(SomeClass& someClass) : someClass(someClass) {} void doThis() { // ... someClass.doThis(); } void doThat() { // ... someClass.doThat(); } private: SomeClass& someClass; }; class SomeUIClass { private: SomeClassOrSomeClassProxy* theThing; // ... };
Tja, wie gewährleiste ich das? Ich habe schon sämtliche Idiome hier durchgelesen, aber keines scheint zu passen. Die einzige Lösung, die mir in den Sinn kommt ist wieder eine Zwischenklasse.
class SomeClassOccurence { private: SomeClass* someClass; SomeClassProxy* someClassProxy; public: SomeClassOccurence() : someClass(nullptr), someClassProxy(nullptr) {} SomeClassOccurence(SomeClass* someClass) : someClass(someClass), someClassProxy(nullptr) {} SomeClassOccurence(SomeClassProxy* someClass) : someClassProxy(someClassProxy), someClass(nullptr) {} };
Doch wie erreiche ich dann den Zugriff? Bei jedem Aufruf eine if-Abfrage machen, ist ja echt doof. Ich könnte bei der Occurence höchstens nochmal das ganze "Interface" hernehmen und alle Aufrufe an die jeweils korrekte Klasse weiterleiten. Skaliert natürlich unglaublich öde mit jedem weiteren Proxy
Wie löse ich das jetzt wieder?
Edit:
Eigentlich kann das ja gar nicht gehen. Den Proxy ohne Interface zu realisieren ist doch dann totaler MüllSo schwierig kann das doch nicht zu realisieren sein, das ist doch nicht das Designproblem des Jahrhunderts. Oder haltet ihr meine Anforderung für bescheuert? Ich finde die total legitim...
-
Ähm, du fügst zum Proxy ein operator SomeClass hinzu und SomeUIClass nimmt einfach nur SomeClass?!
-
Das erzielt doch nicht, was ich möchte. Alle Zugriffe, die in der UI-Class geschehen, sollen natürlich im Proxy-Falle auch darüber laufen.
-
Das heißt, SomeProxy ist eher eine Art Adapter?
Dann machst dus halt umgekehrt.
-
Ne, Adapter hat damit gar nichts zu tun, soweit ich das Pattern verstehe.
Es ist eigentlich ja auch völlig logisch und im Namen drin: Proxy ist ein Stellvertreter. D.h. wenn jemand SomeClass will, sollte man stattdessen auch SomeClassProxy, also seinen Stellvertreter hinschicken können.
Der Stellvertreter ist in diesem Fall jemand, der Befehle ein wenig abgeändert nutzt. Wir können der Einfachheit aber auch sagen, dass er protokolliert, während das SomeClass nicht macht - ansonsten werden die Aufrufe weitergeleitet.
Die Anforderung ist somit, dass SomeUIClass sowohl SomeClass als auch SomeProxyClass akzeptieren soll. Und wenn ich dem SomeClass gebe, sollen auch die Methoden von SomeClass aufgerufen werden, bei SomeProxyClass dessen Methoden (daher ist operator SomeProxyClass() in SomeClass ein schwerwiegender Designfehler, SomeClass kennt SomeClassProxy ja gar nicht und soll ihn auch nicht kennen).
-
class Logger { // oder was das auch machen soll virtual void logDoThis() {} // sehr wahrscheinlich braucht es nur virtual void logThat() {} // eine einzige Methode. Dann nimm aber // lieber std::function }; class RealLogger { virtual void logDoThis() { ... } virtual void logThat() { ... } }; struct LoggedSomeClass { unique_ptr<SomeClass> s; unique_ptr<Logger> l; void doThis() { l->logThis(); s.doThis(); } void doThis() { l->logThat(); s.doThat(); } };
-
Da kann ich immer noch nicht an eine SomeUIClass SomeClass oder LoggedSomeClass übergeben. Je nachdem, was ich übergebe, soll SomeUIClass loggen oder nicht.
class SomeClass { public: void doThis() {...} }; class SomeClassProxy { public: SomeClassProxy(SomeClass& someClass) : someClass(&someClass) {} void doThis() { // mach irgendwas, loggen oder was anderes someClass.doThis(); } }; class SomeUIClass { private: /* ? /* someClassOrProxy; public: void SetSomeClassOrProxy(/* ? */); void doSomething() { // ... someClassOrProxy->doThis(); // ... } };
Je nachdem, ob Proxy oder nicht an SetSomeClassOrProxy übergeben wird, soll er in doSomething die doThis-Methode von eben dem Proxy oder eben dem Nicht-Proxy aufgerufen werden.
(klarer Fall für Polymorphie eigentlich, aber ich darf ja nicht erben, also wie löse ich das jetzt bitte?)
-
Eisflamme schrieb:
Da kann ich immer noch nicht an eine SomeUIClass SomeClass oder LoggedSomeClass übergeben.
SomeClass = LoggedSomeClass mit l = new Logger(), dem Nulllogger
-
Okay, ich versuche es Mal anders zu erklären...
SomeClass soll nicht geändert werden dadurch, dass es einen SomeClassProxy braucht. SomeClassProxy fügt Funktionen hinzu, die aber heterogener sind als ein einfacher Logger.
Würde ich SomeClass dadurch erweiterbar machen, dass ich dort für jede Funktion einen vector von std::function machen würde, damit das manipulierbar ist, hätte ich ein unglaubliches Gefrickel, um meinen "Proxy" damit zu simulieren. Ich glaube nicht, dass Du das wirklich empfehlen willst.
Überhaupt sehe ich keine Nicht-Frickel-Lösung, die intrusiv ist, d.h. SomeClass soll bestehen bleiben und SomeClassProxy soll Funktionen hinzufügen bzw. Aufrufe abändern (Proxy eben), letztendlich aber ein bereits bestehendes SomeClass-Objekt damit manipulieren. Ist ja auch ein Stellvertreter für ein bestehendes Objekt.
Und eine Klasse X soll jetzt SomeClass bzw. SomeClassProxy als Parameter erhalten, nicht wissen, was es genau ist (type erased), und einfach brav die Methoden aufrufen ohne Wissen darüber, wie die Implementierung jetzt konkret aussieht. D.h. meinetwegen kann die Klasse das auch wissen, mir doch egal, aber ich will eben möglichst elegant beides übergeben können.
-
theThing wird zur Laufzeit festgelegt, oder?
-
Genau.
-
Und wieso kannst du nicht nochmal Proxy und Nicht-Proxy die selbe Basisklasse spendieren und in SomeUIClass diese verwenden?
Weil das Proxy nicht zwingend das ganze Interface implementieren muss, sondern manchmal auch nur einen kleinen Satz aus Funktionen von Nicht-Proxy übernimmt!?
-
Weil facepalm das als schlechten Stil angepriesen hat und mich vorläufig auch von seiner Lösung überzeugt hatte - die jetzt jedoch ziemlich unumsetzbar erscheint, wenn man SomeClass und SomeClassProxy so austauschbar nutzen möchte, wie man es eigentlich von einem Proxy gewohnt ist.
Seine Lösung ist halt irgendwie ein CompileTimeProxy. Ich brauche einen RuntimeProxy. Aber dieses Bedürfnis kann ich mit C++ offensichtlich nicht umsetzen, ohne dass es java-style wird. Aber wenn meine Anforderung in Ordnung ist und es ohne Polymorphie nicht geht, muss eben das Interface drüber...
Wobei ich mit dem Interface auch wieder auf Probleme stoße. Da der Stellvertreter ja nur zeigend auf das eigentliche Objekt ist, kann ich darauf schlecht einen Zeiger hingeben, so ein Proxy ist dann ja ziemlich flüchtig, wird oft Mal temporär eingesetzt, ist kopierbar... Ich könnte ihn auch nicht-kopierbar machen, aber dann stellt sich trotzdem wieder die Frage, wer für den Besitz zuständig ist. Ach ich komm gedanklich heute auf keinen grünen Zweig
-
- Kann es eine Proxy-Kette geben? Also P->P->P->NP
- Ist hinterm (letzten) P immer ein NP
- Kann ein P mit einem anderen P ausgetauscht werden
- Müssen die P's dann das selbe NP haben
- ...
-
- Kette: Theoretisch ja, praktisch zurzeit aber nicht notwendig
- Kette endet definitiv mit NP, genau
- da es nur ein P gibt, kann natürlich ein P mit einem anderen ausgetauscht werden; bzw. wenn es später noch einen weiteren P gibt, dann soll auch das gehen
- P dasselbe NP haben verstehe ich nicht ganz, ein Proxy hat ja entweder einen Verweis auf einen anderen Proxy oder auf einen Nicht-Proxy, somit ist klar, dass eine Kette immer auch nur einen NP haben kann
-
Kannst du mir zeigen, wie das hier nicht geht?
class Logger { // oder was das auch machen soll virtual void logDoThis() {} // sehr wahrscheinlich braucht es nur virtual void logThat() {} // eine einzige Methode. Dann nimm aber // lieber std::function }; class RealLogger { virtual void logDoThis() { ... } virtual void logThat() { ... } }; struct LoggedSomeClass { unique_ptr<SomeClass> s; unique_ptr<Logger> l; void doThis() { l->logThis(); s.doThis(); } void doThis() { l->logThat(); s.doThat(); } }; class SomeUIClass { private: unique_ptr<LoggedSomeClass> someClassOrProxy; public: void SetSomeClassOrProxy(unique_ptr<SomeClass> s) { someClassOrProxy.reset(new LoggedSomeClass{move(s), new Logger()}); } void SetSomeClassOrProxy(unique_ptr<LoggedSomeClass> s) { someClassOrProxy = move(s); } void doSomething() { // ... someClassOrProxy->doThis(); // ... } };
Proxyketten lassen sich durch eine Klasse
KettenLogger
abbilden.
-
Ah, ich habe
class RealLogger : public Logger {
vergessen, daher das Missverständnis.
-
Ah okay, das klärt zumindest.
Na ja und sonst ist das mit dem Logger halt eine Vereinfachung von mir gewesen, die nicht so zutrifft.
class SomeClass { public: void meth1(int a); bool meth2(); void meth3(int a, int b); }; class SomeClassTreeProxy { public: SomeClassProxy(SomeClass& someClass, Tree& tree) : /* ... */ void meth1(int a) { // hier wird a vor Weitergabe z.B. geändert a += 10; someClass.meth1(a); } bool meth2() { bool result = someClass.meth2(); // hier wird das Resultat nochmal anhand der Nachbarn/Eltern im Baum geändert return result & /* irgendwelche Abfragen */; } void meth3(int a, int b) { // hier geschieht wiederum etwas vom Baum Abhängiges a -= 5; b *= 2; // beispielhaft natürlich someClass.meth3(a, b); } void meth4() { // das ist sogar eine Zusatzmethode; wer den Proxy nutzt, // hat das noch zusätzlich, weil SomeClass im Baum-Kontext // steht } };
Das ist jetzt wieder abstrahiert und ich hoffe, es wird einfach klar, dass der Proxy nicht einfach NUR filtert oder NUR loggt oder NUR dies und das macht, sondern als Proxy für eine SomeClass dient, die sich im Baumkontext befindet.
Im Baum selbst ist SomeClass abgelegt, wenn man vom Baum aber SomeClass erhalten möchte, erhält man den SomeClassTreeProxy, damit der Proxy brav berücksichtigt, dass SomeClass in einem Baum eben anders agiert.
Erscheint vielleicht erstmal nicht intuitiv, funktioniert aber (mit Polymorphie zumindest) sauber: Wenn ich über den Tree an eine Range gehe und das Resultat jemandem übergebe, wird sichergestellt, dass alle Einfügeoperationen unter Berücksichtigung des Baumes erfolgen.
Dadurch sollte jetzt auch verständlich werden, wieso ich nicht einfach einen std::vectorstd::function einbauen kann, der Mal brav mit einer manipulate(...)-Methode aufgerufen wird. Und SomeClass soll eben auch außerhalb des Baumes verwendbar sein - dann aber ohne Filter und Abwandlungen und Extraprüfungen usw. Würde ich jetzt jede Art von Zusatz rausnehmen und extra einbauen wie beispielsweise extra std::function für Filter, extra std::function für Prüfungen des Rückgabewertes usw., dann hätte ich ziemlich viel Gedöns drin - was auch einfach kein Mensch braucht, der nur SomeClass nutzt. Daher der ganze Proxy.
Hilft das? Sorry für den ganzen Text...
Wenn's hilft, kann ich noch konkreter werden.
-
Na ja, wenn jetzt einfach nichts mehr kommt, löse ich es halt über das Interface. Interessiert mich nicht, ob irgendjemand dazu kommentiert, dass es "javalike" wäre. Dynamische Polymorphie löst man eben nicht über Templates. Und manchmal braucht man dynamische Polymorphie eben - auch in C++.
Jedenfalls komme ich nicht mit Lösungen weiter, die sich darauf fokussieren Interfaces und Polymorphie abzuschaffen statt mein Problem mit allen Anforderungen zu lösen...
Edit:
Den Rest habe ich Mal wegeditiert, hat leider den falschen Ton angenommen, Entschuldigung dafür. Danke an hustbaers nachfolgende Antwort.
-
Eisflamme schrieb:
Na ja, wenn jetzt einfach nichts mehr kommt, löse ich es halt über das Interface. Interessiert mich nicht, ob irgendein Praxisferner dazu kommentiert, dass es "javalike" wäre, es gibt einfach keine "c++-like" Implementierung, die meine (validen und total natürlichen) Anforderungen akzeptiert.
C++ ist eine multi-paradigm Sprache, und Runtime-Polymorphie über Basisklassen mit virtuellen Funktionen ist "perfectly C++-like".
Ein pöser Javaismus wäre alle solchen Klassen auf Krampf mit I zu prefixen und *nur* aus virtual-pure Funktionen bestehen zu lassen. Wenn eine Funktion nie überschrieben werden muss oder sogar darf, dann soll die auch nicht virtual sein. Ein weitere pöser Javaismus wäre einfach überall mit polymorphen Basisklassen draufzuhauen, auch wenn es in C++ viel elegantere Möglichkeiten dafür gibt. Nur so einen Fall hast du ja nicht. Es gibt keine elegantere Möglichkeit. Zumindest keine mir bekannte.