Über unsMediaKontaktImpressum
Prof. Dr. Ulrich Breymann 04. Juli 2014

Von der UML nach C++ - Vererbung und Interfaces

Die Unified Modeling Language (UML) ist eine weit verbreitete grafische Beschreibungssprache für Klassen, Objekte, Zustände, Abläufe und noch mehr. Sie wird vornehmlich in der Phase der Analyse und des Softwareentwurfs eingesetzt. Auf die UML-Grundlagen wird hier nicht eingegangen; dafür gibt es andere Bücher. Hier geht es darum, die wichtigsten UML-Elemente aus Klassendiagrammen in C++-Konstruktionen, die der Bedeutung des Diagramms möglichst gut entsprechen, umzusetzen.

Die nachfolgend vorgestellten C++-Konstruktionen sind Muster, die als Vorlage dienen können. Diese Muster sind nicht einzigartig, sondern nur Empfehlungen, wie man die Umsetzung gestalten kann. Im Einzelfall kann eine Variation sinnvoll sein.

Vererbung

Über Vererbung ist schon einiges gesagt worden, das hier nicht wiederholt werden muss. Die Abbildung 1 zeigt das zugehörige UML-Diagramm.

In vielen Darstellungen wird die Oberklasse oberhalb der abgeleiteten Unterklasse dargestellt; in der UML ist aber nur der Pfeil mit dem Dreieck entscheidend, nicht die relative Lage. In C++ wird Vererbung syntaktisch durch »: public« ausgedrückt:

class Unterklasse : public Oberklasse {
// ... Rest weggelassen
};

Interface anbieten und nutzen

Interface anbieten

Die Abbildung 2 zeigt das zugehörige UML-Diagramm. Die Klasse Anbieter implementiert das Interface Schnittstelle-X. Bei der Vererbung stellt die abgeleitete Klasse die Schnittstellen der Oberklasse zur Verfügung. Insofern gibt es eine Ähnlichkeit, auch gekennzeichnet durch die gestrichelte Linie im Vergleich zum vorherigen Diagramm.

Die Ähnlichkeit wird in der Umsetzung nach C++ abgebildet: Anbieter wird von SchnittstelleX abgeleitet. Um klarzustellen, dass es um ein Interface geht, sollte SchnittstelleX abstrakt sein. Das Datenobjekt d wird nicht als const-Referenz übergeben, weil service() damit auch die Ergebnisse an den Aufrufer übermittelt. Ein einfaches Programmbeispiel finden Sie im Verzeichnis cppbuch/k21/interface.

class SchnittstelleX {
public:
virtual void service(Daten& d) = 0; // abstrakte Klasse
};
class Anbieter : public SchnittstelleX {
public:
void service(Daten& d) {
// ... Implementation der Schnittstelle
}
};

Interface nutzen

Bei der Nutzung des Interfaces bedient sich der Nutzer einer entsprechenden Methode des Anbieters. Die Abbildung 3 zeigt das zugehörige UML-Diagramm.

Ein Nutzer muss ein Anbieter-Objekt kennen, damit der Service genutzt werden kann. Aus diesem Grund wird in der folgenden Klasse bereits dem Konstruktor von Nutzer ein Anbieter-Objekt übergeben, und zwar per Referenz, nicht per Zeiger. Der Grund: Zeiger können NULL sein, aber undefinierte Referenzen gibt es nicht.

class Nutzer {
public:
Nutzer(SchnittstelleX& a)
: anbieter(a) {
daten = ...
}
void nutzen() {
anbieter.service(daten);
}
private:
Daten daten;
SchnittstelleX& anbieter;
};

Nun kann man sich fragen, warum die Referenz oben nicht als const übergeben wird. Das kann je nach Anwendungsfall sinnvoll sein oder auch nicht. Es hängt davon ab, ob sich der Zustand des Anbieter-Objekts durch den Aufruf der Funktion service(daten) ändert. Wenn ja, zum Beispiel durch interne Protokollierung der Aufrufe, entfällt const.

Assoziation

