Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
  1.6: Merkmale von OOP Kapitel 1 2.1: Grundlagen von C++ 
 
  1.7 Klassenbeschreibungen  
 

Wir wollen uns nun zunächst einige Hilfsmittel beschaffen, die wir zur sinnvollen Beschreibung einer Klasse benötigen.

Dabei müssen wir im Auge behalten, daß das Geheimnisprinzip mit unserer Beschreibungsmethode vereinbar ist. Wir wollen schließlich nur spezifizieren, wie sich Objekte der Klasse nach außen verhalten und keinesfalls irgendwelche physikalischen Darstellungen festlegen -- das wäre eine Überspezifikation. Wie aber sollen wir beispielsweise die Klasse der Arrays oder der Kellerspeicher beschreiben, wenn wir nicht auf die Verteilung der einzelnen Elemente dieser Strukturen im Speicher eingehen dürfen?

Wir können aber glücklicherweise unsere Klassen als Implementationen von abstrakten Datentypen auffassen und uns damit einer der dafür in der Informatik bekannten formalen Beschreibungsmethoden (axiomatisch oder algebraisch) bedienen.

Meyer definiert auf diese Weise sogar den Begriff Objektorientiertheit:

"Objektorientierter Entwurf ist die Entwicklung von Softwaresystemen als strukturierte Sammlungen von Implementierungen abstrakter Datentypen."

1.7.1 Algebraische Spezifikation

Während bei der axiomatischen Beschreibung die Operationen nur implizit angegeben werden (s.u.), werden sie bei einer algebraischen Beschreibung explizit als Funktionen dargestellt. Diese Funktionen brauchen allerdings nicht unbedingt besonders rechner- oder implementationsnah zu sein. Manchmal wird die Beschreibung auf diese Weise auch eher umständlich.

Beispiel: SET[X]

Wegen der algebraischen Darstellungsweise ist einer der am einfachsten beschreibbaren Datentypen der einer Menge von Elementen von einem Typ X.

Da die Menge Elemente beliebigen Datentyps X speichern können soll, haben wir es hier mit einem (durch einen anderen Typ) parametrisierten Datentyp zu tun (wir schreiben SET[X]). Dieses Konzept wird von den meisten objektorientierten Sprachen unterstützt und manchmal sogar als Voraussetzung dafür gesehen, daß sich eine Sprache objektorientiert nennen darf.

Unsere Notation einer algebraischen Beschreibung wird aus der folgenden Beispiel-Spezifikation deutlich. Typnamen wollen wir mit großen Buchstaben schreiben, Funktionsnamen mit kleinen.

algebra SET[X] // Name der Algebra
sorts SET, X, BOOLEAN // vorkommende Typnamen
operations // Signaturen der Operationen
new: -> SET[X] // Anlegen einer neuen Menge (leer)
isempty: SET[X] -> BOOLEAN // Test, ob die Menge leer ist
insert: SET[X] ×  X -> SET[A] // Hinzufügen eines Elements
delete: SET[X] ×  X -> SET[A] // Herausnehmen eines Elements
contains: SET[X] ×  X -> BOOLEAN // Enthaltenseins-Test
sets SET[X] = P(X)  (Potenzmenge) // Menge der Werte für SET[X]
functions
new :={ }
isempty(S) :=(S={ })
insert(S,x) :=S vereinigt {x}
delete(S,x) :=S \ {x}
contains(S,x) :=(x in S)
end SET[X]

Den Typ BOOLEAN mit der Operation not: BOOLEAN ->BOOLEAN (für Wahrheitswerte true und false) wollen wir als eine Art "Metatyp" ansehen, da er für die Notation der Bedingungen notwendig ist (siehe Schreibweise bei isempty und contains).

Im Abschnitt operations werden lediglich die Signaturen der Operationen festgelegt. Um den Operationen eine Bedeutung zu geben, muß zusätzlich eine Semantik angegeben werden.

Dazu zählt zunächst (im sets-Teil) die Definition des sogenannten Trägers des Datentyps, d.h. der reinen Menge der Werte, die Elemente des Datentyps annehmen können (sozusagen der Datentyp nach Vergessen der algebraischen Struktur).

Bei unserer algebraischen Spezifikation folgt dann im functions-Teil die direkte Definition der Operationen mit Hilfe mathematischer Notationen.

Bemerkungen:

1.7.2 Axiomatische Spezifikation

Die axiomatische Beschreibung eines Datentyps ist die gebräuchlichere. Sie gibt überhaupt keine Datenelemente für die Objekte an, sondern definiert sie alleine durch die mit dem Typ möglichen Operationen. Die Operationen werden als Abbildungen zwischen bereits definierten Typen aufgefaßt. Einige Abbildungen werden partiell sein, was durch die Auflistung sogenannter Vorbedingungen genauer spezifiziert wird. Besonders wichtig sind die Axiome, die das Zusammenwirken der Operationen bestimmen. Durch sie und nur durch sie wird das gesamte Verhalten des Typs nach außen festgelegt.

