Interface-Design in C++
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 61 von 104.
Ein Interface in C++ ist eine abstrakte Klasse, in der alle Elementfunktionen rein virtuell sind. Im Gegensatz zu abstrakten Klassen, die eine gewisse Implementierung enthalten können, definieren Interfaces nur, welche Operationen existieren müssen – sie legen einen Vertrag ohne jegliches Verhalten fest.
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Dieses Drawable-Interface garantiert, dass jede implementierende Klasse über draw()- und resize()-Methoden verfügen wird, sagt aber nichts darüber aus, wie diese funktionieren. Diese Trennung ist leistungsstark – Code kann vom Interface abhängen, ohne etwas über die konkreten Typen zu wissen.
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};
class Button : public Drawable {
std::string label;
public:
Button(std::string l) : label(l) {}
void draw() override { std::cout << "Drawing button: " << label << std::endl; }
void resize(double factor) override { /* Button-Größe ändern */ }
};Eine Klasse kann mehrere Schnittstellen implementieren, was flexible Designs ermöglicht, bei denen Objekte verschiedene Rollen erfüllen können:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Interfaces fördern eine lose Kopplung – Ihr Code hängt von Abstraktionen ab statt von konkreten Implementierungen, was die Erweiterung und das Testen erleichtert.
Aufgabe
EinfachLassen Sie uns ein Geräteverwaltungssystem erstellen, das zeigt, wie Interfaces Verträge für verschiedene Arten von Geräten definieren. Sie werden zwei separate Interfaces erstellen, die Geräte implementieren können, und dann konkrete Geräteklassen bauen, die einen oder beide dieser Verträge erfüllen.
Sie werden Ihren Code über drei Dateien organisieren:
Interfaces.h: Definieren Sie zwei rein abstrakte Klassen (Interfaces), die unterschiedliche Gerätefunktionen repräsentieren:Powerable— jedes Gerät, das ein- und ausgeschaltet werden kann:- Eine rein virtuelle
powerOn()Methode - Eine rein virtuelle
powerOff()Methode - Eine rein virtuelle
getPowerStatus()Methode, die einenstd::stringzurückgibt - Ein virtueller Destruktor
Connectable— jedes Gerät, das sich mit einem Netzwerk verbinden kann:- Eine rein virtuelle
connect(const std::string& network)Methode - Eine rein virtuelle
disconnect()Methode - Eine rein virtuelle
getConnectionInfo()Methode, die einenstd::stringzurückgibt - Ein virtueller Destruktor
- Eine rein virtuelle
Devices.h: Implementieren Sie konkrete Geräteklassen, die diese Interfaces nutzen:Lamp— implementiert nurPowerable:- Ein privates
bool isOnMitglied (startet alsfalse) - Ein privates
std::string nameMitglied - Ein Konstruktor, der den Namen der Lampe entgegennimmt
powerOn()setztisOnauf true und gibt aus:<name>: Light turned onpowerOff()setztisOnauf false und gibt aus:<name>: Light turned offgetPowerStatus()gibt"ON"oder"OFF"basierend auf dem Zustand zurück
SmartTV— implementiert sowohlPowerableals auchConnectable:- Private Mitglieder:
bool isOn(startet false),std::string brand,std::string currentNetwork(startet leer) - Ein Konstruktor, der die Marke des Fernsehers entgegennimmt
powerOn()setztisOnauf true und gibt aus:<brand> TV: Powered onpowerOff()setztisOnauf false, leert das Netzwerk und gibt aus:<brand> TV: Powered offgetPowerStatus()gibt"ON"oder"OFF"zurückconnect()speichert den Netzwerknamen und gibt aus:<brand> TV: Connected to <network>disconnect()leert das Netzwerk und gibt aus:<brand> TV: DisconnectedgetConnectionInfo()gibt"Connected to <network>"zurück, wenn verbunden, oder"Not connected", wenn leer
- Ein privates
main.cpp: Lesen Sie drei Eingaben ein (jede in einer separaten Zeile):- Lampenname
- TV-Marke
- Netzwerkname
Erstellen Sie eine
Lampund einenSmartTV. Demonstrieren Sie, wie dasselbe Interface mit verschiedenen Geräten verwendet werden kann:Arbeiten Sie zuerst mit beiden Geräten über das
PowerableInterface. Speichern Sie Pointer auf beide in einem Array vonPowerable*, durchlaufen Sie dieses in einer Schleife und rufen Sie für jedespowerOn()auf, gefolgt von der Ausgabe ihres Status als:Status: <powerStatus>Geben Sie eine Leerzeile aus und arbeiten Sie dann mit dem SmartTV über das
ConnectableInterface. Erstellen Sie einenConnectable*Pointer auf Ihren SmartTV, rufen Sieconnect()mit dem Netzwerknamen auf und geben Sie aus:Connection: <connectionInfo>Geben Sie eine weitere Leerzeile aus, schalten Sie dann beide Geräte über das
PowerableArray aus und zeigen Sie ihren endgültigen Status an.
Zum Beispiel mit den Eingaben Desk Lamp, Samsung und HomeWiFi:
Desk Lamp: Light turned on
Status: ON
Samsung TV: Powered on
Status: ON
Samsung TV: Connected to HomeWiFi
Connection: Connected to HomeWiFi
Desk Lamp: Light turned off
Status: OFF
Samsung TV: Powered off
Status: OFFBeachten Sie, wie der SmartTV entweder als Powerable oder als Connectable behandelt werden kann, je nachdem, welchen Interface-Pointer Sie verwenden. Diese Flexibilität ist die Stärke der Implementierung mehrerer Interfaces — Ihr Code kann mit jedem Gerät arbeiten, das den benötigten Vertrag erfüllt, ohne den konkreten Typ kennen zu müssen.
Spickzettel
Ein Interface in C++ ist eine abstrakte Klasse, in der alle Elementfunktionen rein virtuell (pure virtual) sind. Interfaces definieren nur, welche Operationen existieren müssen, ohne jegliche Implementierung:
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Klassen implementieren Interfaces, indem sie alle rein virtuellen Funktionen überschreiben:
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};Eine Klasse kann mehrere Interfaces mittels Mehrfachvererbung implementieren:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Interfaces fördern eine lose Kopplung, indem sie es ermöglichen, dass Code von Abstraktionen anstatt von konkreten Implementierungen abhängt.
Probier es selbst
#include <iostream>
#include <string>
#include "Devices.h"
using namespace std;
int main() {
// Eingaben lesen
string lampName;
string tvBrand;
string networkName;
getline(cin, lampName);
getline(cin, tvBrand);
getline(cin, networkName);
// TODO: Ein Lamp- und ein SmartTV-Objekt erstellen
// TODO: Ein Array von Powerable*-Pointern erstellen, das beide Geräte enthält
// TODO: Das Array durchlaufen und powerOn() für jedes Gerät aufrufen
// Nach jedem powerOn(), ausgeben: Status: <powerStatus>
// TODO: Eine Leerzeile ausgeben
// TODO: Einen Connectable*-Pointer auf den SmartTV erstellen
// connect() mit dem Netzwerknamen aufrufen
// Ausgeben: Connection: <connectionInfo>
// TODO: Eine Leerzeile ausgeben
// TODO: Das Powerable-Array durchlaufen und powerOff() für jedes Gerät aufrufen
// Nach jedem powerOff(), ausgeben: Status: <powerStatus>
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container