Eine Assoziation sagt zunächt einmal nur aus, dass zwei Klassen in einer Beziehung (mit Ausnahme der Vererbung) stehen. Die Art der Beziehung und zu wie vielen Objekten sie aufgebaut wird, kann variieren. In der Regel gelten Assoziationen während der Lebensdauer der beteiligten Objekte. Nur kurzzeitige Verbindungen werden meistens nicht notiert. Ein Beispiel für eine kurzzeitige Verbindung ist der Aufruf anbieter.service(daten); oben: anbieter kennt durch die Parameterübergabe das Objekt daten, wird aber vermutlich die Verbindung nach Ablauf der Funktion lösen.

Einfache gerichtete Assoziation

Das UML-Diagramm einer einfachen gerichteten Assoziation sehen Sie in Abbildung 4.

Mit »gerichtet« ist gemeint, dass die Umkehrung nicht gilt, wie zum Beispiel die Beziehung »ist Vater von«. Falls zwar Klasse1 die Klasse2 kennt, aber nicht umgekehrt, wird dies durch ein kleines Kreuz bei Klasse1 vermerkt. Es kann natürlich sein, dass eine Beziehung zwischen zwei Objekten derselben Klasse besteht. Im UML-Diagramm führt dann der von einer Klasse ausgehende Pfeil auf dieselbe Klasse zurück. In C++ wird eine einfache gerichtete Assoziation durch ein Attribut zeigerAufKlasse2 realisiert:

