namespace cpp {}

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:minikurs:objekt

minikurs(10): Klassen und Objekte

Hätt ich mir nicht die Flamme vorbehalten,
so hätt ich nichts aparts für mich.
— Mephisto

Objektorientierter Entwurf

Das Parkplatzprogramm wurde nun schon mehrfach umgeschrieben. Der anfängliche Entwurf ist kaum noch wiederzuerkennen. Höchste Zeit für eine Aktualisierung. Der objektorientierte Ansatz verlangt eine andere Entwurfstechnik und eine andere Art der Entwurfsdokumentation.

Objekt- und Klassendiagramm

Was macht eigentlich einen Parkplatz aus? Ein einzelnes Parkplatz-Objekt für die gestellte Aufgabe ist durch zwei Zahlen eindeutig beschrieben:

----------------------------
  neumannmühle : Parkplatz
----------------------------
  fahrzeuge = 4
  reifen = 10
---------------------------- 

In der Entwurfssprache UML nennt man diese Tafel ein Objektdiagramm. Es kann helfen, die Gemeinsamkeiten aller Parkplatzobjekte, der Klasse Parkplatz, herauszuarbeiten. Jedes Objekt hat bestimmte Eigenschaften (Attribute) mit konkreten Attributwerten. Aus diesen kann jedes Parkplatzobjekt über die Methoden die Anzahl der Autos und Mopeds bestimmen. Das Klassendiagramm gibt eine Übersicht über alle Attribute und Methoden:

------------------
  Parkplatz
------------------
- fahrzeuge : int
- reifen    : int
------------------
+ autos()   : int
+ mopeds()  : int
------------------

Das Klassendiagramm ist der Ausgangspunkt für die Typdefinition im Programm.

Zugriffsschutz und Kapselung

Warum stehen Minuszeichen vor den Attributen und Pluszeichen vor den Methoden? Dies ist kein Zufall oder Versehen, sondern Absicht. Dahinter steht das Kapselungskonzept. Ein Objekt ist für seine Daten (Attributwerte) selbst verantwortlich. Niemand außer den Methoden der Klasse sollte direkten Zugriff auf die Daten haben. Attribute sind privat, der Zugriff ist nicht erlaubt (-). Methoden wiederum sind nur von außen, öffentlich nutzbar, wenn der Zugriff erlaubt ist (+).

Das Kapselungskonzept stützt sich auf diese Überlegungen:

  1. Ein Objekt soll zu Beginn gültige Datenwerte erhalten.
  2. Methoden haben Zugriff auf die Datenwerte und sollen sie wiederum nur in gültige Daten überführen.
  3. Niemand sonst hat Zugriff auf die Objektdaten.

Sind alle Methoden richtig ausprogrammiert, müssten die Objektdaten immer gültig sein. Treten Fehler auf, ist die Ursache im Quelltext dieser Klasse zu finden. Statt Millionen von Quelltextzeilen müssen nur ein paar hundert durchforstet werden.

Über Zugriffsrechte wird festgelegt, wer Zugriff auf welche Bestandteile der Klasse hat. Das Zugriffsrecht wird von der Klasse verliehen (engl.: access is granted), so wie ein König ein Lehen an einen Untertan verleiht. Einmal verliehenes Recht kann nicht wieder entzogen werden. Der Lehensnehmer, seine Erben oder die Öffentlichkeit, ist verpflichtet, es gewissenhaft zu gebrauchen. Der König wiederum sollte mit Lehen sparsam umgehen, sonst bleibt ihm selbst nichts privates mehr. Der höchstmögliche Schutzgrad sollte angestrebt werden. Damit wird die dritte Forderung erfüllt. Abweichungen weichen das Kapselungsprinzip auf.

Die Methoden können nach den Regeln der Programmierkunst sauber ausgeführt werden, so ist auch die zweite Forderung erfüllt. Der erste Punkt bleibt noch zu klären. Wie kommen Anfangswerte in die privaten Attribute der Objekte? Wie wird garantiert, dass nur gültige Daten übernommen werden?

