Hallo zusammen
Ich habe ein kleines Design Problem, und weiss nicht wie ich es am
besten lösen soll:
Sagen wir ich habe 3 Geräte, die über eine RS485 Verbindung an den PC
angeschlossen werden können. Das Kommunikationsprotokol und die
Basis-Funktionen (z.B. FW-Update) sind bei allen gleich.
Desswegen bin ich bis jetzt so vorgegangen:
Die Klasse CDevie stellt die Basis-Funktionen bereit. Man könnte es als
"allgemeines" Gerät bezeichnen.
Ob an der Schnittstelle dann auch wirklich ein solches Device
angeschlossen ist, kann ich wie folgt ermitteln:
1
// Setup Modbusmaster, Serial-Driver and Device with 1MBaud
2
if(poDevice->bInitDevice(1000000)==true)
3
{
4
// Initialize application stack
5
6
}
7
else
8
{
9
// No Device detected...
10
// Possible usage:
11
// - as true RS485 device
12
// - as Modbus Rtu device
13
}
Sobald ein Device identifiziert wurde, fängt mein Problem an:
Normalerweise würde hier über eine klassische Vererbung arbeiten, aber
da ich von meinenm Programm mehrere unterschiedliche Devices unterstütze
muss ich das reale Device erst nachträglich zur Verfügung stellen. Bei
einer Vererbung entstehen aber unterschiedliche Instanzen von
Basisklassen, was nicht sehr effizient ist.
Am Schluss könnte die Verwendung sein:
1
// Usage of Device
2
// Return values indicates the communication result and are ignored
Mir fehlt der Zwischenschritt, der die effektive Applikation
zusammenbaut anhand dessen was für ein Device angeschlossen ist. Die
Hardware deviniert ob und wie viele Sensoren, Inputs un Outputs
vorhanden.
Wie macht man so etwas normalerweise?
Oder gibt es eine bessere Lösung?
Gruss
Patrick
Wie effizient muss es sein? Erste Idee wäre, CDevice kümmert sich nur um
das Kommunikationsprotokol und gibt die Pakete an einen DataHandler
weiter.
Das if(poDevice->bInitDevice(1000000) == true) legt eine Instanz der
passenden DataHandler Klasse an.
Noch einer schrieb:> Wie effizient muss es sein?
mhm, das ganze sollte schon möglichst schlank sein (weshalb ich mehrere
Instanzen von der Basis vermeiden möchte, da pro Schnittstelle sowieso
nur 1 Device möglich ist).
Die Grundidee ist, dass ich am Schluss Plattformabhängig bin.
Theoretisch könnte ein kleiner Fitlet-PC oder etwas in der Art wie ein
Raspberry genauso gut die Verwaltung übernehmen wie ein PC. Oder ein
etwas stärkerer ARM-Controller.
Schwer zu verstehen, was du überhaupt machen willst. Aber anhand dessen,
was du schreibst vermute ich, dass das Stichwort zu Lösung deines
Problems heisst: virtuelle Methoden...
Patrick B. schrieb:> ISerialDriver& roSerialDriver = *new CSerialDriver();> CModbusRtuMaster& roModbusMaster = *new> CModbusRtuMaster(roSerialDriver);> CDevice* poDevice = new CDevice(roModbsMaster);
Kannst du genauer erklären was du hier machen willst? Warum Referenzen?
Warum Pointer? Warum new?
mh schrieb:> Kannst du genauer erklären was du hier machen willst? Warum Referenzen?> Warum Pointer? Warum new?
Die Serielle Schnittstelle ISerialDriver ist als abstrakte Klasse
umgesetzt welche von CSerialDriver implementiert wird. Somit hätte ich
die Möglichkeit die Schnittstelle Systemabhängig zu erstellen (bei einem
uC oder PC sieht diese ja anders aus).
Die Modbus-Klasse bildet die absolute Basis der Kommunikation. Der Grund
für Referenzen ist, weil ich es so gelernt habe (Referenzen vor Pointer
bevorzugen). Eine Referenz vereinfacht das hanling innerhalb der Klasse,
da diese nie NULL sein kann.
Michael P. schrieb:> Virtuelle Methoden wurde schon genannt, abstrakte Basisklasse wäre> vermutlich auch noch ein Stichwort
Die Basisklasse sollte aber nicht rein "abstrakt" sein, da die
Grundfunktionen (FW-Update...) trotzdem bereit gestellt werden sollen.
Die effektiven Devices erweitern die Basisklasse mit ihren Funktionen
und Registeradressen.
Patrick B. schrieb:> Die> Hardware definiert ob und wie viele Sensoren, Inputs und Outputs> vorhanden.
Ich denke, dass genau deshalb es nicht notwendig ist, vollkommen
dynamisch zu sein. Es entscheidet sich nicht erst zum Zeitpunkt der
Initialisierung, welches Device dran hängt, sondern das ist doch vorher
schon klar. Wenn du trotzdem nur ein Stück Software schreiben willst,
das alle Hardwaremöglichkeiten umfasst, dann sieh vor, eine
Konfigurationsvariable aus dem EEPROM auszulesen, in der steht, welche
Devices instanziiert werden müssen.
Oder ist es so, dass der User die Hardware beliebig umstöpseln kann?
Dann wird dir wohl nichts anderes übrig bleiben, als eine
Software-Komponente zu schreiben, die feststellt, was dran hängt, und
entsprechende Device-Driver instantiiert.
Übrigens, nur weil du verschiedene Klassen von der gleichen Basis
ableitest, hast du noch lange keine Instanzen. Von ineffizient kann man
zu diesem Zeitpunkt noch nicht sprechen. Ineffizient ist es erst, wenn
du Variablen anlegst, die du gar nicht brauchst, weil die Hardware nicht
dran hängt.
tictactoe schrieb:> Oder ist es so, dass der User die Hardware beliebig umstöpseln kann?> Dann wird dir wohl nichts anderes übrig bleiben, als eine> Software-Komponente zu schreiben, die feststellt, was dran hängt, und> entsprechende Device-Driver instantiiert.
Der User könnte vor dem Programmstart eine beliebige Hardware
anstöpseln. Während dem Betrieb bleibt diese aber fix.
Wie könnte so eine Komponente aussehen?
tictactoe schrieb:> Ineffizient ist es erst, wenn> du Variablen anlegst, die du gar nicht brauchst, weil die Hardware nicht> dran hängt.
Genau das ist doch aber das Problem, oder nicht?
Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der
Basisklasse, wovon eine nicht verwendet wird. Zusätzlich würde die reale
Device Instanz von einem ebenfalls nicht verwendet:
Patrick B. schrieb:> Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der> Basisklasse, wovon eine nicht verwendet wird. Zusätzlich würde die reale> Device Instanz von einem ebenfalls nicht verwendet:
Das wäre natürlich fürchterlich. Nicht benutzte Daten im Speicher
verfaulen nach kürzester Zeit, und dann laufen die Elektronen aus ...
Was genau sind denn deine Effizienzbedenken?
Oliver
Als erstes sollten wir mal alle unser Vokabular einigen, eine
"Spezialisierung" ist in C++ meiner Meinung nach nämlich keine
abgeleitete Klasse, sondern eine Template Spezialisierung, die perse mit
Vererbung einmal nichts zu tun hat.
Und langsam wird mir damit glaub ich auch klar was du eigentlich willst.
Du suchst eine simple Möglichkeit verschiedene Instanzen von
abgeleiteten Klassen zur Laufzeit anzulegen, aber eben wirklich nur die
Instanzen die du brauchst. Das Problem hatten schon viele vor dir und
deshalb gibts dafür ein zugeschnittenes Design-Pattern namens
"Factory-Pattern" (sprich Fabrik).
In der einfachsten Form ist dieses Pattern eine freie oder statische
Funktion, die abhängig von irgendwelchen Eingangsparametern dynamisch
neue Instanzen erzeugt und diese zurückgibt.
Damit wird auch recht schnell ersichtlich, dass die Prüfung welches
Device aktuell angeschlossen ist wohl nicht wirklich Teil der
Device-Klasse selbst sein sollte.
Will man das dynamische Anlegen von Speicher verhindern, so liese sich
theoretisch auch ein Summentyp (variant) an Stelle des Pointers als
Rückgabewert nutzen. Der Summentyp würde nur so viel Speicher einnehmen,
wie die größte abgeleitete Klasse benötigt und wäre somit Punkto
Speichereffizienz irgendwo zwischen "dynamisch, so klein wie möglich"
und "statisch, so groß wie möglich".
Ansonsten würde ich empfehlen ein wenig über C++11 und dynamische
Speicherverwaltung zu lesen. Direktes new und delete sollten heutzutage
nach Möglichkeit gemieden werden.
Referenzen auf dynamischen Speicher anzulegen ist in meinen Augen ein
absolutes No-Go und Code Obfuscation in Reinstform!
Patrick B. schrieb:> Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der> Basisklasse, wovon eine nicht verwendet wird.
Wenn sich aber erst zur Laufzeit entscheidet welche Devices tatsächlich
vorhanden sind muss ja trotzdem der Code für alle möglichen Devices im
Programm vorhanden sein, deshalb halte ich die Vererbung von einer
Basisklasse für nicht verschwenderisch. Falls du das mit nicht optimal
meintest.
Im ersten Post hast du das Interface im Konstruktor mitgegeben, dann
hat das Device ein z.B. Modubus Interface. Bei der Vererbung sagst du
das ist ein Modbus Device. Beides geht, hier würde ich das auch mit
Vererbung machen. Je mehr in der abstrakten Basis drin ist (wie
ID/Status liefern, read/write/reset) desto mehr kann man im Programm
generisch mit den Devices umgehen und die switch/case Orgien vermeiden.
Wenn man die Basis komplett abstrakt macht (nur generisches read/write)
könnte man auch verschiedene Klassen mit anderen Hardwareinterfaces
erstellen.
Also für mich hört sich diese Diskussion nach Vererbung vs Komposition
an. Es gibt einen etwas dürftigen Wikipediaartikel, der das Pattern
Komposition statt Vererbung anhand von Delegation zu erklären versucht:
https://en.wikipedia.org/wiki/Composition_over_inheritancehttps://de.wikipedia.org/wiki/Komposition_an_Stelle_von_Vererbung
Meine meinung dazu ist, dass dies zwar ein sehr gutes Designprinzip ist,
aber sich nicht sinvoll in Klassischen OOP Sprachen umsetzen lässt,
wegen dem vielen Boilerplate code für das Delegation Pattern one welchen
das ganze Projektdesign zusammenbrechen würde. Dafür zeigen Sprachen wie
C oder Go hier ihre wahre Stärke: Durch die Trennung von Daten und
Funktionen, bzw. das Fehlen von Methoden und Vererbung, entfällt das
Delegation pattern, und man bekommt Komposition statt Vererbung gratis
dazu. Beispiel:
Das Delegation pattern wird Hinfällig, weil man cdevice direkt verwenden
kann: "cdevice_do_something(device_a->device)". In C ganz normal und
kein Designproblem, in C++ eine Designkatastrophe. Das erst nachträglich
bekannt wird, dass es ein device_a ist, spielt auch keine rolle, weil
man ja Komposition verwendet und man durch die simple Verwenden eines
Pointers die Lebenszeit der Objekte voneinander unabhängig machen kann.
Vincent H. schrieb:> Direktes new und delete sollten heutzutage> nach Möglichkeit gemieden werden.
Wenn man die STL oder Boost verwenden kann, gibt es bessere Lösungen.
Nur für den kleinen Embedded-Bereich sind STL und Boost sehr ungeeignet.
Und wenn man den Code noch stark Platformunabhängig machen möchte (so
dass auf Windows, Linux, SPS, uC...) das ganze kompilier bleiben soll,
muss man leider auf solche Features verzichten.
Vincent H. schrieb:> Und langsam wird mir damit glaub ich auch klar was du eigentlich willst.> Du suchst eine simple Möglichkeit verschiedene Instanzen von> abgeleiteten Klassen zur Laufzeit anzulegen, aber eben wirklich nur die> Instanzen die du brauchst. Das Problem hatten schon viele vor dir und> deshalb gibts dafür ein zugeschnittenes Design-Pattern namens> "Factory-Pattern" (sprich Fabrik).
An so etwas hatte ich auch gedacht...
Patrick B. schrieb:> Nur für den kleinen Embedded-Bereich sind STL und Boost sehr ungeeignet.
Teile von Boost sowie der C++ Standard Library (so heißt das, was man
meistens mit "STL" meint, nämlich richtig) sind auch für Embedded kein
Problem. Die Kunst besteht darin zu wissen, welche! Wenn "new" geht,
sollten unique_ptr und vector z.B. auch gehen.