Hallo,
ich habe mal eine Frage zu C++. Oder eigentlich eine allgemeinere Frage,
wie man ein Projekt halbwegs gebrauchbar objektorientiert strukturiert,
ohne dass es auf Dauer eklig wird. Die Grundlagen von Objektorientierung
kann ich, aber bei den Feinheiten wird's dünn. Daher frage ich hier.
Mir geht es nicht um Salamitaktik, der Hintergrund steht unten.
Gegeben ist eine zweistufige[1] C++-API, bestehend aus einem
HidlProvider-Objekt, welches mehrere HidlDevice-Objekte bereitstellt. Da
die HidlDevice-Objekte ziemlich eklige Datenstrukturen verwenden, hätte
ich gerne eine Abstraktion dazwischen.
[1] Eigentlich dreistufig, aber die dritte Ebene ist erstmal
uninteressant.
Erste Frage:
Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und
meine ganzen Devices davon erben. Die HidlDevice-Klasse implementiert
dann die externe API (die Methoden müssen also public sein). Wie kann
ich nun verhindern, dass meine Device-Klassen diese Methoden
überschreiben oder aufrufen können? Geht das in C++ überhaupt?
Zweite Frage:
Wenn ich das so mache, dann muss ich im HidlProvider die jeweils
konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber
unterschiedliche und unterschiedlich viele Devices, daher hätte ich
gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet. Mit
Makros ginge das, aber wie macht man das sinnvoll in C++?
Ich möchte das folgende vermeiden und hätte stattdessen gern eine
Schleife:
1
sp<HidlDevice>dev0=TypeZeroDevice();
2
if(dev0->isReady()){mDevices.push_back(dev0);}
3
sp<HidlDevice>dev1=TypeOneDevice();
4
if(dev0->isReady()){mDevices.push_back(dev1);}
5
// ...
Dritte Frage:
Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?
Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die
einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die
konkrete Klasse instantiieren...
Hintergrund: Es geht um eine HIDL-Schnittstelle in Android, konkret
CameraProvider, CameraDevice und CameraSession (sowie die jeweiligen
ICamera* und ICamera*Callback-Klassen). Die API ist komplex und
verwendet unhandliche, serialisierte Datenstrukturen, daher hätte ich
gerne eine eigene Abstraktion dazwischen. Hinten fällt ein einzelnes
Binary raus, welches den Provider, alle Devices und
Wichtig ist mir, dass die einzelnen Devices möglichst wenig API-Code und
Boilerplate beinhalten. Im Prinzip will ich den ganzen API-Layer mit dem
nötigen Boilerplate einmal implementieren, aber dann getrennte
Implementationen mit den konkreten Details separat haben. Am liebsten in
einer Shared Library pro Device (wie das dann mit CameraSession läuft,
weiß ich noch nicht; da passiert im Prinzip das gleiche umgekehrt - die
getrennten Implementationen laufen wieder auf eine API-Implementation
zusammen).
Aus meiner Sicht fällt das unter "advanced object orientation", und da
fehlt mir Erfahrung. Daher frage ich hier.
Gruß,
Svenska
S. R. schrieb:> Hallo,>> ich habe mal eine Frage zu C++. Oder eigentlich eine allgemeinere Frage,> wie man ein Projekt halbwegs gebrauchbar objektorientiert strukturiert,> ohne dass es auf Dauer eklig wird.
Das fragen sich sehr viele. ;-)
> Wie kann ich nun verhindern, dass meine Device-Klassen diese Methoden> überschreiben oder aufrufen können? Geht das in C++ überhaupt?
Überschreiben können sie nur, was in der Basisklasse virtual definiert
wurde. Einen Aufruf kann man nicht verhindern. Aber wozu? Denkst du,
dass so viele versuchen werden, deinen Code mit solchen Aufrufen zu
sabotieren?
> Zweite Frage:> Wenn ich das so mache, dann muss ich im HidlProvider die jeweils> konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber> unterschiedliche und unterschiedlich viele Devices, daher hätte ich> gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet.
Wenn du Polymorphie nutzt, ist der Typ automatisch dabei (RTTI).
> Dritte Frage:> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die> konkrete Klasse instantiieren...
Du speicherst in deiner Liste nur Zeiger auf die abstrakte Basisklasse.
Die Instanziierung übernimmt eine Factory.
S. R. schrieb:> Erste Frage:> Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und> meine ganzen Devices davon erben.
Vererbung sollte nur benutzt werden, wo wirklich nötig. In C++ wird sie
häufig missbraucht, um nicht "nochmal" die ganzen Methoden deklarieren
zu müssen. Siehe auch Composition (Has-A) vs. Inheritance (Is-A).
> Die HidlDevice-Klasse implementiert> dann die externe API (die Methoden müssen also public sein). Wie kann> ich nun verhindern, dass meine Device-Klassen diese Methoden> überschreiben oder aufrufen können? Geht das in C++ überhaupt?
Also Composition nicht Inheritance. (Private Inheritance kann in sehr
seltenen Fällen auch ein Lösung sein.)
> Zweite Frage:
Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus? Häufig
kommt man mit Templates weiter.
> Dritte Frage:> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die> konkrete Klasse instantiieren...
Hört sich aber nach einer Factory an, was du hier suchst.
https://en.wikipedia.org/wiki/Factory_method_pattern
S. R. schrieb:> Erste Frage:> Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und> meine ganzen Devices davon erben. Die HidlDevice-Klasse implementiert> dann die externe API (die Methoden müssen also public sein). Wie kann> ich nun verhindern, dass meine Device-Klassen diese Methoden> überschreiben oder aufrufen können? Geht das in C++ überhaupt?
Das Interface sollte non-virtual sein (NVI Idiom) und ist damit nicht
überschreibbar. Aufrufen lässt sich nicht vermeiden... aber wozu auch?
> Zweite Frage:> Wenn ich das so mache, dann muss ich im HidlProvider die jeweils> konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber> unterschiedliche und unterschiedlich viele Devices, daher hätte ich> gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet. Mit> Makros ginge das, aber wie macht man das sinnvoll in C++?
Du kannst die Devices innerhalb des HidlProviders ja als std::unique_ptr
in irgendeinem Container speichern? Zum Beispiel in einer std::map.
Damit wäre dann auch eine Namensgebung möglich.
> Dritte Frage:> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die> konkrete Klasse instantiieren...
In dem die Devices bereits außerhalb des HidlProviders erzeugt werden.
Der HidlProvider bekommt nur noch fertige Devices, bzw. einen Container
mit Devices. Je nach Komplexität kann man dafür schlichtweg Constructors
verwenden oder eben die bereits vorgeschlagene Factory.
Rolf M. schrieb:>> Wie kann ich nun verhindern, dass meine Device-Klassen diese Methoden>> überschreiben oder aufrufen können? Geht das in C++ überhaupt?>> Überschreiben können sie nur, was in der Basisklasse virtual definiert> wurde. Einen Aufruf kann man nicht verhindern. Aber wozu? Denkst du,> dass so viele versuchen werden, deinen Code mit solchen Aufrufen zu> sabotieren?
Zumindest mein zukünftiges Ich würde dazugehören... :-)
Ein Design, was man prinzipiell nicht falsch verwenden kann ist mir
trotzdem lieber.
Außerdem mag ich es nicht, wenn in einer Klasse irgendwelche Methoden
rumliegen, die für die eigentliche Klasse nicht relevant sind. Das
Device hat sich nicht dafür zu interessieren, welche Methoden das
HidlDevice implementieren muss.
Rolf M. schrieb:> Die Instanziierung übernimmt eine Factory.
Hmm. Klingt sinnvoll, muss ich mal drüber grübeln.
Mikro 7. schrieb:> Also Composition nicht Inheritance.> (Private Inheritance kann in sehr seltenen Fällen auch ein Lösung sein.)
Klingt auch sinnvoll. Muss ich mich mal einlesen, wie sowas
funktioniert.
Mikro 7. schrieb:>> Zweite Frage:> Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus?> Häufig kommt man mit Templates weiter.
Aus der Hüfte, nicht implementiert:
1
#define MAKE_DEVICE(NAME, TYPE) do { \
2
sp<HidlDevice> dev = TYPE(); \
3
if(dev->isReady() { \
4
mDeviceNames.push_back(NAME); \
5
mDevices[NAME] = dev; \
6
} \
7
} while(0);
8
9
HidlProvider::HidlProvider(){
10
MAKE_DEVICE("dev0",TypeZeroDevice);
11
}
Das Grundproblem ist: "Wie iteriere ich über eine Liste verschiedener
Typen?"
Vincent H. schrieb:> Das Interface sollte non-virtual sein (NVI Idiom) und ist damit nicht> überschreibbar. Aufrufen lässt sich nicht vermeiden... aber wozu auch?
Idiotie. Ich kenne die vorhandene Codebasis und darin zu arbeiten ist...
sehr wenig angenehm. Extrem viel Boilerplate, subtile
Verhaltensabhängigkeiten zwischen den verschiedenen Teilen, etc.
Vincent H. schrieb:> Du kannst die Devices innerhalb des HidlProviders ja als std::unique_ptr> in irgendeinem Container speichern? Zum Beispiel in einer std::map.
Das mache ich auch, für die instantiierten Devices. Aber die Liste der
zu instantiierenden Devices kann ich da nicht drin speichern, weil die
existiert ja zu der Zeit nicht.
Vincent H. schrieb:> In dem die Devices bereits außerhalb des HidlProviders erzeugt werden.
Hmm, das wird schwierig. Außen um den HidlProvider herum gibt es
eigentlich keine Struktur mehr. Allerdings könnte er ja die Factory
instantiieren und die dann laufen lassen... ich denke mal drüber nach.
Erstmal danke für die Anregungen.
S. R. schrieb:> Mikro 7. schrieb:>> Also Composition nicht Inheritance.>> (Private Inheritance kann in sehr seltenen Fällen auch ein Lösung sein.)>> Klingt auch sinnvoll. Muss ich mich mal einlesen, wie sowas> funktioniert.
So wie ich es verstanden habe, hast du Devices, die alle recht ähnlich
sind.
Die gemeinsame Funktion kommt dann in die "Core" class (quasi ein Lib).
Die Schnittstelle, die die Devices zur Verfügung stellen ist das
(abstrakte) "Interface". Die Implementierung eines "Zero" Devices wäre
dann etwas wie
1
structZero:Interface
2
{
3
// ... implement Interface methods ...
4
private:
5
Corecore;// as member -- not inherited!
6
};
> Mikro 7. schrieb:>>> Zweite Frage:>> Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus?>> Häufig kommt man mit Templates weiter.>> Aus der Hüfte, nicht implementiert:
Wo siehst du das Problem, das "nur" durch ein Makro gelöst werden kann?
In der Zeile: TYPE()? Das kann man doch direkt als Methode schreiben.
Ggf. mit TYPE als Template argument (wobei ich das Gefühl habe, hier
gehört iwo eine Factory rein).
> Das Grundproblem ist: "Wie iteriere ich über eine Liste verschiedener> Typen?"
Da fehlt mir jetzt der Zusammenhang. Aber grundsätzlich: Entweder sie
haben alle eine gemeinsame Basisklasse; oder du benutzt etwas wie
boost::variant oder boost::any; oder...
Ich hab das Problem noch nicht verstanden. Devices besitzen alle die
selbe API, bzw. das selbe Interface, aber keine gemeinsame Basis? Dann
kann dein Macro mit dem Array "mDevices" aber auch nicht funktionieren?
(mit Standard C++ ohne jegliches Magic Unicorn TMP zumindest nicht)
mh schrieb:> S. R. schrieb:> aus deinem>> (Makrosalat)> Solltest du ganz schnell so etwas wie>> (Templatesalat)> machen.
Da hast du natürlich recht, aber solche Makros kann ich aus der Hüfte
schießen, Templates nicht. :-) Aber in beiden Fällen kann ich nicht über
eine Liste iterieren, daher löst das nur ein Teil meines Problems.
> Über Typen iterieren macht man z.B. mit Typ-Listen.
Hmm. Das klingt alles ziemlich eklig.
Mikro 7. schrieb:> So wie ich es verstanden habe, hast du Devices,> die alle recht ähnlich sind.
Jaein. Wenn ich mehrere CameraDevices betrachte, dann unterscheiden die
sich hauptsächlich in ihren Fähigkeiten, Charakteristika (Auflösungen,
Frameraten, ...) und in den Registertabellen. Soweit nichts besonderes.
Aber dann gibt es Ausreißer: Sensoren, die andere Registerstandards
benutzen. Extern konfigurierte Sensoren (d.h. die Auflösung wird vom
Sensor vorgegeben, nicht vom System). Sensoren mit besonderen
Fähigkeiten. Sensoren, die besondere ISP-Pipelines brauchen. Sensoren,
die zwei Output-Ports haben. Und so weiter. Sensoren mit integriertem
ISP (z.B. USB). Oder im Augenblick ein Dummy-Sensor komplett ohne ISP.
:-)
Mit zunehmender Seltsamkeit der Hardware verschwinden die
Gemeinsamkeiten. In jedem Fall bleibt aber "muss die
CameraDevice/CameraSession-API implementieren" übrig, und die API stinkt
ein wenig. :-)
Composition klingt spontan nach einer guten Lösung:
Ein HidlDevice enthält ein Device (mit der eigenen Implementation) und
implementiert die API; der HidlProvider enthält nur Referenzen zu den
HidlDevices, die er aus einer Fabrik holt.
Mikro 7. schrieb:>> Das Grundproblem ist:>> "Wie iteriere ich über eine Liste verschiedener Typen?"> Da fehlt mir jetzt der Zusammenhang.
Das hat zwei Hintergründe:
Erstens: Wenn ich in meinem Gerät zwei TypeZero-Sensoren und einen
TypeTwo-Sensor habe, dann muss ich die explizit hinschreiben. Ich hätte
lieber eine externe Tabelle, wo ich für jedes Gerät angebe, welche
Sensoren es gibt (bzw. welche Treiber geladen werden müssen).
Zweitens: Warum ich die HIDL-API nicht mag ist, dass ich keine
Hashmap<Key,Value> für die Metadaten haben kann, weil jeder Key seinen
eigenen Typen hat (oft ein Enum oder Array, manchmal was ganz anderes)
und ich das in C++ nicht typsicher abbilden kann.
> Aber grundsätzlich: Entweder sie haben alle eine gemeinsame> Basisklasse; oder du benutzt etwas wie boost::variant oder> boost::any; oder...
Boost ist keine Option, und vom "sie haben alle eine gemeinsame
Basisklasse" komme ich ja gerade her, weil das andere Probleme aufbaut.
Eine andere Sprache ist übrigens auch keine Option: Eine C-API kann man
von überall benutzen, eine C++-API nicht.
Vincent H. schrieb:> Dann kann dein Macro mit dem Array "mDevices"> aber auch nicht funktionieren?
Alle Devices implementieren die gleiche Schnittstelle, aber die
Schnittstelle stinkt. Daher hätte ich gerne einen allgemeinen Wrapper
drumherumgelegt, der die internen Strukturen passend konvertiert.
Allerdings muss der Wrapper wissen, welche Devices es gibt und sie auch
erzeugen - und das ist recht schwierig, wenn der Wrapper die
Besonderheiten nicht kennen soll.
Das mDevices ist im Augenblick ein Pointer auf die Basisklasse, daher
funktioniert das...
Wie gesagt, ich hab das in meinem Kopf noch nicht ganz aufgeräumt und
nicht genug Wissen um OO-Design, um das aufzuräumen. Composition und
Factory sind schonmal gute Stichwörter.
S. R. schrieb:>> Über Typen iterieren macht man z.B. mit Typ-Listen.>> Hmm. Das klingt alles ziemlich eklig.
Ist es nicht wirklich, vielleicht 10 Zeilen mit übersichtlichen
Templates. Aber ich hab mir dein Problem nochmal angeguckt. Du willst ja
nicht über Typen iterieren, sondern über Tuple(Name,Typ). Da man
Stringliterale nicht zu "non type template parametern" machen kann, muss
man mehr Aufwand treiben. Du musst die Liste mit Namen und Typen eh
irgendwo im Quelltext haben, wo ist also das Problem mit einer "Liste"
der Form:
1
register_device<Type1>("Name1");
2
register_device<Type2>("Name2");
3
register_device<Type3>("Name3");
4
register_device<Type4>("Name4");
5
...
Wenn du die Namen direkt in die Device-Klassen auslagern kannst, wird
alles einfacher.
S. R. schrieb:> Erstens: Wenn ich in meinem Gerät zwei TypeZero-Sensoren und einen> TypeTwo-Sensor habe, dann muss ich die explizit hinschreiben. Ich hätte> lieber eine externe Tabelle, wo ich für jedes Gerät angebe, welche> Sensoren es gibt (bzw. welche Treiber geladen werden müssen).
Soll das dynamisch zur Laufzeit auf dem Gerät passieren, oder zur
Compilezeit? Ganz oben schriebst du was von „hinten fällt ein Binary
raus“.
Oliver
Oliver S. schrieb:> Soll das dynamisch zur Laufzeit auf dem Gerät passieren,> oder zur Compilezeit? Ganz oben schriebst du was von> „hinten fällt ein Binary raus“.
Eine HIDL-Implementation in Android besteht aus einem Binary, welches
die ganzen Interfaces implementiert. Das wird zusammen mit dem Rest des
Systems für ein bestimmtes Gerät gebaut, also zur Compilezeit sind alle
möglichen Treiber und IDs bekannt.
Real gibt es dann noch Varianten, die man sinnvollerweise per #ifdef
auswählt. Ich habe Code gesehen, der das mit einer Mischung aus
Device-Trees, Makefiles, im Dateisystem verstreuten Dateien und #ifdef
gemeinsam macht und würde das gerne vermeiden. (Unterschiedliche
Hersteller.) :-)
mh schrieb:> Wenn du die Namen direkt in die Device-Klassen auslagern> kannst, wird alles einfacher.
Im Augenblick habe ich im HidlProvider eine Tabelle aus (int,
string)-Paaren und iteriere über die. Der String wird dann an die
HidlDeviceFactory übergeben, und da gibt es eine if/else-Kette, die die
korrekte Klasse erzeugt.
Das reicht erstmal aus. Bisher habe ich genau null Implementationen,
daher ist der Rest erstmal nebensächlich. :-)