Erzeugen und Vernichten von Objekten

Jede Klasse hat besondere Funktionen, die automatisch dann aufgerufen werden, wenn ein Objekt entsteht. Diese Funktionen werden Konstruktoren, Erzeugende, Generatorfunktion, "Factory" o.ä. genannt. Der Aufruf eines Konstruktors kann nicht umgangen werden. Konstruktoren können Datenwerte übernehmen. Bei ungültigen Datenwerten sollte kein Objekt entstehen.

Jedes gebaute Objekt muss irgendwann im Programm zerstört werden. (Falls nicht, entsteht ein Speicherleck.) Auch dabei wird automatisch eine Funktion der Klasse aufgerufen, der Destruktor.

Warum stehen Konstruktoren und Destruktor nicht im Klassenentwurf? Weil sie noch nicht eingetragen wurden. Ein sauber dokumentierter Entwurf würde auch diese aufführen. Bei großen Projekten ist der Entwurf meist so umfangreich, dass der Software-Architekt (Designer) darauf verzichtet, solche "Kleinigkeiten" anzugeben. Irgendwas muss dem Programmierer auch noch zu tun übrig bleiben…

Objektorientiertes Programmieren

Klassendefinition

Vergleiche die Änderungen zur Vorversion:

parkplatz.h
//: parkplatz.h : Autos und Zweiraeder - R.Richter 2007-01-13
/////////////////////////////////////////////////////////////
 
#ifndef PARKPLATZ_H
#define PARKPLATZ_H
 
class Parkplatz
{
public:
  Parkplatz(int fahrzeuganzahl, int reifenanzahl);
  int autos () const;
  int mopeds() const;
 
private:
  int fahrzeuge;
  int reifen;
};
 
#endif // PARKPLATZ_H

Vergleiche diesen Quelltext auch mit dem obigen Entwurf.

Welche Rolle hat die Funktion, die den Klassennamen trägt? Warum fehlt bei dieser der Ergebnistyp? Warum darf der Ergebnistyp hier fehlen?

Klassen sind Strukturen, bei denen das Zugriffsrecht private voreingestellt ist. Warum wurde das Schlüsselwort class überhaupt erfunden, obwohl struct auch einsetzbar ist? Warum wird bevorzugt class geschrieben?

Das Schlüsselwort const hinter den Methodenköpfen zeigt an, dass die Methoden nichts an den Attributwerten ändern wollen. Diese "freiwillige Selbstverpflichtung" wird vom Compiler streng überwacht. Attributwerte ändernde Methoden dürfen kein const-Merkmal tragen, sollten dann aber auch nichts Unerlaubtes tun…

Methodenimplementierung

Das Klassenmodul ist mit seinen Aufgaben gewachsen:

parkplatz.cpp
//: parkplatz.cpp : Autos und Zweiraeder - R.Richter 2007-01-13
///////////////////////////////////////////////////////////////
 
#include <limits>
#include <string>
#include "parkplatz.h"
 
void fehler(std::string meldung)
{
  throw meldung;
}
 
int Parkplatz::autos () const
{
  return fahrzeuge - mopeds();
}
 
int Parkplatz::mopeds() const
{
  return (4*fahrzeuge - reifen) / 2;
}
 
Parkplatz::Parkplatz(int fahrzeuganzahl, int reifenanzahl)
: fahrzeuge (fahrzeuganzahl)
, reifen    (reifenanzahl  )
{
  if (fahrzeuge > std::numeric_limits<int>::max() / 4) 
                            fehler("Anzahl der Fahrzeuge zu gross");
  if (fahrzeuge < 0)        fehler("Anzahl der Fahrzeuge negativ");
  if (reifen < 0)           fehler("Anzahl der Reifen negativ");
  if (reifen < 2*fahrzeuge) fehler("Anzahl der Reifen zu klein");
  if (reifen > 4*fahrzeuge) fehler("Anzahl der Reifen zu gross");
  if (reifen % 2 != 0)      fehler("Anzahl der Reifen ungerade");
}

