Hallo Forum,
mir geht es vornehmlich darum, ob ich Interfaces in C++ soweit
verstanden habe.
Also:
Man definiert ein Interface, hier als Beispiel die (abstrakte?) Klasse
IHardware
1
classIHardware{
2
public:
3
virtualboolgetPinState(uint8_tpin_um)=0;
4
...
5
};
Dann braucht man eine Klasse die dieses Interface implementiert, hier
die Klasse Hardware
1
classHardware:publicIHardware{
2
public:
3
boolgetPinState(uint8_tpin_num);
4
...
5
};
6
7
boolHardware::getPinState(uint8_tpin_num){
8
boolstate=....
9
returnstate;
10
};
Als drittes braucht man noch etwas, was auf dieses Interface zugreift,
z.B. eine Funktion
1
voidaccessHardware(IHardware&hw){
2
boolstate=hw.getPinState(5);
3
...
4
}
Zusammengeklatscht könnte das ganze so aussehen:
1
intmain(void){
2
HardwareHwObj;
3
...
4
accessHardware(HwObj);
5
...
6
return0;
7
}
Dadurch hab ich die Möglichkeit, z.B. dem Objekt HwObj ein neues
Verhalten zu gegebn, in dem ich z.B. gegen eine neue Klasse Hardware
(welches auch das Interface IHardware implementiert), welche einen Mock
darstellt, baue.
Weiterer Vorteil ist, dass man in die Funktion accessHardware alle
Objekte reingeben kann, die das Interface IHardware implementieren.
Ist das soweit richtig?
Grüße
Kaj G. schrieb:> Dadurch hab ich die Möglichkeit, z.B. dem Objekt HwObj ein neues> Verhalten zu gegebn, in dem ich z.B. gegen eine neue Klasse Hardware> (welches auch das Interface IHardware implementiert), welche einen Mock> darstellt, baue.
Neues Verhalten einem Objekt hinzufügen, macht man eher über
Komposition.
Kaj G. schrieb:> Weiterer Vorteil ist, dass man in die Funktion accessHardware alle> Objekte reingeben kann, die das Interface IHardware implementieren.>> Ist das soweit richtig?
Ja, das ist ein großer Vorteil der Interface-Vererbung. Deine konkrete
Klasse Hardware, kann beliebig komplex sein. Du kannst auch ganz andere
Klassen verwenden (z.B. Maus, Monitor, CPU). Für accessHardware ist nur
relevant dass sie von IHardware abgeleitet sind.
Kaj G. schrieb:> Dann braucht man eine Klasse die dieses Interface implementiert, hier> die Klasse Hardware> [c]> class Hardware : public IHardware {> public:> bool getPinState(uint8_t pin_num);> ...> };
Hier könntest Du noch ein "override" hinzufügen, damit Du nicht aus
Versehen die Elementfunktion re-definierst.
Ausserdem sollten Getter (also Observator-Funktionen) immer "const"
sein.
> Dadurch hab ich die Möglichkeit, z.B. dem Objekt HwObj ein neues> Verhalten zu gegebn, in dem ich z.B. gegen eine neue Klasse Hardware> (welches auch das Interface IHardware implementiert), welche einen Mock> darstellt, baue.
Da verstehe ich nicht genau was Du meinst: generell beschreibt das
Interface eine "Verhaltenseigenschaft". Die Datenelemente den (internen)
Zustand.
> Weiterer Vorteil ist, dass man in die Funktion accessHardware alle> Objekte reingeben kann, die das Interface IHardware implementieren.
Vielleicht beachtest Du auch die Art der Parameterübergabe: Du übergibt
als non-const reference: das bedeutet dann semantisch ein
Input-Output-Parameter. Ist das so gewünscht?
Kann man so machen. Ist halt so wie man es unter Java machen würde.
Unter C++ wäre "IHardware" nur ein Denkkonstrukt. Die Implementierung in
der Klasse Hardware würde es genau so geben, nur eben ohne virtuelle
Funktionen. Und immer wenn es eine Funktion gibt, an die das Objekt
HwObj weitergereicht wird, wäre eine Template-Funktion. Damit fällt die
ganze Indirektion weg und der Compiler kann optimieren bis zum
gehtnichtmehr.
Das Konstrukt "IHardware" besteht dann nur mehr in der Dokumentation der
Template-Funktionen: "Der Parameter xy muss die Eigenschaft erfüllen,
dass xy.getPinState(num) ein gültiger Ausdruck ist."
tictactoe schrieb:> Kann man so machen. Ist halt so wie man es unter Java machen würde.>> Unter C++ wäre "IHardware" nur ein Denkkonstrukt. Die Implementierung in> der Klasse Hardware würde es genau so geben, nur eben ohne virtuelle> Funktionen.
Wenn es denn um eine Anwendung im uC-Bereich geht: ja.
>> Das Konstrukt "IHardware" besteht dann nur mehr in der Dokumentation der> Template-Funktionen: "Der Parameter xy muss die Eigenschaft erfüllen,> dass xy.getPinState(num) ein gültiger Ausdruck ist."
Man kann diese Anwendung der statischen Polymorphie auch direkt in C++
ausdrücken mit Hilfe des CRTP-Patterns. Denn Dokumentation und Code
driftet leider häufig auseinander.
Kaj G. schrieb:> Ist das soweit richtig?
Vom Prinzip her geht es um Typ-Kompatibilität.
Die klassische Art in vielen OO-Sprachen (aber nicht allen) einen zu
einem anderen Typ kompatiblen Typ zu erhalten ist durch Ableiten einer
Klasse. Das schränkt aber insoweit ein, dass alle zu einem Typen
kompatibel Typen Unterklassen einer Oberklasse sein müssen.
Mit Interfaces hat man diese Einschränkung nicht. Alle Klassen die das
Interface implementieren sind typ-kompatibel, müssen aber nicht mehr
alle von der gleichen Oberklasse abgeleitet sein.
Ein weiterer Vorteil von Interfaces ist dass man mehrere Interfaces in
einer Klasse implementieren kann und die Klasse damit kompatibel zu
mehreren Typen ist. Damit vermeidet man Mehrfachvererbung.
Hannes J. schrieb:> Kaj G. schrieb:>> Ist das soweit richtig?>> Vom Prinzip her geht es um Typ-Kompatibilität.
Das nennt sich Inklusionspolymorphie bzw. Liskovs'sches
Substitutionsprinzip, falls man mal googlen möchte.
> Mit Interfaces hat man diese Einschränkung nicht. Alle Klassen die das> Interface implementieren sind typ-kompatibel, müssen aber nicht mehr> alle von der gleichen Oberklasse abgeleitet sein.
In C++ sind Interfaces Klassen ohne Zustand und nur mit rein-virtuellen
Elementfunktionen und einem virtuellen Destruktor.
> Ein weiterer Vorteil von Interfaces ist dass man mehrere Interfaces in> einer Klasse implementieren kann und die Klasse damit kompatibel zu> mehreren Typen ist. Damit vermeidet man Mehrfachvererbung.
In C++ implementiert man mehrere Interfaces durch öffentliche
Mehrfachvererbung, anders geht es nicht.
Hannes J. schrieb:> Mit Interfaces hat man diese Einschränkung nicht. Alle Klassen die das> Interface implementieren sind typ-kompatibel, müssen aber nicht mehr> alle von der gleichen Oberklasse abgeleitet sein.
Das ist meines Wissens nach z.B. in Java so, aber nicht in C++. Oder?
Mark B. schrieb:> Hannes J. schrieb:>> Mit Interfaces hat man diese Einschränkung nicht. Alle Klassen die das>> Interface implementieren sind typ-kompatibel, müssen aber nicht mehr>> alle von der gleichen Oberklasse abgeleitet sein.>> Das ist meines Wissens nach z.B. in Java so, aber nicht in C++. Oder?
"Implementieren" bedeutet hier, von dem Interface-Type ableiten.
Wilhelm M. schrieb:> Vielleicht beachtest Du auch die Art der Parameterübergabe: Du übergibt> als non-const reference: das bedeutet dann semantisch ein> Input-Output-Parameter. Ist das so gewünscht?
Das ist ja hier nur ein Beispiel, aber danke fuer den Hinweis. :)
const waere aber angebracht, da hast du recht.
Wilhelm M. schrieb:>> Dadurch hab ich die Möglichkeit, z.B. dem Objekt HwObj ein neues>> Verhalten zu gegebn, in dem ich z.B. gegen eine neue Klasse Hardware>> (welches auch das Interface IHardware implementiert), welche einen Mock>> darstellt, baue.>> Da verstehe ich nicht genau was Du meinst: generell beschreibt das> Interface eine "Verhaltenseigenschaft". Die Datenelemente den (internen)> Zustand.
Damit meine ich sowas wie Dependency Injection fuer Unittest.
Wilhelm M. schrieb:> Hier könntest Du noch ein "override" hinzufügen, damit Du nicht aus> Versehen die Elementfunktion re-definierst.
Ah, okay. Dafuer ist das Ding also gut :D
tictactoe schrieb:> Und immer wenn es eine Funktion gibt, an die das Objekt> HwObj weitergereicht wird, wäre eine Template-Funktion. Damit fällt die> ganze Indirektion weg und der Compiler kann optimieren bis zum> gehtnichtmehr.
Werde ich mir mal anschauen. Hoert sich interessant an.
Wilhelm M. schrieb:> In C++ sind Interfaces Klassen ohne Zustand und nur mit rein-virtuellen> Elementfunktionen und einem virtuellen Destruktor.
Warum ist der virtuelle Destruktor so wichtig?
Gruesse
Kaj G. schrieb:>> Wilhelm M. schrieb:>> In C++ sind Interfaces Klassen ohne Zustand und nur mit rein-virtuellen>> Elementfunktionen und einem virtuellen Destruktor.> Warum ist der virtuelle Destruktor so wichtig?
Damit bei einem "delete" über einen BasiklassenZeiger nicht der
Basis-dtor sondern der dtor des tatsächlichen Typs aufgerufen wird. Das
ist absolutes MUSS!
Hannes J. schrieb:> Die klassische Art in vielen OO-Sprachen (aber nicht allen) einen zu> einem anderen Typ kompatiblen Typ zu erhalten ist durch Ableiten einer> Klasse. Das schränkt aber insoweit ein, dass alle zu einem Typen> kompatibel Typen Unterklassen einer Oberklasse sein müssen.>> Mit Interfaces hat man diese Einschränkung nicht. Alle Klassen die das> Interface implementieren sind typ-kompatibel, müssen aber nicht mehr> alle von der gleichen Oberklasse abgeleitet sein.
Dafür müssen sie das Interface implementieren. Wo ist denn für dich der
elementare Unterschied zwischen dem Implementieren eines Interfaces und
dem Ableiten von einer abstrakten Basisklasse? Für mich sind das nur
zwei verschiedene Bezeichnungen für ein und das selbe.
> Ein weiterer Vorteil von Interfaces ist dass man mehrere Interfaces in> einer Klasse implementieren kann und die Klasse damit kompatibel zu> mehreren Typen ist. Damit vermeidet man Mehrfachvererbung.
Auch hier die Frage: Wo außer beim Namen ist da der Unterschied?
Abgesehen vielleicht davon, dass Mehrfachvererbung flexibler ist, was
für mich aber kein Grund ist, es vermeiden zu wollen.
Kaj G. schrieb:> Damit meine ich sowas wie Dependency Injection fuer Unittest.
Ja. Eine Methode/Funktion, die ein Objekt vom Typ Interface
hineingereicht bekommt, kann bzgl. dieses Objekts recht einfach und
unabhängig verunittestet werden. Der Unittest kann seine
Testanforderungen in seiner eigenen Interface-Implementierung
formulieren. Und es besteht weder in der produktiven Methode/Funktion
noch im Unittest eine Anhängigkeit zu irgendeiner produktiven
Interface-Implementierung, die nicht Bestandteil der zu vertestenden
Unit ist.
Logisch ist dann auch, daß das Interface zur
Komponente/Library/Framework gehört, die es verarbeitet, das vom User
dieser/dieses Komponente/Library/Frameworks zu implementieren ist. Das
nennt sich Entkoppelung. Die/das Komponente/Library/Framework darf seine
User nicht kennen. Einmal fertiggestellt, verprobt und dann in eine Lib
gepackt, ausgeliefert und, bis neue Anforderungen/Bug-Reports kommen,
nicht mehr angefaßt.