class Klasse1 {
public:
Klasse1() {
: zeigerAufKlasse2(nullptr) {
}
void setKlasse2(Klasse2 ptr2) {
zeigerAufKlasse2 = ptr2;
}
private:
Klasse2 zeigerAufKlasse2;
};

Ein Zeiger ist hier besser als eine Referenz geeignet, weil es sein kann, dass das Kennenlernen erst nach dem Konstruktoraufruf geschieht.

Gerichtete Assoziation mit Multiplizität

Die Multiplizität, auch Kardinalität genannt, gibt an, zu wie vielen Objekten eine Verbindung aufgebaut werden kann. In Abbildung 5 bedeutet die 1, dass jedes Objekt der Klasse2 zu genau einem Objekt der Klasse1 gehört. Das Sternchen * bei Klasse2 besagt, dass einem Objekt der Klasse1 beliebig viele (auch 0) Objekte der Klasse2 zugeordnet sind.

Im folgenden C++-Beispiel entspricht Fan der Klasse1 und Popstar der Klasse2. Ein Fan kennt N Popstars. Die Beziehung ist also »kennt«. Der Popstar hingegen kennt seine Fans im Allgemeinen nicht. Um die Multiplizität auszudrücken, bietet sich ein vector an, der Verweise auf Popstar-Objekte speichert. Wenn die Verweise eindeutig sein sollen, ist ein set die bessere Wahl.

class Fan {
public:
void werdeFanVon(Popstar star) {
meineStars.insert(star); // zu set::insert() siehe Seite 803
}
void denKannsteVergessen(Popstar star) {
meineStars.erase(star); // Rückgabewert ignoriert
}
// Rest weggelassen
private:
std::set<Popstar> meineStars;
};

Die Objekte als Kopie abzulegen, also Popstar als Typ für den Set statt Popstar* zu nehmen, hat Nachteile. Erstens ist es wenig sinnvoll, die Kopie zu erzeugen, wenn es doch das Original gibt, und zweitens kostet es Speicherplatz und Laufzeit. Es gibt nur einen Vorteil: Es könnte ja sein, dass es das originale Popstar-Objekt nicht mehr gibt, zum Beispiel durch ein delete irgendwo. Ein noch existierender Zeiger wäre danach auf eine undefinierte Speicherstelle gerichtet. Eine noch existierende Kopie könnte als Wiedergänger auftreten.

Einfache ungerichtete Assoziation

Eine ungerichtete Assoziation wirkt in beiden Richtungen und heißt deswegen auch bidirektionale Assoziation. Die Abbildung 6 zeigt das UML-Diagramm.

Wenn zwei sich kennenlernen, kann das mit einer ungerichteten Assoziation modelliert werden. Zur Abwechslung sei die Umsetzung in C++ nicht mit zwei, sondern nur mit einer Klasse (namens Person) gezeigt. Das heißt, die Klasse hat eine Beziehung zu sich selbst, siehe Abbildung 7. Solche Assoziationen werden auch rekursiv genannt und dienen zur Darstellung der Beziehung verschiedener Objekte derselben Klasse.

Die Umsetzung in C++ wird am Beispiel von Personen gezeigt, die sich gegenseitig kennenlernen. Ein Aufruf A.lerntkennen(B); impliziert, dass B auch A kennenlernt:

// cppbuch/k21/bidirektionaleAssoziation/main.cpp
#include "Person.h"

int main() {
Person mabuse("Dr. Mabuse");
Person klicko("Witwe Klicko");
Person holle("Frau Holle");
mabuse.lerntkennen(klicko);
holle.lerntkennen(klicko);
// ...
}

Die entscheidende Methode der Klasse Person ist lerntkennen(Person& p) (siehe unten). Beim Eintrag in die Menge der Bekannten wird festgestellt, ob der Eintrag vorher schon vorhanden war. Wenn nicht, wird er auch auf der Gegenseite vorgenommen.

// cppbuch/k21/bidirektionaleAssoziation/Person.h
#ifndef PERSON_H
#define PERSON_H
#include<iostream>
#include<set>
#include<string>

class Person {
public:
Person(const std::string& name_)
: name(name_) {
}

virtual ~Person() = default;

const std::string& getName() const {
return name;
}

void lerntkennen(Person& p) {
bool nichtvorhanden = bekannte.insert(p.getName()).second;
if (nichtvorhanden) { // falls unbekannt, auch bei p eintragen
p.lerntkennen(this);
}
}

void bekannteZeigen() const {
std::cout << "Die Bekannten von " << getName() << " sind:" << std::endl;
for (auto bekannt : bekannte) {
std::cout << bekannt << std::endl;
}
}

private:
std::string name;
std::set<std::string> bekannte;
};
#endif

Aggregation

Die »Teil-Ganzes«-Beziehung (englisch part of) wird auch Aggregation genannt. Sie besagt, dass ein Objekt aus mehreren Teilen besteht (die wiederum aus Teilen bestehen können). Die Abbildung 8 zeigt das UML-Diagramm. Die Struktur entspricht der gerichteten Assoziation, sodass deren Umsetzung in C++ hier Anwendung finden kann. Ein Teil kann für sich allein bestehen, also auch vom Ganzen gelöst werden. Letzteres geschieht in C++ durch Nullsetzen des entsprechenden Zeigers.

Komposition

Die Komposition ist eine spezielle Art der Aggregation, bei der die Existenz der Teile vom Ganzen abhängt. Damit ist gemeint, dass die Teile zusammen mit dem Ganzen erzeugt und auch wieder vernichtet werden. Ein Teil ist somit stets genau einem Ganzen zugeordnet; die Multiplizität kann also nur 1 sein. Die Abbildung 9 zeigt das UML-Diagramm.

Es empfiehlt sich, bei der Umsetzung in C++ Werte statt Zeiger zu nehmen. Dann ist gewährleistet, dass die Lebensdauer der Teile an das Ganze gebunden ist:

class Ganzes {
public:
Ganzes(int datenFuerTeil1, int datenFuerTeil2)
: ersterTeil(datenFuerTeil1),
zweiterTeil(datenFuerTeil2) {
// ...
}
// ...
private:
Teil ersterTeil;
Teil zweiterTeil;
};
Autor

Prof. Dr. Ulrich Breymann

Prof. Dr. Ulrich Breymann lehrte Informatik an der Hochschule Bremen. Er engagierte sich im DIN-Arbeitskreis zur ersten Standardisierung von C++ und ist ein renommierter Autor zum Thema C++. Aus seiner Tätigkeit in Industrie und...
>> Weiterlesen
botMessage_toctoc_comments_9210