Beispiel: STACK[X]

Das Standard-Beispiel zur Demonstration solcher axiomatischer Beschreibungen ist der Stapelspeicher (Stack, Keller), der in diversen Bereichen der Informatik eine sehr wichtige Rolle spielt. Er dient zur Speicherung und Verwaltung von Daten (beliebigen Typs), die man sich wie übereinander gestapelt vorstellen sollte:

Ein neues Datum wird immer oben auf den Stapel gelegt, alte Daten können immer nur oben von diesem Stapel wieder heruntergeholt werden. Das Datum, was also zuletzt abgespeichert wurde, erhält man auch als nächstes wieder zurück, daher auch die Bezeichnung LIFO-Speicher (last in first out).

Da der Stack nicht auf die Speicherung eines bestimmten Datentyps festgelegt sein soll, haben wir wiederum einen parametrisierten Datentyp vor, geschrieben STACK[X] mit einem bereits bekannten Typ X.

Der Datentyp Stack hat klassischerweise 4 oder 5 Operationen, je nachdem, ob man erlauben will, nur nachzuschauen, welches Element oben auf dem Stack liegt ("top"), oder ob man fordert, daß dieses Element dazu vom Stack heruntergeholt werden muß (Kombination "pop/push").

Einige Funktionen sind partiell, was wir durch die Schreibweise -/-> andeuten. Beispielsweise kann man sich nicht das oberste Element eines leeren Stacks anschauen. Solche Einschränkungen werden durch die sogenannten "Vorbedingungen" beschrieben.

Es gibt eine alternative Notation, bei der der Teil für Vorbedingungen entfällt und mit einem speziellen error-Wert gearbeitet wird. Beispielsweise würde man dann top(new())=error schreiben.

push und pop werden, ähnlich wie bei SET[X], so aufgefaßt, daß sie das Stack-Objekt verändern.

type STACK[X] // Name des neuen Typs
sorts STACK, X, BOOLEAN // vorkommende Typnamen
operations // Signaturen der Operationen
new: -> STACK[X] // Erzeugung eines neuen Stacks
isempty: STACK[X] -> BOOLEAN // Test, ob der Stack leer ist
push: X ×  STACK[X] -> STACK[X] // Speichern eines Elements
pop: STACK[X] -/-> STACK[X] // Löschen des obersten Elements
top: STACK[X] -/-> X // Erfragen des obersten Elements
preconditions // Vorbedingungen
pre pop(s:STACK[X])=not isempty(s) // (d.h. es gibt kein
pre top(s:STACK[X])=not isempty(s) // oberstes Element)
axioms
isempty(new())
not isempty(push(x,s))
top(push(x,s))=x
pop(push(x,s))=s
end STACK[X]

Bemerkungen:

1.7.3 Einteilung der Funktionen

Die Funktionen erhalten abhängig von der Art ihrer Signaturen bestimmte Bezeichnungen (der abstrakte Datentyp heiße T):

Konstruktoren:

Das sind Funktionen, bei der T nur auf der rechten Seite vorkommt (üblicherweise dann genau einmal). Solche Funktionen erzeugen neue Elemente des Typs T, deren Anfangswert von eventuellen Werten von der linken Seite der Signatur abhängig ist. Im Stack-Beispiel ist new ein (der) Konstruktor.

Zugriffsfunktionen:

Hier kommt der Typ T nur auf der linken Seite vor (meistens genau einmal). Diese Funktionen liefern Werte, Zustände, etc. von Elementen des Typs. isempty und top sind die Zugriffsfunktionen des Stacks.

Transformationsfunktionen:

T kommt auf beiden Seiten vor (üblicherweise rechts genau einmal). Die Funktionen werden so aufgefaßt, daß ein Element verändert wird, eventuell abhängig von weiteren Argumenten von der linken Seite, so wie push und pop das beim Stack tun.

Funktionen, in deren Signatur T gar nicht auftaucht, haben in der eigentlichen Spezifikation von T natürlich nichts zu suchen. (In C++ können sie als statische Memberfunktionen dennoch dem Typ zugeordnet werden.)

Bei objektorientierter Programmierung erfolgt jede Methodenausführung durch das Schicken einer Nachricht an genau ein Objekt. Deshalb ist ein T-Argument ausgezeichnet. Folgendes ist üblich:

Das Gegenstück zu den Konstruktoren sind die sogenannten Destruktoren, mit denen sich Objekte wieder löschen lassen. Während es sehr sinnvoll ist, Konstruktoren in die axiomatische Beschreibung aufzunehmen (insbesondere wegen der Initialisierung mit Anfangswerten), passen Destruktoren nicht in dieses Konzept. In der Implementation mit einer Programmiersprache spielen die Destruktoren aber eine wichtige Rolle.

 
  1.6: Merkmale von OOP Startseite 2.1: Grundlagen von C++ 
 

© 1998 Axel Rogat