Der Konstruktor, die Methode mit dem Klassennamen, übernimmt angegebene Datenwerte in die Attribute. Bemerkenswert ist hier die Initialisiererliste zwischen dem Funktionskopf und dem Funktionsrumpf, eine sprachliche Besonderheit der Konstruktoren. Bei unzulässigen Daten wird im Rumpf der Ausnahmezustand verhängt.

Objekte erschaffen und nutzen

Unmittelbar beim Anlegen der Parkplatz-Variable werden — hoffentlich zulässige — Anfangswerte angegeben. Der umgebende try-catch-Block garantiert, dass das Programm auch bei Fehleingaben "sanft" und mit einer für den Bediener verständlichen Ausschrift endet.

parkplatz9.cpp
//: parkplatz9.cpp : Autos und Zweiraeder - R.Richter 2007-01-13
////////////////////////////////////////////////////////////////
 
#include <iostream>
#include <string>
#include "parkplatz.h"
 
bool eingabe(std::string prompt, int& zahl)
{
  std::cout << prompt;
  return std::cin >> zahl;
}
 
int main()
{
  try
  {
    int f, r;
    if (!(eingabe("Fahrzeuge", f) && eingabe("Reifen", r)))
      throw std::string("keine Zahlen eingegeben");  
 
    Parkplatz p(f, r);
    std::cout << "Autos: "       << p.autos () << '\n'; 
    std::cout << "Motorraeder: " << p.mopeds() << '\n';
  }
  catch (std::string meldung)
  {
    std::cerr << "Fehler: " << meldung << '\n';
    return 1;
  }
  return 0;
}

Rückblick

Werfen wir einen Blick zurück. Wir haben einen weiten Bogen durchmessen. Aus einer harmlosen Aufgabe haben wir einen Entwurf eines Programms abgeleitet. Dieser wurde zuerst in natürlicher Sprache formuliert, als Pseudocode, bevor Quelltext entstand. Der Test zeigte, dass die Aufgabe doch einige Tücken hatte. Mit zunehmender Programmgröße sind Modularisierungstechniken, Unterprogramme, Aufteilen und Zusammenfassen notwendig. Die objektorientierte Entwurfstechnik behandelt Daten und zugehörige Prozeduren als Einheit. Neue Datentypen entstehen, die der Aufgabenstellung besser angepasst sind.

Macht die objektorientierte Technik mit der UML-Schreibweise alle vorher gezeigten Entwurfsmittel (Pseudocode, Struktogramme, Datenkataloge) überflüssig? Nein. Schau Dir das Hauptprogramm noch einmal an. Es entspricht immer noch dem anfänglichen Entwurf. Das Fachwissen wurde in die Typdefinition der Klasse Parkplatz verlagert. Pseudocode und Struktogramme haben weiter ihre Berechtigung. Mit ihnen lassen sich Abläufe in den Methoden beschreiben. Dies wird sogar einfacher, wenn jede einzelne Methode eine überschaubare Aufgabe hat. Komplexe Aufgaben können meist in mehrere einfache aufgespalten werden. Nur den Überblick muss man dabei bewahren. Sehr häufig entsteht beim Lesen objektorientiert geschriebener Programme der Eindruck: Das klingt alles ganz einleuchtend und einfach, nur, wo ist das eigentliche Programm? Das Geheimnis liegt im korrekten Zusammenwirken der einzelnen Teile. Die Art der Zusammenstellung der Bausteine macht daraus mehr als nur eine Summe von Einzelteilen.

Wie weiter?

lernen/minikurs/objekt.txt · Zuletzt geändert: 2020-08-06 16:50 von 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki