Grundkonzepte objektorientierter Programmierung

Werbung
Grundkonzepte
objektorientierter Programmierung Beispiele in
Java, Oberon-2, Python und Delphi
Projektarbeit im Studiengang Lehramt Informatik
Sommersemester 2006
Friedrich-Schiller-Universität Jena
erstellt von
Nicole Himmerlich
betreut von
Prof. Dr. Michael Fothe
&
Lutz Kohl
August 2006
Inhaltsverzeichnis
1 Konzepte und Begrie der Objektorientierung
1
2 Beispiel: Konto
3
2.1
Aggregierung und enge Kopplung
. . . . . . . . . . . . . . . . . . . . .
3
2.2
Datenkapselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.3
Konstruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
2.4
Klassen- und instanzspezische Komponenten
. . . . . . . . . . . . . .
14
2.5
Manipulierbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.6
Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.7
Reimplementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
2.8
Polymorphismus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.9
Überladen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
32
3 Beispiel: Mediendatenbank
35
4 Beispiel: Geometrische Figuren
48
A Python-Quellcode
61
A.1
Beispiel: Konto
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
A.2
Beispiel: Mediendatenbank . . . . . . . . . . . . . . . . . . . . . . . . .
67
A.3
Beispiel: Geometrische Figuren
69
. . . . . . . . . . . . . . . . . . . . . .
B Delphi-Quellcode
72
B.1
Beispiel: Konto
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
72
B.2
Beispiel: Mediendatenbank . . . . . . . . . . . . . . . . . . . . . . . . .
87
B.3
Beispiel: Geometrische Figuren
91
. . . . . . . . . . . . . . . . . . . . . .
1 Konzepte und Begrie der Objektorientierung
Die Prädicate der Erscheinung können dem Objecte selbst beigelegt werden,
in Verhältniÿ auf unseren Sinn, z.B. der Rose die rothe Farbe, oder der Geruch; [...]
Kant
1
Wie wir uns räumliche Gegenstände überhaupt nicht auÿerhalb der Zeit
denken können, so können wir uns keinen Gegenstand auÿerhalb der Möglichkeiten seiner Verbindung mit anderen denken.
2
Wittgenstein
Die obigen Zitate zeigen, dass objektorientiertes Denken keine Erndung der Informatik ist, vielmehr gehen wir alle täglich mit Objekten um. [Somit ist Objektorientierung]
für uns Menschen eine natürliche Betrachtungsweise. Wir wissen, dass Objekte Eigenschaften und ein Verhalten besitzen und aus anderen Objekten zusammengesetzt sein
können. Wir betrachten von einem Objekt nur gerade so viel, wie uns im Moment
interessiert und fassen gleichartige Objekte in Gruppen zusammen.
3
Die Informatik hat diese Betrachtungsweise aufgegrien und zu einem Programmierparadigma entwickelt.
Der Begri Objekt umfasst Gegenstände, Personen, Sachverhalte oder Ereignisse. Jedes
einzelne Objekt lässt sich durch drei Aspekte charakterisieren: es besitzt einen eindeutigen Zustand, ein wohldeniertes Verhalten und eine Identität.
Der Zustand eines Objektes repräsentiert alle seine relevanten Eigenschaften und ist
durch eine Menge von Attributen und deren Werte bestimmt. Durch sie wird die Struktur der Objekte vereinbart. Die Attributmenge eines Objektes ändert sich normalerweise nicht, die konkreten Werte sind jedoch veränderlich.
Das Verhalten wird in der objektorientierten Programmierung durch eine Menge von
Methoden beschrieben. Sie bestimmen die Art und Weise, in der ein Objekt, in Abhängigkeit von seinem Zustand auf Einwirkungen reagiert, und können Attributwerte
manipulieren.
Die Identität eines Objektes ist diejenige Eigenschaft, die es von allen anderen Objekten unterscheidet. Sie stellt sicher, dass alle Objekte unterscheidbar sind, auch wenn sie
zufällig identische Attributwerte besitzen. Dabei muss gerade in der Softwareentwicklung zwischen dem Namen eines Objektes und seiner Identität dierenziert werden, da
der Name der Adressierung dient und ein einzelnes Objekt über verschiedene Namen
angesprochen werden kann.
Objekte können nach bestimmten Kriterien gruppiert werden. Statt jedes Objekt einzeln zu beschreiben, werden gemeinsame Merkmale in Klassen zusammengefasst. Eine
1 [Kan89, S.115, Ÿ8 Allgemeine Anmerkungen zur transzendentalen Ästhetik, Abschnitt iii]
2 [Wit66, S.12, Satz 2.0121]
3 [LS96, S.4]
1
Klasse ist somit eine abstrakte Beschreibung der Struktur und des Verhaltens von
gleichartigen Objekten. Ein konkretes Objekt wird dann auch als Instanz einer Klasse
bezeichnet.
Zu den Grundprinzipien der objektorientierten Programmierung gehören noch weitere
Konzepte:
ˆ
Aggregierung
ˆ
Kopplung
ˆ
Kapselung und Information Hiding
ˆ
Konstruktoren und Destruktoren
ˆ
Instanz- und klassenspezische Komponenten
ˆ
Vererbung
ˆ
Mehrfachvererbung
ˆ
Reimplementierung
ˆ
Polymorphismus
ˆ
Dynamisches Binden
ˆ
Abstrakte Klassen
ˆ
Generische Klassen
ˆ
Assoziation
ˆ
Aggregation
ˆ
Komposition
Einige dieser Konzepte werden im Folgenden anhand einfacher Beispiele näher erläutert. Auf eine ausführliche Darstellung der anderen Prinzipien wird verzichtet. Sie sind
hier nur erwähnt, um die Vielfalt objektorientierter Programmierung zu verdeutlichen.
Nicht alle vorgestellten Konzepte sind rein objektorientiert, objektorientierte Programmierung wird viel mehr durch die Gesamtheit aller Konzepte geprägt. Dies bedeutet andererseits aber nicht, dass jede als objektorientiert bezeichnete Programmiersprache auch alle Konzepte beinhaltet bzw. unterstützt.
4
Der der Informatik-Lehrplan für Thüringen sieht vor, dass die Grundprinzipien des objektorientierten Programmierens im Leistungskurs Informatik behandelt werden. Für
5
die Umsetzung der Beispiele wurden deshalb Programmiersprachen
gewählt, die an
Thüringer Schulen im Einsatz sind.
4 Thüringer Kultusministerium (Hrsg.): Lehrplan für das Gymnasium - Informatik, 1999.
5 Java: Java 2 SDK 5.0; Oberon-2: POW!; Python: Python 2.4.3; ObjectPascal: Delphi 7.
2
2 Beispiel: Konto
Zur näheren Erläuterung zentraler Eigenschaften objektorientierter Programmierung
soll zunächst ein Bankkonto schrittweise modelliert werden.
Jedes Bankkonto besitzt üblicherweise eine eindeutige Kontonummer und einen aktuellen Kontostand. Natürlich sind noch weitere Kennzeichen - z.B. Kontoinhaber denkbar. Sie werden jedoch zunächst vernachlässigt, damit die Quelltexte überschaubar bleiben. Typische Aktionen, die auf einem Konto ausgeführt werden, sind das Einbzw. Auszahlen von Geldbeträgen und das Drucken eines Kontoauszugs.
2.1 Aggregierung und enge Kopplung
In der prozeduralen Programmierung werden zunächst die für die Beschreibung der
Daten notwendigen Datenstrukturen geschaen und danach werden die Funktionen
entwickelt, mit deren Hilfe die Inhalte der vorhandenen Datenstrukturen ausgegeben
bzw. manipuliert werden können. Obwohl sich die Funktionen auf die Datenstrukturen
beziehen, können Daten und Funktionen nicht als eine Einheit betrachtet werden. Insbesondere wenn mehrere Datenstrukturen deklariert werden müssen, ist die Zuordnung
der Funktionen nicht sofort ersichtlich.
In der objektorientierten Programmierung werden zusammengehörige Attribute (Daten) und Methoden (Funktionen) zu einer manipulierbaren Gröÿe - einer Klasse - zu-
6
sammengefasst. Sie bilden sozusagen ein Aggregat , dessen Komponenten aufeinander
Bezug nehmen können.
In unserem Beispiel werden alle notwendigen Attribute und Methoden für ein Bankkonto in der Klasse Konto zusammengefasst, so dass sich das in Abb. 1 gezeigte Modell
ergibt.
Die Methoden einzahlen(betrag) und auszahlen(betrag) verändern den Konto-
Abbildung 1: Klasse Konto
stand eines Bankkontos, d.h. sie müssen auf das Attribut saldo zugreifen und dessen
Kapselung verwendet. Um Verwechselungen mit dem
Datenkapselung - im Sinne des Information Hiding - zu vermeiden, wird hier der Begri
Aggregierung (ein Aggregat bilden) benutzt.
6 In der Literatur wird häug der Begri
Begri
3
Wert verändern. Die Methode auszug() bezieht sich sogar auf die beiden Attribute der
Klasse, da sie kontonummer und saldo ausgibt. Daran zeigt sich die enge Kopplung
zwischen Attributen und Methoden.
Java
In Java wird ein Klasse über das Schlüsselwort
class
deniert. Alle Variablen, Funk-
tionen bzw. Prozeduren, die innerhalb der geschweiften Klammern deniert werden,
gehören damit zur Klasse und bilden deren Attribute und Methoden. Innerhalb einer Klasse können Attribute und Methoden direkt über ihren Namen angesprochen
werden. Eine Quelldatei sollte nur eine Klassendenition enthalten und den gleichen
Namen wie die Klasse tragen.
//Datei: Konto1/Konto.java
//Beispiel : Aggregierung und Kopplung
// −−> Aggregierung
class Konto{
// Attribute
int kontonummer;
double saldo;
// Methoden
void einzahlen(double betrag){
}
saldo = saldo + betrag; // −−> Kopplung
void auszahlen(double betrag){
}
saldo = saldo − betrag; // −−>Kopplung
void auszug(){
}
}
System.out.println("Kontonummer: "+kontonummer+" Saldo: "+saldo+" Euro");
Java ist eine rein objektorientierte Sprache, so dass jedes Programm eine Klasse deniert. Deshalb wird das Testprogramm für die Klasse Konto in eine Klasse KontoTest
eingebettet, die nur die Methode main enthält.
Um von der Klasse Konto ein Objekt bzw. eine Instanz zu erzeugen, wird eine Variable
vom Typ Konto deklariert. Der Operator
new
legt, mit Hilfe der vom Compiler er-
zeugten Methode Konto(), ein neues Konto -Objekt an, das anschlieÿend der Variablen
meinKonto zugewiesen wird. Dabei gilt: Variablen, deren Typ eine Klasse ist, enthalten
nicht das eigentliche Objekt, sondern nur einen Adressverweis auf dessen Speicherplatz.
Insbesondere bedeutet dies, dass in meinKonto nur die Speicheradresse des Objektes
abgelegt ist (siehe Abb. 2).
Für den Zugri auf Attribute oder Methoden eines Objektes wird eine Punktnotation verwendet. Attribute können mittels Referenz.Attribut gelesen und deren Werte
verändert werden. Methoden werden durch Referenz.Methode() aufgerufen.
4
Abbildung 2: Objektreferenz
// Datei: Konto1/KontoTest.java
// Beispiel : Aggregierung und Kopplung
public class KontoTest{
public static void main(String[] args){
Konto meinKonto; // Variable vom Typ Konto
meinKonto = new Konto(); // new erzeugt Instanz der Klasse Konto
meinKonto.kontonummer = 4531088; // Zugri auf Attribute
meinKonto.saldo = 200.00;
System.out.println("Kontonummer: "+meinKonto.kontonummer +" Saldo: "+ meinKonto.saldo);
meinKonto.einzahlen(300.00); // Methodenaufruf
meinKonto.auszug();
System.out.println ();
}
}
Konto deinKonto = new Konto(); // deinKonto zeigt auf neue Instanz der Klasse Konto
deinKonto.kontonummer = 1733065;
deinKonto.saldo = 1000.00;
deinKonto.auszug();
deinKonto.auszahlen(100.00);
deinKonto.auszug();
Oberon-2
Oberon-2 verwendet sogenannte typgebundene Prozeduren, um die Zugehörigkeit der
Methoden zur Klasse deutlich zu machen.
Die Attribute einer Klasse werden als Felder eines Recordtyps dargestellt. Da Oberon
nicht automatisch mit Referenzvariablen arbeitet, wird auÿerdem ein Referenztyp zum
Recordtyp der Klasse deniert.
Die Implementierung der Methoden muss zusätzlich einen speziellen Parameter vor
dem Prozedurnamen besitzen. Dieser Parameter wird Empfänger genannt und repräsentiert das Objekt, für das die Methode aufgerufen wird. Der Typ des Empfängers
gibt an, zu welcher Klasse die Methode gehört. Im Prozedurrumpf wird der Empfänger
genutzt, um auf die Attribute der Klasse zuzugreifen. Dabei wird wieder die Punktnotation (Empfänger.Attribut ) angewendet.
In Oberon-2 müssen zunächst alle Datentypen deklariert werden, bevor Prozeduren
implementiert werden können. Deshalb sollte jede Klasse in einem separaten Modul
beschrieben werden. Sind mehrere Klassendenitionen in einem Modul enthalten, geht
schnell die Übersichtlichkeit verloren und es ist nicht mehr klar erkennbar, welche Methoden zu welchem Datentyp gehören.
5
(* Datei: Konto1/Konto.mod *)
(* Beispiel : Aggregierung und Kopplung *)
MODULE Konto;
IMPORT Out;
TYPE
(* Attribute *)
Konto* = RECORD
kontonummer* : LONGINT;
saldo * : REAL
END;
(* zugehoeriger Referenztyp *)
KontoRef* = POINTER TO Konto;
(* Methoden *)
(* −−> Aggregierung durch typgebundene Prozeduren*)
PROCEDURE (k : KontoRef) einzahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo + betrag; (* −−> Kopplung *)
END einzahlen;
PROCEDURE (k : KontoRef) auszahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo − betrag; (* −−> Kopplung *)
END auszahlen;
PROCEDURE (k : KontoRef) auszug*();
BEGIN
Out.String("Kontonummer: ");
Out.Int(k.kontonummer, 10);
Out.String(" Saldo: ");
Out.Real(k.saldo , 10);
Out.String(" Euro");
Out.Ln;
END auszug;
BEGIN
END Konto.
Um eine Instanz der Klasse Konto zu erzeugen, wird eine Variable vom Typ KontoRef
deklariert. Der Aufruf
NEW(meinKonto) legt ein Objekt vom Typ Konto an, d.h. er
stellt entsprechenden Speicherplatz zur Verfügung. Zudem bewirkt er, dass meinKonto
auf dieses Objekt verweist. Über die Variable meinKonto können die Attribute der
Instanz angesprochen werden (meinKonto.Attribut ). Für den Aufruf von Methoden
wird ebenfalls die Punktnotation (meinKonto.Methode() ) verwendet.
(* Datei: Konto1/KontoTest.mod *)
(* Beispiel : Aggregierung und Kopplung *)
MODULE KontoTest;
IMPORT Konto, Out;
PROCEDURE ProgMain*();
VAR meinKonto, deinKonto : Konto.KontoRef; (* Variablen vom Referenztyp Konto *)
BEGIN
NEW(meinKonto); (* NEW erzeugt Instanz der Klasse Konto *)
6
meinKonto.kontonummer := 4531088; (* Zugri auf Attribute *)
meinKonto.saldo := 200.00;
Out.String("Kontonummer: ");
Out.Int(meinKonto.kontonummer, 10);
Out.String(" Saldo: ");
Out.Real(meinKonto.saldo, 10);
Out.Ln;
meinKonto.einzahlen(300.00); (* Methodenaufruf *)
meinKonto.auszug();
Out.Ln;
NEW(deinKonto); (* deinKonto zeigt auf neue Instanz der Klasse Konto *)
deinKonto.kontonummer := 1733065;
deinKonto.saldo := 1000.00;
deinKonto.auszug();
deinKonto.auszahlen(100.00);
deinKonto.auszug();
END ProgMain;
BEGIN
END KontoTest.
2.2 Datenkapselung
7
Das Prinzip der Datenkapselung wurde bereits 1972 von David Parnas propagiert . Er
prägte damals den Begri Information Hiding. Im Deutschen wird oft die Bezeichnung
Geheimnisprinzip verwendet.
Für den Benutzer einer Klasse ist relevant, wie er eine Komponente verwenden kann,
nicht wie sie realisiert ist. Deshalb wird die Struktur eines Objektes und die Implementierung der Methoden nicht oen gelegt, sondern in der Klasse verborgen. Der Zugri
auf die gekapselten Attribute und deren Manipulation ist dann ausschlieÿlich über die
Methoden der Klasse durchführbar. So wird sichergestellt, dass Attributwerte nicht
willkürlich geändert und nur zulässige Werte gespeichert werden.
Das Verstecken der Implementierungsdetails vermindert die Fehleranfälligkeit, da ein
unerwarteter Zugri unmöglich ist. Änderungen der Implementierung haben keine Auswirkung auf den Nutzer, da dieser die Zugrismethoden weiterhin verwenden kann.
Im Fall der Klasse Konto sollen die Attribute kontonummer und saldo vor unkontrolliertem Zugri geschützt werden. Da das Lesen und Verändern der Attribute aber
zunächst noch möglich sein soll, werden neue Methoden benötigt. Die Methoden get-
Kontonummer() und setKontonummer(nummer) ermöglichen Lese- und Schreibzugri
auf das Attribut kontonummer. Für die Manipulation von saldo wurden die Operationen getSaldo() und setSaldo(betrag) deniert.
7 Das Konzept der Datenkapselung ist ein wichtiges Prinzip der Informatik, das nicht nur in der
objektorientierten Programmierung eingesetzt wird.
7
Java
Die Zugrisrechte werden in Java über sogenannte Modizierer festgelegt.
Wird ein Attribut ohne Zusatz deklariert, so ist es in allen Klassen des deklarierenden
Paketes
8
sichtbar, nicht jedoch in anderen Paketen. Das Schlüsselwort
public erlaubt
lesenden und schreibenden Zugri, d.h. die Attribute sind in allen Paketen sichtbar.
Sollen Attribute nur innerhalb der Klasse sichtbar sein, so müssen sie mit dem Modizierer
private
gekennzeichnet werden.
Da die Attribute kontonummer und saldo versteckt werden sollen, sind sie ab sofort
mit dem Vorsatz
private
versehen.
Modizierer regeln auch die Sichtbarkeit von Methoden. Da die Methoden der Klasse
Konto in jedem Paket zugänglich sein sollen, müssen sie als
Das Schlüsselwort
public
public deklariert werden.
public kann auch auf Klassen angewendet werden. Klassen, die mit
gekennzeichnet sind, können überall benutzt werden. In einer Quelldatei darf
jedoch höchstens eine Klasse als
public deklariert werden.
//Datei: Konto2/Konto.java
//Beispiel : Datenkapselung
public class Konto{
private int kontonummer; // −−> Datenkapselung
private double saldo;
// neue Methoden fuer sicheren Zugri auf Attribut kontonummer
public int getKontonummer(){
return kontonummer;
}
public void setKontonummer(int nummer){
}
kontonummer = nummer;
// neue Methoden fuer sicheren Zugri auf Attribut saldo
public double getSaldo(){
return saldo;
}
public void setSaldo(double betrag){
}
saldo = betrag;
// urspruengliche Methoden
public void einzahlen(double betrag){
}
saldo = saldo + betrag;
public void auszahlen(double betrag){
}
saldo = saldo − betrag;
public void auszug(){
}
}
System.out.println("Kontonummer: "+kontonummer+" Saldo: "+saldo+" Euro");
8 Pakete bestehen aus mehreren Klasse, die zu einer gröÿeren Einheit zusammengefasst werden.
Um eine Klasse einem bestimmten Paket zuzuordnen muss die Anweisung
erste Anweisung im Quellcode stehen.
8
package
Paketname; als
Auÿerhalb der Klasse Konto ist ein direkter Zugri auf die Attribute kontonummer
und saldo nicht mehr möglich. Der Befehl meinKonto.kontonummer = 4531088 führt
zu einem Fehler bei der Übersetzung. Sogar der Versuch lesend auf die Variable zuzugreifen, erzeugt eine Fehlermeldung. Die Attribute können nur über entsprechende
Methoden angesprochen werden.
//Datei: Konto2/KontoTest.java
//Beispiel : Datenkapselung
public class KontoTest{
public static void main(String[] args){
Konto meinKonto = new Konto();
/* meinKonto.kontonummer = 4531088; !! Fehler
System.out.println(meinKonto.kontonummer); !! Fehler */
meinKonto.setKontonummer(4531088); // schreibender Zugri auf Attribute ueber entsprechende Methoden
meinKonto.setSaldo(200.00);
System.out.print("Kontonummer: "+meinKonto.getKontonummer()); // lesender Zugri
System.out.println(" Saldo: "+meinKonto.getSaldo()+" Euro");
}
}
Oberon-2
In Oberon-2 werden Attribute und Methoden nur dann exportiert, wenn sie in ihrer
Deklaration mit einem Stern (*) markiert sind. Wird die Markierung weggelassen, so
sind die Namen nur innerhalb des Moduls sichtbar, in der die Klasse implementiert
wird. Andere Module, die das Modul importieren, können nur auf die exportierten
Attribute und Methoden zugreifen.
In der Klasse Konto wird der Stern an den Attributen kontonummer und saldo entfernt.
Der Name des Record, der zugehörige Referenztyp und die Methoden sollen weiterhin
sichtbar sein und behalten deshalb ihre Markierung.
(* Datei: Konto2/Konto.mod *)
(* Beispiel : Datenkapselung *)
MODULE Konto;
IMPORT Out;
TYPE
Konto* = RECORD
kontonummer : LONGINT; (* −−> Datenkapselung *)
saldo : REAL
END;
KontoRef* = POINTER TO Konto;
(* neue Methoden fuer sicheren Zugri auf Atrribut kontonummer *)
PROCEDURE (k : KontoRef) getKontonummer*() : LONGINT;
BEGIN
RETURN k.kontonummer;
END getKontonummer;
PROCEDURE (k : KontoRef) setKontonummer*(nummer : LONGINT);
BEGIN
k.kontonummer := nummer;
END setKontonummer;
9
(* neue Methoden fuer sicheren Zugri auf Atrribut saldo *)
PROCEDURE (k : KontoRef) getSaldo*() : REAL;
BEGIN
RETURN k.saldo;
END getSaldo;
PROCEDURE (k : KontoRef) setSaldo*(betrag : REAL);
BEGIN
k.saldo := k.saldo + betrag;
END setSaldo;
(* urspruengliche Methoden *)
PROCEDURE (k : KontoRef) einzahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo + betrag;
END einzahlen;
PROCEDURE (k : KontoRef) auszahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo − betrag;
END auszahlen;
PROCEDURE (k : KontoRef) auszug*();
BEGIN
Out.String("Kontonummer: ");
Out.Int(k.kontonummer, 10);
Out.String(" Saldo: ");
Out.Real(k.saldo , 10);
Out.String(" Euro");
Out.Ln;
END auszug;
BEGIN
END Konto.
Die Namen kontonummer und saldo werden vom Modul Konto nicht mehr exportiert.
Die Verwendung dieser Namen im Modul KontoTest führt deshalb zu der Fehlermeldung Error 83: undened record eld . Um die Attribute trotzdem zu bearbeiten,
benutzt man die set- und get-Methoden.
(* Datei: Konto2/KontoTest.mod *)
(* Beispiel : Datenkapselung *)
MODULE KontoTest;
IMPORT Konto, Out;
PROCEDURE ProgMain*();
VAR meinKonto : Konto.KontoRef;
BEGIN
NEW(meinKonto);
(* meinKonto.kontonummer = 4531088; !! Fehler
Out.Int(meinKonto.kontonummer); !! Fehler *)
meinKonto.setKontonummer(4531088); (* schreibender Zugri auf Attribute ueber entsprechende Methoden *)
meinKonto.setSaldo(200.00);
Out.String("Kontonummer: ");
Out.Int(meinKonto.getKontonummer(),10); (* lesender Zugri *)
Out.String(" Saldo: ");
Out.Real(meinKonto.getSaldo(),10);
Out.String(" Euro");
END ProgMain;
10
BEGIN
Out.Open
END KontoTest.
2.3 Konstruktor
Konstruktoren sind spezielle Methoden einer Klasse, die immer dann zum Einsatz kommen, wenn ein neues Objekt erzeugt werden soll. Sie sind eine elegante Möglichkeit,
Objekte zu initialisieren, können aber auch individuelles Verhalten implementieren.
Die Klasse Konto erhält einen Konstruktor, der als Parameter eine Nummer übergeben bekommt. Diese Nummer dient dem Konstruktor als Initialwert für das Attribut
kontonummer. Die Methode setKontonummer(nummer) wird gelöscht, da eine Kontonummer einmalig bei der Erönung des Kontos vergeben wird. Der Wert von saldo
wird durch den Konstruktor auf 0.00 gesetzt. Änderungen des Kontostandes sollen
anschlieÿend nur über die Methoden einzahlen(betrag) und auszahlen(betrag) möglich
sein, weshalb setSaldo(betrag) entfernt wird.
Java
In Java ist der Konstruktor eine Methode, die den gleichen Namen, wie die zugehörige
Klasse trägt. Er unterscheidet sich bei der Denition von anderen Methoden dadurch,
dass kein Objekttyp für die Rückgabe speziziert werden darf.
Der Konstruktor wird bei der Erzeugung eines Objektes nach dem Operator
new
auf-
gerufen. Ein expliziter Aufruf der Form: Referenz.Konstuktor() ist nicht möglich.
Ist für eine Klasse kein Konstruktor explizit deklariert, dann baut der Compiler einen
Standardkonstruktor ohne Parameter ein. Dieser Konstruktor initialisiert alle Attribute mit NULL.
//Datei: Konto3/Konto.java
//Beispiel : Konstruktor
public class Konto{
private int kontonummer;
private double saldo;
// −−> Konstruktor
public Konto(int nummer){
kontonummer = nummer;
saldo = 0.0;
}
public int getKontonummer(){
return kontonummer;
}
// setKontonummer(nummer) entfernt
11
public double getSaldo(){
return saldo;
}
// setSaldo(betrag) entfernt
public void einzahlen(double betrag){
}
saldo = saldo + betrag;
public void auszahlen(double betrag){
}
saldo = saldo − betrag;
public void auszug(){
}
}
System.out.println("Kontonummer: "+kontonummer+" Saldo: "+saldo+" Euro");
Bisher wurden die Instanzen der Klasse Konto mit dem Standardkonstruktor Konto()
erstellt. Dieser wird jetzt durch den neuen Konstruktor Konto(nummer) ersetzt. Er
übernimmt die Initialisierung der Attribute, so dass dies nicht mehr explizit gesetzt
werden müssen.
//Datei: Konto3/Konto.java
//Beispiel : Konstruktor
public class KontoTest{
public static void main(String[] args){
Konto meinKonto;
/* bisher :
meinKonto = new Konto(); // Objekt erzeugen
meinKonto.setKontonummer(4531088); // Kontonummer initialisieren
meinKonto.setSaldo(0.00); // Kontostand initialisieren */
/* jetzt :
*/
meinKonto = new Konto(4531088); // Konstruktor erzeugt Objekt und initialisiert Attribute
meinKonto.auszug();
}
}
Oberon-2
Im Sprachkonzept von Oberon-2 sind Konstruktoren nicht enthalten. Sie können aber
durch entsprechend implementierte Methoden nachgebaut werden.
Im Beispiel der Klasse Konto wurde eine Methode Konto(nummer) deniert, die als
Konstruktor verwendet wird. Diese Methode ist verantwortlich für die Initialisierung
der Attribute kontonummer und saldo.
(* Datei: Konto3/Konto.mod *)
(* Beispiel : Konstruktor *)
MODULE Konto;
IMPORT Out;
12
TYPE
Konto* = RECORD
kontonummer : LONGINT;
saldo : REAL
END;
KontoRef* = POINTER TO Konto;
(* −−> Konstruktor *)
PROCEDURE (k : KontoRef) Konto*(nummer : LONGINT);
BEGIN
k.kontonummer := nummer;
k.saldo := 0.0;
END Konto;
PROCEDURE (k : KontoRef) getKontonummer*() : LONGINT;
BEGIN
RETURN k.kontonummer;
END getKontonummer;
(* setKontonummer(nummer) entfernt *)
PROCEDURE (k : KontoRef) getSaldo*() : REAL;
BEGIN
RETURN k.saldo;
END getSaldo;
(* setSaldo(betrag) entfernt *)
PROCEDURE (k : KontoRef) einzahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo + betrag;
END einzahlen;
PROCEDURE (k : KontoRef) auszahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo − betrag;
END auszahlen;
PROCEDURE (k : KontoRef) auszug*();
BEGIN
Out.String("Kontonummer: ");
Out.Int(k.kontonummer, 10);
Out.String(" Saldo: ");
Out.Real(k.saldo , 10);
Out.String(" Euro");
Out.Ln;
END auszug;
BEGIN
END Konto.
Instanzen einer Klasse werden weiterhin durch den Operator
NEW
erzeugt. Die Me-
thode Konto(nummer) initialisiert lediglich die Attribute. Da Konto(nummer) eine
normale Methode ist, kann sie - im Gegensatz zu Konstruktoren in Java - uneingeschränkt genutzt und mittels Referenz.Konto(nummer) aufgerufen werden. Der Programmierer muss selbst darauf achten, dass sie nur nach der Generierung eines neuen
Objektes verwendet wird, da sie sonst eventuell die Daten eines bereits bestehenden
Objektes überschreibt.
9
9 Eine weitere Variante der Konstruktormethode für Oberon-2-Programme, die den NEW-Operator
beinhaltet, wird in den Beispielen
Mediendatenbank und geometrische Figuren vorgestellt.
13
(* Datei: Konto3/KontoTest.mod *)
(* Beispiel : Konstruktor*)
MODULE KontoTest;
IMPORT Konto, Out;
PROCEDURE ProgMain*();
VAR meinKonto : Konto.KontoRef;
BEGIN
NEW(meinKonto); (* Objekt erzeugen *)
(* bisher :
meinKonto.setKontonummer(4531088); // Kontonummer initialisieren
meinKonto.setSaldo(0.00); // Kontostand initialisieren *)
(* jetzt :
*)
meinKonto.Konto(4531088); (* Konstruktor initialisiert Attribute *)
meinKonto.auszug;
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.4 Klassen- und instanzspezische Komponenten
Für Klassen können statische Variablen und Methoden deniert werden. Das Besondere an diesen Komponenten ist, dass sie nicht auf die Existenz von Instanzen der Klasse
angewiesen sind.
Von Klassenvariablen existiert, unabhängig von der Anzahl der Instanzen, nur ein
Exemplar, das von allen Objekten der Klasse gemeinsam genutzt wird. Sie werden
beim Laden der Klasse erzeugt und erst freigegeben, wenn das Programm endet. Während der Zugri auf Instanzvariablen durch den Befehl Instanz.Variablenname erfolgt,
werden klassenspezische Variablen in der Form: Klassenname.Variablenname angesprochen. Wenn ein Attribut für alle Instanzen einer Klasse den gleichen Wert haben
soll, ist die Verwendung von Klassenvariablen angebracht. Anstatt den Wert in jedem
einzelnen Objekt anzulegen, wird er in der Klasse gespeichert.
Klassenmethoden werden eingesetzt um Verhalten zu implementieren, dass für alle
Instanzen einer Klasse gleich ist. Man muss jedoch darauf achten, dass sie nicht auf Instanzvariablen zugreifen oder Instanzmethoden aufrufen, da sie auch aufgerufen werden
dürfen, wenn keine Instanz existiert. Zudem wäre in solch einer Situation nicht eindeutig festgelegt, welche Instanz verwendet werden muss. Klassenmethoden werden genau wie Klassenvariablen - über den Name der Klasse aufgerufen.
Die Klasse Konto könnte zum Beispiel einen Instanzenzähler enthalten, der festhält,
wie viele Objekte vom Typ Konto erzeugt wurden. Der Wert der Variablen muss also
immer dann um 1 erhöht werden, wenn ein neues Objekt angelegt wird. Deshalb sollte
14
die entsprechende Anweisung im Konstruktor implementiert werden. Zu Beginn der
Programm-Abarbeitung steht der Zähler auf 0, da noch keine Instanz existiert.
Java
Klassenvariablen und -methoden werden in Java durch das Schlüsselwort
static
ge-
kennzeichnet.
Im Beispiel der Klasse Konto wird das Attribut zaehler durch den Vorsatz
static
zu
einer Klassenvariablen. Unabhängig von der Anzahl der Konto -Objekte existiert nur
ein Exemplar dieser Variablen. Die Attribute kontonummer und saldo sind Instanzvariablen. Sie werden für jede Instanz der Klasse Konto neu angelegt (siehe Abb.3).
Abbildung 3: Instanz- und Klassenvariablen
//Datei: Konto4/Konto.java
//Beispiel : Klassen− und Instanzvariablen
public class Konto{
static int zaehler = 0; // −−> Klassenvariable
private int kontonummer; // −−> Instanzvariablen
private double saldo;
public Konto(int nummer){
}
zaehler = zaehler + 1;
kontonummer = nummer;
saldo = 0.00;
public int getKontonummer(){
return kontonummer;
}
15
public double getSaldo(){
return saldo;
}
public void einzahlen(double betrag){
}
saldo = saldo + betrag;
public void auszahlen(double betrag){
}
saldo = saldo − betrag;
public void auszug(){
}
}
System.out.println("Kontonummer: "+kontonummer+" Saldo: "+saldo+" Euro");
Für die Variable zaehler wird beim Laden der Klasse Konto Speicherplatz belegt, der
mit 0 vorinitialisiert ist. Nachdem durch den Konstruktor eine Instanz erzeugt wurde,
ist der Wert von zaehler 1. Das Anlegen einer weiteren Instanz bewirkt eine erneute
Erhöhung.
// Datei: Konto4/KontoTest.java
// Beispiel : Klassenvariablen
public class KontoTest{
public static void main(String[] args){
// Zugri auf Klassenvariablen ueber Klassennamen
System.out.println("Anzahl Konten: "+Konto.zaehler);
Konto meinKonto = new Konto(4531088);
System.out.println("Anzahl Konten: "+Konto.zaehler);
}
}
Konto deinKonto = new Konto(1733065);
System.out.println("Anzahl Konten: "+Konto.zaehler);
Oberon-2
In Oberon-2 gibt es keine statischen Komponenten im eigentlichen Sinne. Die Datenfelder des Recordtyps sind Instanzvariablen und werden für jedes Objekt neu angelegt.
Klassenvariablen können somit nicht im Record deklariert werden. Stattdessen werden
sie im Variablendeklarationsteil des Moduls deniert. Man muss jedoch beachten, dass
diese Variablen nicht an eine Klasse gebunden sind. Viel mehr sind es Modulvariablen,
die von auÿen mittels Modulname.Variablenname angesprochen werden. Dies ist insbesondere dann wichtig, wenn der Klassenname von Namen des Moduls abweicht oder
mehrere Klassen in einem Modul implementiert sind.
Ähnliches gilt für Methoden. Instanzmethoden werden durch einen Empfängerparameter dem Recordtyp zugeordnet. Wird dieser Parameter weggelassen, so sind die
Prozeduren statisch. Sie können in Modulen, über den Modulnamen aufgerufen werden.
16
In unserem Beispiel wird im Modul Konto eine Integer-Variable zaehler deklariert.
Der Konstruktor der Klasse Konto verwendet diese Variable, um die Anzahl der Instanzen zu zählen. Um auf den Wert der Variable zuzugreifen, genügt innerhalb des
deklarierenden Moduls der Variablenname.
(* Datei: Konto4/Konto.mod *)
(* Beispiel : Klassen− und Instanzvariablen *)
MODULE Konto;
IMPORT Out;
TYPE
Konto* = RECORD
kontonummer : LONGINT; (* −−> Instanzvariablen *)
saldo : REAL
END;
KontoRef* = POINTER TO Konto;
(* −−> Modulvariable, ersetzt Klassenvariable *)
VAR zaehler* : INTEGER;
PROCEDURE (k : KontoRef) Konto*(nummer : LONGINT);
BEGIN
zaehler := zaehler + 1;
k.kontonummer := nummer;
k.saldo := 0.0;
END Konto;
PROCEDURE (k : KontoRef) getKontonummer*() : LONGINT;
BEGIN
RETURN k.kontonummer;
END getKontonummer;
PROCEDURE (k : KontoRef) getSaldo*() : REAL;
BEGIN
RETURN k.saldo;
END getSaldo;
PROCEDURE (k : KontoRef) einzahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo + betrag;
END einzahlen;
PROCEDURE (k : KontoRef) auszahlen*(betrag : REAL);
BEGIN
k.saldo := k.saldo − betrag;
END auszahlen;
PROCEDURE (k : KontoRef) auszug*();
BEGIN
Out.String("Kontonummer: ");
Out.Int(k.kontonummer, 10);
Out.String(" Saldo: ");
Out.Real(k.saldo , 10);
Out.String(" Euro");
Out.Ln;
END auszug;
BEGIN
zaehler := 0;
END Konto.
17
(* Datei: Konto4/KontoTest.mod *)
(* Beispiel : Klassen− und Instanzvariablen*)
MODULE KontoTest;
IMPORT Konto, Out;
PROCEDURE ProgMain*();
VAR meinKonto, deinKonto : Konto.KontoRef;
BEGIN
Out.String("Anzahl Konten: ");
(* Zugri auf Klassenvariablen ueber Modulnamen *)
Out.Int(Konto.zaehler,2);
Out.Ln;
NEW(meinKonto);
meinKonto.Konto(4531088);
Out.String("Anzahl Konten: ");
Out.Int(Konto.zaehler,2);
Out.Ln;
NEW(deinKonto);
meinKonto.Konto(1733065);
Out.String("Anzahl Konten: ");
Out.Int(Konto.zaehler,2);
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.5 Manipulierbarkeit
Instanzen einer Klasse können wie andere Werte manipuliert werden. Dazu gehört unter
anderem, dass sie einer anderen Referenzvariable vom selben Typ zugewiesen werden
können. Im Programmbeispiel werden drei Referenzvariablen angelegt, aber nur zwei
Instanzen erzeugt. Die Zuweisung sorgt dafür, dass die beiden Variablen meinKonto
und unserKonto auf dasselbe Objekt verweisen, während deinKonto die zweite Instanz
referenziert (siehe Abb. 4).
Auÿerdem können Objekte auf Gleichheit und Ungleichheit getestet werden. Wichtig
ist dabei, dass nur Zeigervergleiche und keine Wertvergleiche stattnden. Obwohl die
Kontonummer und der Kontostand von meinKonto und deinKonto identisch sind, liefert der Vergleich den Wert FALSE. Nur der Vergleich von deinKonto und unserKonto
ergibt TRUE.
Abbildung 4: Zuweisung und Vergleich von Objekten
18
Es ist möglich Prozeduren oder Funktionen zu denieren, die Klasseninstanzen als Parameter haben. Für die Klasse Konto wurde eine Prozedur ueberweisen implementiert,
die als Argumente zwei Konto -Objekte nimmt und einen vorgegebenen Betrag von
einem auf das andere überweist.
Java
//Datei: Konto5/Konto.java
//Beispiel : Manipulierbarkeit
public class KontoTest{
static void ueberweisen(double betrag, Konto kontoA, Konto kontoZ){
}
kontoA.auszug();
kontoZ.auszug();
kontoA.auszahlen(betrag);
kontoZ.einzahlen(betrag);
System.out.print("Von Konto "+kontoA.getKontonummer());
System.out.print(" wurden "+betrag+" Euro ");
System.out.println("auf Konto "+kontoZ.getKontonummer()+" ueberwiesen.");
kontoA.auszug();
kontoZ.auszug();
public static void main(String[] args){
Konto meinKonto = new Konto(4531088);
meinKonto.einzahlen(300.00);
Konto deinKonto = new Konto(4531088);
deinKonto.einzahlen(300.00);
Konto unserKonto;
// Zuweisung
unserKonto=deinKonto;
// Vergleich
if (meinKonto == deinKonto)
System.out.println("Mein Konto und dein Konto sind identisch.");
else
System.out.println("Mein Konto und dein Konto sind verschieden.");
if (unserKonto == deinKonto)
System.out.println("Unser Konto und dein Konto sind identisch.");
else
System.out.println("Unser Konto und dein Konto sind verschieden.");
}
}
// Parameter
ueberweisen(30.00, meinKonto, deinKonto);
Oberon-2
(* Datei: Konto5/KontoTest.mod *)
(* Beispiel : Manipulierbarkeit*)
MODULE KontoTest;
IMPORT Konto,Out;
PROCEDURE ueberweisen(betrag : REAL; kontoA, kontoZ : Konto.KontoRef);
BEGIN
kontoA.auszug();
19
kontoZ.auszug();
kontoA.auszahlen(betrag);
kontoZ.einzahlen(betrag);
Out.String("Von Konto ");
Out.Int(kontoA.getKontonummer(),10);
Out.String(" wurden ");
Out.Real(betrag,10);
Out.String(" Euro auf Konto ");
Out.Int(kontoZ.getKontonummer(),10);
Out.String(" ueberwiesen."); Out.Ln;
kontoA.auszug();
kontoZ.auszug();
END ueberweisen;
PROCEDURE ProgMain*();
VAR meinKonto, deinKonto, unserKonto : Konto.KontoRef;
BEGIN
NEW(meinKonto);
meinKonto.Konto(4531088);
meinKonto.einzahlen(300.00);
NEW(deinKonto);
deinKonto.Konto(1733065);
deinKonto.einzahlen(100.00);
(* Zuweisung *)
unserKonto := deinKonto;
(* Vergleich *)
IF meinKonto = deinKonto
THEN Out.String("Mein Konto und dein Konto sind identisch."); Out.Ln;
ELSE Out.String("Mein Konto und dein Konto sind verschieden."); Out.Ln;
END;
IF unserKonto = deinKonto
THEN Out.String("Unser Konto und dein Konto sind identisch."); Out.Ln;
ELSE Out.String("Unser Konto und dein Konto sind verschieden."); Out.Ln;
END;
(* Parameter *)
ueberweisen(30.00, meinKonto, deinKonto);
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.6 Vererbung
Oft modellieren Klassen Dinge der realen Welt, die zwar in verschiedenen Varianten
vorkommen, aber auch grundlegende Gemeinsamkeiten haben. In einer Bank gibt es
z.B. verschiedene Kontoarten. Ein Sparkonto ist ein Konto mit Kontonummer und
Kontostand; zusätzlich zum normalen Konto werden jedoch Zinsen auf das Guthaben
gezahlt. Auÿerdem gibt es noch Girokonten, die über ein bestimmtes Kreditlimit verfügen. Für beide Konten sind das Einzahlen und Auszahlen von Geldbeträgen sowie
das Drucken eines Kontoauszuges typische Handlungen.
Diese Methoden sind bereits in der Klasse Konto implementiert und können durch Kopieren in die Klassen Sparkonto und Girokonto übernommen werden. Damit müssen
20
jedoch drei Versionen des selben Algorithmus gepegt werden. Eine Möglichkeit, diese
Code-Duplizierung zu vermeiden, ist Vererbung.
Vererbung ist eines der mächtigsten und interessantesten Konzepte des objektorientierten Programmierens. Sie ermöglicht, dass neue Klassen auf bereits vorhandenen
Klassen und deren Funktionalität aufbauen.
Haben mehrere Klassen gleiche Eigenschaften, so werden diese in einer Superklasse
zusammengefasst. Wird von dieser Klasse eine Subklasse abgeleitet, so erbt sie alle
Komponenten der übergeordneten Klasse, d.h. Attribute und Methoden werden automatisch übernommen und müssen nicht erneut implementiert werden. Die Subklasse
kann anschlieÿend die für sie relevanten Attribute und Methoden hinzufügen.
Auf diese Art und Weise ist es möglich, hierarchische Beziehungen zwischen einzelnen
Klassen herzustellen. So können z.B. mehrere Klassen von einer Superklasse erben und
eine Subklasse kann wiederum selbst Superklasse weiterer Subklassen sein.
Von der Klasse Konto soll zunächst nur die Klasse Sparkonto abgeleitet werden. Diese
erbt die Attribute kontonummer und saldo, benötigt aber noch ein weiteres Attribut
zinssatz, in dem festgehalten wird, wieviel Prozent Zinsen gezahlt werden. Auf das Attribut kann über die Methoden getZinssatz() und setZinssatz(zins) zugegrien werden.
Für die Buchung der Zinsen auf das jeweilige Konto wird eine zusätzliche Methode
verzinsen() implementiert. Alle weiteren Methoden werden von der Superklasse übernommen. Insgesamt ergibt sich damit das in Abb. 5 dargestellte Klassendiagramm.
Abbildung 5: Vererbung
Java
Um in Java eine neue Klasse aus einer bestehenden abzuleiten, wird im Kopf der Klassenbeschreibung das Schlüsselwort
extends
zusammen mit dem Namen der Super-
klasse verwendet. Die Superklasse gibt dann die Variablen und Methoden, die
oder
protected
public
deklariert sind, an die abgeleiteten Klassen weiter. Komponenten, die
21
als
private
eingestuft sind, werden nicht vererbt. Sollen Attribute oder Methoden für
andere Klassen nicht sichtbar sein, aber trotzdem an die Subklasse vererbt werden,
so müssen sie mit dem Schlüsselwort
protected
gekennzeichnet sein.
sich der Modizierer der Attribute kontonummer und saldo von
Die Methoden der Klasse Konto behalten den Zusatz
10
Deshalb ändert
private in protected.
public, damit sie auch für andere
Klassen sichtbar sind.
//Datei: Konto6/Konto.java
//Beispiel : Vererbung
public class Konto{
static int zaehler = 0;
protected int kontonummer; // Attribute sollen vererbt werden
protected double saldo;
public Konto(int nummer){
}
zaehler = zaehler + 1;
kontonummer = nummer;
saldo = 0.00;
// Implementierung der Methoden hier ausgelassen
}
public int getKontonummer(){}
public double getSaldo(){}
public void einzahlen(double betrag){}
public void auszahlen(double betrag){}
public void auszug(){}
Die Klasse Sparkonto wird durch
extends
von der Klasse Konto abgeleitet. Dadurch
erbt sie alle Eigenschaften der übergeordneten Klasse und kann diese im Programmcode verwenden. Alle Deklarationen die im Rumpf der Klasse gemacht werden, sind
Erweiterungen von Konto.
Konstruktoren werden nicht vererbt, aus diesem Grund benötigt die Klasse Sparkonto
einen eigenen Konstuktor Sparkonto(nummer, zins). In diesem wird mit dem Befehl
super(nummer)
zunächst der Konstruktor der Superklasse aufgerufen, d.h. alle An-
weisungen von Konto(nummer) werden ausgeführt. Anschlieÿend muss nur noch das
neue Attribut zinssatz initialisiert werden.
Die Methode verzinsen() ändert den Kontostand der aufrufenden Instanz. Obwohl das
Attribut saldo nicht in der Klasse Sparkonto deklariert worden ist, kann der Name
verwendet werden.
//Datei: Konto6/Sparkonto.java
//Beispiel : Vererbung
// −−> Vererbung
public class Sparkonto extends Konto{
private double zinssatz; // zusaetzliches Attribut
10 Komponenten für die kein Modizierer angegeben ist, werden nur innnerhalb eines Paketes an
Subklassen vererbt.
22
// Konstuktor der Klasse Sparkonto
public Sparkonto(int nummer, double zins){
super(nummer); // Konstruktor der Superklasse Konto wird aufgerufen
}
zinssatz = zins;
// neue Methoden fuer Zugri auf Attribut zinssatz
public double getZinssatz(){
return zinssatz;
}
public void setZinssatz(double zins){
}
zinssatz = zins;
// neue Methode
public void verzinsen(){
}
}
saldo = saldo + (zinssatz *saldo)/100; // verwendet geerbte Attribute
Eine Instanz der Klasse Sparkonto wird durch den Konstruktor Sparkonto(nummer,zins)
erzeugt und initialisiert. Jedes Objekt vom Typ Sparkonto besitzt die drei Attribute
kontonummer, saldo und zinssatz, die mit Hilfe der Methoden getKontonummer(),
getSaldo() und getZinssatz() ausgelesen werden können. Das Datenfeld zinssatz kann
durch die Methode setZinssatz(zins) beschrieben werden.
Alle Methoden, die für eine Instanz der Klasse Konto deniert sind, können auch auf ein
Objekt vom Typ Sparkonto angewendet werden. Dazu gehören die Methoden einzah-
len(betrag), auszahlen(betrag) und auszug(). Zusätzlich verfügt jedes Sparkonto -Objekt
über die Methode verzinsen().
//Datei: Konto6/KontoTest.java
//Beispiel : Vererbung
public class KontoTest{
public static void main(String[] args){
Sparkonto meinSparkonto = new Sparkonto(5613990,3.4);
// jede Instanz der Klasse Sparkonto hat 3 Attribute
System.out.print("Kontonummer: "+meinSparkonto.getKontonummer()); // geerbtes Attribut
System.out.print(" Saldo: "+meinSparkonto.getSaldo()+" Euro"); //geerbtes Attribut
System.out.println(" Zinssatz: "+meinSparkonto.getZinssatz()); // neu deklariertes Attribut
// Methoden einzahlen(betrag), auszahlen(betrag) und auszug() koennen verwendet werden
meinSparkonto.einzahlen(250.00); // geerbte Methoden
meinSparkonto.auszug();
meinSparkonto.auszahlen(10.00);
meinSparkonto.auszug();
}
}
// ausserdem gibt es fuer Instanzen der Klasse Sparkonto die Methode verzinsen()
meinSparkonto.verzinsen(); // neu implementierte Methode
meinSparkonto.auszug();
23
Oberon-2
In Oberon-2 erbt die Subklasse nur die Komponenten, die vom Modul der Superklasse
exportiert werden. Soll die Klasse Sparkonto die Attribute kontonummer und saldo
enthalten, so müssen diese in der Klasse Konto mit einem Stern markiert sein. Damit
sind sie auch in anderen Modulen sichtbar und nicht mehr gekapselt. Jeder Programmier muss für seine Anwendung selbst entscheiden, ob Datenkapselung oder Vererbung
wichtiger ist. Beide Konzepte können nur dann parallel verwendet werden, wenn Subund Superklasse in einem Modul implementiert sind. Dies geht aber zu Lasten der
Programmlesbarkeit und das Prinzip der Aggregierung wird gelockert.
(* Datei: Konto6/Konto.mod *)
(* Beispiel : Vererbung *)
MODULE Konto;
IMPORT Out;
TYPE
Konto* = RECORD
kontonummer* : LONGINT; (* Attribute sollen vererbt werden *)
saldo * : REAL
END;
KontoRef* = POINTER TO Konto;
VAR zaehler* : INTEGER;
(* Implementierung des Konstruktors und der Methoden wurde hier ausgelassen *)
PROCEDURE (k : KontoRef) Konto*(nummer : LONGINT);
PROCEDURE (k : KontoRef) getKontonummer*() : LONGINT;
PROCEDURE (k : KontoRef) getSaldo*() : REAL;
PROCEDURE (k : KontoRef) einzahlen*(betrag : REAL);
PROCEDURE (k : KontoRef) auszahlen*(betrag : REAL);
PROCEDURE (k : KontoRef) auszug*();
BEGIN
zaehler := 0;
END Konto.
Dass eine Klasse von einer anderen abgeleitet werden soll, drückt man aus, indem
man den Recordtyp der Klasse in Klammern hinter das Schlüsselwort
RECORD
in
die Typdeklaration der neuen Klasse schreibt. Bendet sich die Implementierung der
Superklasse in einem anderen Modul, so muss dieses importierte Modul und der Modulname bei der Typableitung mit angegeben werden.
Die Klasse Sparkonto übernimmt somit alle Attribute und Methoden der Klasse Konto,
inklusive der Konstruktormethode. Im neuen Konstruktor Sparkonto(nummer, zins)
wird diese verwendet, um die geerbten Attribute kontonummer und saldo zu initialisieren. Das neue Attribut zinssatz muss explizit mit einem Startwert belegt werden.
Die zusätzlichen Methoden der Klasse Sparkonto werden wie gewohnt als typgebundene
Prozeduren implementiert.
24
(* Datei: Konto6/Sparkonto.mod *)
(* Beispiel : Vererbung *)
MODULE Sparkonto;
IMPORT Konto, Out;
TYPE
Sparkonto* = RECORD (Konto.Konto) (* −−> Vererbung *)
zinssatz * : REAL; (* zusaetzliches Attribut *)
END;
SparkontoRef* = POINTER TO Sparkonto;
(* Konstuktor der Klasse Sparkonto *)
PROCEDURE (sk : SparkontoRef) Sparkonto*(nummer : LONGINT; zins : REAL);
BEGIN
sk.Konto(nummer); (* Konstruktor der Superklasse Konto wird aufgerufen *)
sk. zinssatz := zins ;
END Sparkonto;
(* neue Methoden fuer Zugri auf Attribut zinssatz *)
PROCEDURE (sk : SparkontoRef) getZinssatz*() : REAL;
BEGIN
RETURN sk.zinssatz;
END getZinssatz;
PROCEDURE (sk : SparkontoRef) setZinssatz*(zins : REAL);
BEGIN
sk. zinssatz := zins ;
END setZinssatz;
(* neue Methode *)
PROCEDURE (sk : SparkontoRef) verzinsen*();
BEGIN
sk.saldo := sk.saldo + (sk. zinssatz *sk.saldo)/100; (* verwendet geerbte Attribute *)
END verzinsen;
BEGIN
END Sparkonto.
Der
NEW -Operator
erzeugt ein Objekt vom Typ Sparkonto und speichert einen
Adressverweis auf dieses Objekt in meinSparkonto. Die Attribute werden durch den
Aufruf der Methode Sparkonto(nummer, zins) mit Werten belegt, so dass sich die in
Abb. 6 dargestellte Situation ergibt.
Für die Instanz meinSparkonto können alle Methoden aufgerufen werden, die in den
Klassen Sparkonto und Konto implementiert wurden.
Abbildung 6: Instanz der Klasse Sparkonto
25
(* Datei: Konto6/KontoTest.mod *)
(* Beispiel : Verbung*)
MODULE KontoTest;
IMPORT Sparkonto, Out;
PROCEDURE ProgMain*();
VAR meinSparkonto : Sparkonto.SparkontoRef;
BEGIN
NEW(meinSparkonto);
meinSparkonto.Sparkonto(5613990,3.4);
(* jede Instanz der Klasse Sparkonto hat 3 Attribute *)
Out.String("Kontonummer: ");
Out.Int(meinSparkonto.getKontonummer(),10); (* geerbtes Attribut *)
Out.String(" Saldo: ");
Out.Real(meinSparkonto.getSaldo(),10); (* geerbtes Attriubut *)
Out.String(" Euro Zinssatz: ");
Out.Real(meinSparkonto.getZinssatz(),10); (* neu deklariertes Attribut *)
Out.Ln;
(* Methoden einzahlen(betrag), auszahlen(betrag) und auszug() koennen verwendet werden *)
meinSparkonto.einzahlen(250.00); (* geerbte Methoden *)
meinSparkonto.auszug();
meinSparkonto.auszahlen(10.00);
meinSparkonto.auszug();
(* ausserdem gibt es fuer Instanzen der Klasse Sparkonto die Methode verzinsen() *)
meinSparkonto.verzinsen(); (* neu implementierte Methode *)
meinSparkonto.auszug();
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.7 Reimplementierung
In vielen Fällen reicht es nicht aus, Methoden unverändert von der Superklasse zu
erben. Für ein Girokonto muss bspw. beim Auszahlen überprüft werden, ob der Über-
11
ziehungskredit überschritten wird. Ist dies der Fall, darf kein Geld ausgezahlt werden.
Die Klasse Girokonto kann trotzdem von Konto abgeleitet werden, die geerbte Methode muss lediglich an die neue Situation angepasst werden.
Wird in einer Subklasse eine Methode der Superklasse mit einer anderen Funktionalität
implementiert, so bezeichnet man diesen Vorgang als Überschreiben oder Reimplementieren der Methode. Das ist dann der Fall, wenn man die Methode in der Subklasse
mit gleichem Rückgabewert, Namen und Parametern deniert und die Anweisungen
im Methodenrumpf ändert. Die Objekte der Subklasse haben dann zwei Methoden mit
gleichem Namen und gleicher Signatur, jedoch wird die ursprüngliche Methode der
Superklasse von der überschreibenden Methode verdeckt.
11 Eine Überprüfung, ob das Konto überzogen wird ist natürlich auch für Konten ohne Kreditrahmen
sinnvoll. Ich habe jedoch darauf verzichtet.
26
Java
Solange eine Methode in der Superklasse nicht mit
nal
gekennzeichnet ist, kann man
sie in einer Subklasse überschreiben. Dazu wird der Methodenkopf aus der übergeordneten Klasse übernommen und die Anweisungen im Methodenrumpf werden der
Aufgabe entsprechend angepasst.
Die Klasse Girokonto wird von Konto abgeleitet und reimplementiert die Methode
auszahlen(betrag). Der gewünschte Betrag wird nicht mehr in allen Fällen vom Konto
abgebucht, sondern nur wenn das Limit nicht überschritten ist. Diese Voraussetzung
überprüft die eingebaute if-Anweisung. Ist die Bedingung erfüllt, ruft der Befehl
per.auszahlen(betrag)
su-
die verdeckte Methode der Superklasse Konto auf. In allen an-
deren Fällen wird eine Fehlermeldung ausgegeben.
Alle anderen Methoden werden unverändert von Konto übernommen.
//Datei: Konto7/Girokonto.java
//Beispiel : Reimplementierung
public class Girokonto extends Konto{
private double limit;
public Girokonto(int nummer, double limit){
super(nummer);
if ( limit < 0)
limit = −limit;
this . limit = limit ; // Parameter this zeigt auf das Objekt fuer das die Methode aufgerufen wird
}
public double getLimit(){
return limit ;
}
// Methode fuer schreibenden Zugri ueberprueft ob limit postiv ist
public void setLimit(double limit){
if ( limit < 0)
limit = −limit;
this . limit = limit ;
}
// −−> Reimplementierung
public void auszahlen(double betrag){
if (betrag <= saldo + limit)
super.auszahlen(betrag);
else
}
}
System.out.println("Limit ueberschritten!");
Genau wie die Klasse Sparkonto erbt Girokonto alle Attribute und Methoden von
Konto. Insbesondere die Methode einzahlen(betrag) kann auf Instanzen der Klasse Girokonto angewendet werden.
Ist auf dem Konto noch genügend Geld vorhanden, so verhält sich die Methode aus-
zahlen(betrag) - wenn sie für ein Girokonto -Objekt aufgerufen wird - genauso, als wäre
das Objekt eine Instanz der Klasse Konto. Der Unterschied zwischen beiden Implementierungen zeigt sich erst wenn man das Konto überzieht. Vom normalen Konto wird
der Betrag abgebucht, während das Girokonto die Auszahlung verweigert.
27
Welche Implementierung verwendet wird, hängt davon ab, von welchem Typ das aufrufende Objekt ist.
//Datei: Konto7/KontoTest.java
//Beispiel : Reimplenetierung
public class KontoTest{
public static void main(String[] args){
Konto meinKonto = new Konto(4531088);
meinKonto.einzahlen(400.00);
meinKonto.auszug();
System.out.println("Versuche 100 Euro abzuheben.");
meinKonto.auszahlen(100.00); // Methode auszahlen(betrag) der Klasse Konto
meinKonto.auszug();
System.out.println("Versuche 500 Euro abzuheben.");
meinKonto.auszahlen(500.00); // Methode auszahlen(betrag) der Klasse Konto
meinKonto.auszug();
System.out.println ();
}
}
Girokonto meinGirokonto = new Girokonto(1733065,100);
meinGirokonto.einzahlen(400.00);
meinGirokonto.auszug();
System.out.println("Versuche 100 Euro abzuheben.");
meinGirokonto.auszahlen(100.00); // Methode auszahlen(betrag) der Klasse Girokonto
meinGirokonto.auszug();
System.out.println("Versuche 500 Euro abzuheben.");
meinGirokonto.auszahlen(500.00); // Methode auszahlen(betrag) der Klasse Girokonto
meinGirokonto.auszug();
Oberon-2
Oberon-2 ermöglicht ebenfalls das Überschreiben von Methoden. Hierzu deklariert man
die Prozedur mit der gleichen Schnittstelle, bindet sie aber durch den Empfängerparameter an die Subklasse. Die verdeckte Methode der Superklasse wird aufgerufen, wenn
an den Methodennamen ein Pfeil (^) angehängt wird.
An der Oberon-Syntax wird nochmal deutlich, dass die Klasse Girokonto tatsächlich
über zwei Varianten der Methode auszahlen(betrag) verfügt, die geerbte Variante aber
nur dann ausgewählt wird, wenn man dies explizit fordert.
Die Methode setLimit(limit) zeigt, warum Attribute nur durch Methoden verändert
werden sollten, auch wenn sie gekapselt sind. Sie stellt sicher, dass der Überziehungskredit immer als positiver Wert gespeichert ist. Dies ist ein Detail der Implementierung,
das in der Methode auszahlen(betrag) zum Tragen kommt. Wären negative Werte möglich, so würde in bestimmen Fällen die Auszahlung verweigert werden, obwohl noch
genügend Geld auf dem Konto ist. Dank der im Konstruktor und in setLimit(limit)
realisierten Betragsbildung muss sich der Anwender jedoch nicht um dieses Problem
kümmern.
28
(* Datei: Konto7/Girokonto.mod *)
(* Beispiel : Reimplementierung *)
MODULE Girokonto;
IMPORT Konto, Out;
TYPE
Girokonto* = RECORD (Konto.Konto)
limit * : REAL;
END;
GirokontoRef* = POINTER TO Girokonto;
PROCEDURE (gk : GirokontoRef) Girokonto*(nummer : LONGINT; limit : REAL);
BEGIN
gk.Konto(nummer);
IF limit < 0 THEN
limit := −limit
END;
gk. limit := limit ;
END Girokonto;
PROCEDURE (gk : GirokontoRef) getLimit*() : REAL;
BEGIN
RETURN gk.limit;
END getLimit;
(* Methode fuer schreibenden Zugri ueberprueft ob limit positiv ist *)
PROCEDURE (gk : GirokontoRef) setLimit*(limit : REAL);
BEGIN
IF limit < 0 THEN
limit := −limit
END;
gk. limit := limit ;
END setLimit;
(* −−> Reimplementierung *)
PROCEDURE (gk : GirokontoRef) auszahlen*(betrag : REAL);
BEGIN
IF betrag <= gk.saldo + gk.limit THEN
gk.auszahlen^(betrag);
ELSE
Out.String("Limit ueberschritten!"); Out.Ln;
END;
END auszahlen;
BEGIN
END Girokonto.
Von den Klassen Konto und Girokonto wird jeweils eine Instanz erzeugt. Für das
e festgelegt. Obwohl beide nach der ersten
Auszahlung einen Kontostand von 300 e haben, können nur vom normalen Konto 500 e
Girokonto wird ein Kreditrahmen von 100
abgehoben werden.
(* Datei: Konto7/KontoTest.mod *)
(* Beispiel : Reimplementierung*)
MODULE KontoTest;
IMPORT Konto, Girokonto, Out;
PROCEDURE ProgMain*();
VAR meinGirokonto : Girokonto.GirokontoRef;
meinKonto : Konto.KontoRef;
29
BEGIN
NEW(meinKonto);
meinKonto.Konto(4531088);
meinKonto.einzahlen(400.00);
meinKonto.auszug();
Out.String("Versuche 100 Euro abzuheben."); Out.Ln;
meinKonto.auszahlen(100.00); (* Methode auszahlen(betrag der Klasse Konto *)
meinKonto.auszug();
Out.String("Versuche 100 Euro abzuheben."); Out.Ln;
meinKonto.auszahlen(500.00); (* Methode auszahlen(betrag der Klasse Konto *)
meinKonto.auszug();
Out.Ln;
NEW(meinGirokonto);
meinGirokonto.Girokonto(1733065,100);
meinGirokonto.einzahlen(400.00);
meinGirokonto.auszug();
Out.String("Versuche 100 Euro abzuheben."); Out.Ln;
meinGirokonto.auszahlen(100.00); (* Methode auszahlen(betrag) der Klasse Girokonto *)
meinGirokonto.auszug();
Out.String("Versuche 500 Euro abzuheben."); Out.Ln;
meinGirokonto.auszahlen(500.00); (* Methode auszahlen(betrag) der Klasse Girokonto *)
meinGirokonto.auszug();
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.8 Polymorphismus
Durch Vererbung ist es möglich, dass Variablen zur Laufzeit auf Objekte von verschiedenem Typ verweisen, d.h. ein Objekt vom Typ einer Subklasse kann einer Variablen
der Superklasse zugewiesen werden. Diese Eigenschaft wird Polymorphismus
12
genannt.
Für jede Variable muss deshalb zwischen ihrem statischen Typ und dem dynamischen
Typ unterschieden werden. Der statische Typ ist der Typ, mit dem sie deklariert wurde. Er bestimmt, auf welche Attribute und Methoden man zugreifen kann, d.h. nur
die Komponenten, die in der Superklasse deklariert wurden, sind ansprechbar. Der dynamische Typ ist der Typ des Objektes, auf das die Variable zur Laufzeit zeigt. Er
bestimmt, welche Methoden aufgerufen werden.
Der dynamische Typ ist zur Übersetzungszeit häug nicht bekannt. Der Compiler
kann bei der Typüberprüfung nur den statischen Typ berücksichtigen. Deshalb muss
die entsprechende Methode dynamisch gebunden werden, d.h. es wird erst zur Laufzeit
entschieden, welche Implementierung beim Aufruf einer Methode zum Einsatz kommt.
Der Vorteil dieser Zuweisungskompatibilität besteht darin, dass Programme, die bisher nur Objekte der Superklasse verwenden, in der Lage sind, automatisch auch mit
12 Hier bezieht sich der Begri Polymorphismus auf polymorphe Variablen. In der Literatur wird
häug auch von polymorphen Operationen gesprochen, die durch Überschreiben und Überladen von
Methoden erzeugt werden.
30
Objekten der Subklasse zu arbeiten. Programmänderungen sind nicht nötig. Die Programme können später sogar mit Klassen arbeiten, die bei der Erstellung noch nicht
existierten.
Die Variable meinKonto wird als Variable vom Typ Konto deklariert. Im Verlauf des
Programmes verweist sie jedoch auf Objekte verschiedener Typen.
Der Befehl meinKonto.auszahlen(500.00) bspw. löst zwei unterschiedliche Reaktionen
aus. Beim ersten Aufruf wird das Geld wie gewünscht ausgezahlt, da meinKonto auf
ein Objekt von Typ Konto zeigt. Zum Zeitpunkt des zweiten Aufrufs verweist die Variable jedoch auf eine Instanz der Klasse Girokonto. In diesem Fall wird die Auszahlung
verweigert. Dies liegt daran, dass stets die Version der Methode verwendet wird, die
zum dynamischen Typ der Variable gehört. Die Methoden beider Klassen heiÿen zwar
gleich, sind jedoch unterschiedlich implementiert.
Obwohl meinKonto am Ende des Programmes auf ein Sparkonto -Objekt zeigt, kann die
Methode verzinsen() der Klasse Sparkonto nicht auf meinKonto angewendet werden,
da verzinsen() in der Klasse Konto nicht existiert. Um auf die Methode verzinsen()
zugreifen zu können, ist eine explizite Typumwandlung erforderlich.
Java
//Datei: Konto8/KontoTest.java
//Beispiel : Polymorphismus
public class KontoTest{
public static void main(String[] args){
Konto meinKonto ; // Variable vom Typ Konto
meinKonto = new Konto(4531088); // meinKonto zeigt auf ein Objekt vom Typ Konto
meinKonto.einzahlen(300.00);
meinKonto.auszug();
meinKonto.auszahlen(500.00); // Methode auszahlen(betrag) der Klasse Konto wird aufgerufen
meinKonto.auszug();
System.out.println ();
meinKonto = new Girokonto(5613990,100); // meinKonto zeigt jetzt auf ein Objekt vom Typ Girokonto
meinKonto.einzahlen(300.00);
meinKonto.auszug();
meinKonto.auszahlen(500.00); // Methode auszahlen(betrag) der Klasse Girokonto wird aufgerufen
// −−> Polymorphismus & dynamische Bindung
meinKonto.auszug();
System.out.println ();
meinKonto = new Sparkonto(1733065,1.25); // meinKonto zeigt jetzt auf ein Objekt vom Typ Sparkonto
meinKonto.einzahlen(400.00);
meinKonto.auszug();
meinKonto.auszahlen(100.00); // Methode auszahlen(betrag) der Klasse Sparkonto wird aufgerufen
meinKonto.auszug();
/* meinKonto.verzinsen(); // Fehler: die Klasse Konto kennt die Methode verzinsen() nicht */
((Sparkonto)meinKonto).verzinsen();
meinKonto.auszug();
}
}
31
Oberon-2
(* Datei: Konto8/KontoTest.mod *)
(* Beispiel : Polymorphismus*)
MODULE KontoTest;
IMPORT Konto, Sparkonto, Girokonto, Out;
PROCEDURE ProgMain*();
VAR meinKonto : Konto.KontoRef; (* Variable vom Typ Konto. *)
meinSparkonto : Sparkonto.SparkontoRef;
meinGirokonto : Girokonto.GirokontoRef;
BEGIN
NEW(meinKonto); (* meinKonto zeigt auf ein Objekt vom Typ Konto *)
meinKonto.Konto(4531088);
meinKonto.einzahlen(300.00);
meinKonto.auszug();
meinKonto.auszahlen(500.00); (* Methode auszahlen(betrag) der Klasse Konto wird aufgerufen. *)
meinKonto.auszug();
Out.Ln;
NEW(meinGirokonto);
meinGirokonto.Girokonto(5613990,100);
(* −−> Polymorphismus *)
meinKonto := meinGirokonto; (* meinKonto zeigt jetzt auf ein Objekt vom Typ Girokonto.*)
meinKonto.einzahlen(300.00);
meinKonto.auszug();
meinKonto.auszahlen(500.00); (* Methode auszahlen(betrag) der Klasse Girokonto wird aufgerufen.
−−> dynamische Bindung *)
meinKonto.auszug();
Out.Ln;
NEW(meinSparkonto);
meinSparkonto.Sparkonto(1733065,1.25);
meinKonto := meinSparkonto; (* meinKonto zeigt jetzt auf ein Objekt vom Typ Sparkonto *)
meinKonto.einzahlen(400.00);
meinKonto.auszug();
meinKonto.auszahlen(100.00); (* Methode auszahlen(betrag) der Klasse Sparkonto wird aufgerufen *)
meinKonto.auszug();
(*meinKonto.verzinsen(); // Fehler: die Klasse Konto kennt die Methode verzinsen() nicht *)
meinKonto(Sparkonto.SparkontoRef).verzinsen();
meinKonto.auszug();
END ProgMain;
BEGIN
Out.Open
END KontoTest.
2.9 Überladen
Überladen bezeichnet die Tatsache, dass mehrere Methoden mit demselben Namen,
jedoch unterschiedlicher Signatur deklariert werden dürfen. D.h. die Parameterlisten
der Methoden müssen sich in mindestens einem Parameter unterscheiden. Entweder
ist die Anzahl der Parameter verschieden oder sie haben unterschiedliche Typen.
Es ist durchaus üblich, dass eine Klasse mehrere Versionen einer Methode anbietet,
diese sollten allerdings eine vergleichbare Funktionalität haben.
Beim Aufruf einer solche Methode entscheidet der Compiler anhand der übergebenen
32
Parameter, welche Implementierung auszuführen ist.
Für ein Konto sollte es bspw. möglich sein bei der Erönung zu entscheiden, ob gleich
ein gewisser Geldbetrag eingezahlt wird oder nicht. Dies bedeutet, dass zwei Konstruktoren notwendig sind. Gleiches gilt für Sparkonten und Girokonten.
Java
Die Programmiersprache Java erlaubt das Überladen von Methoden und Konstruktoren. Der bisherigen Klassenbeschreibungen von Konto wird einfach ein zweiter Konstruktor hinzugefügt, der mit zwei Parametern deklariert ist. Für die Klassen Sparkonto
und Girokonto wird ein weiterer Konstruktor mit je drei Argumenten eingeführt.
//Datei: Konto9/Konto.java
//Beispiel : Ueberladen
class Konto{
static int zaehler = 0;
protected int kontonummer;
protected double saldo;
Konto(int nummer){ // bisheriger Konstruktor mit 1 Parameter
}
zaehler = zaehler + 1;
kontonummer = nummer;
saldo = 0.00;
Konto(int nummer, double betrag){ // weiterer Konstruktor mit 2 Parametern
zaehler = zaehler + 1;
kontonummer = nummer;
saldo = betrag;
}
}
// die bereits bekannten Methoden wurde hier weggelassen
//Datei: Konto9/Sparkonto.java
//Beispiel : Ueberladen
class Sparkonto extends Konto{
private double zinssatz;
Sparkonto(int nummer, double zins){ // bisheriger Konstruktor mit 2 Parameter
super(nummer); // ruft 1−stelligen Konstruktor der Superklasse auf
}
zinssatz = zins;
Sparkonto(int nummer, double betrag, double zins){ // weiterer Konstruktor mit 3 Parametern
super(nummer, betrag); // ruft 2−stelligen Konstruktor der Superklasse auf
zinssatz = zins;
}
}
// die bereits bekannten Methoden wurde hier weggelassen
33
//Datei: Konto9/Girokonto.java
//Beispiel : Ueberladen
class Girokonto extends Konto{
private double limit;
Girokonto(int nummer, double limit){ // bisheriger Konstruktor mit 2 Parameter
super(nummer); // ruft 1−stelligen Konstruktor der Superklasse auf
if ( limit < 0)
limit = −limit;
this . limit = limit ;
}
Girokonto(int nummer, double betrag, double limit){ // weiterer Konstruktor mit 2 Parametern
super(nummer, betrag); // ruft 2−stelligen Konstruktor der Superklasse auf
if ( limit < 0)
limit = −limit;
this . limit = limit ;
}
}
// die bereits bekannten Methoden wurde hier weggelassen
Instanzen der einzelnen Klassen können jetzt wahlweise mit dem bisher bekannten
Konstruktor erzeugt werden, der den Kontostand zu Beginn auf 0.00 setzt, oder man
verwendet die neuen Konstruktoren, die bereits Geld auf das Konto einzahlen. Dabei
ist besonders bei Objekten der Klassen Sparkonto und Girokonto auf die Reihenfolge
der Argumente zu achten. Zuerst werden Kontonummer und gewünschter Kontostand
angegeben, erst danach der Zinssatz bzw. das Limit.
//Datei: Konto9/KontoTest.java
//Beispiel : Ueberladen
public class KontoTest{
public static void main(String[] args){
Konto meinKonto = new Konto(4531088);
meinKonto.einzahlen(300.00);
meinKonto.auszug();
Konto deinKonto = new Konto(1733065, 200.00);
deinKonto.auszug();
Sparkonto meinSparkonto = new Sparkonto(5613990, 1.25);
meinSparkonto.einzahlen(600.00);
meinSparkonto.auszug();
Sparkonto deinSparkonto = new Sparkonto(7835406, 500.00, 1.3);
deinSparkonto.auszug();
Girokonto meinGirokonto = new Girokonto(2571183, 400.00);
meinGirokonto.einzahlen(250.00);
meinGirokonto.auszug();
}
}
Girokonto deinGirokonto = new Girokonto(6464951, 600.00, 400.00);
deinGirokonto.auszug();
Oberon-2
In Oberon-2 dürfen Namen nicht mehrfach deklariert werden, so dass ein Überladen
von Methoden nicht möglich ist.
34
3 Beispiel: Mediendatenbank
13
In diesem Beispiel
soll eine Anwendung programmiert werden, die verschiedene Ar-
ten von Medien verwalten kann. Eine solche Datenbank speichert Informationen über
Bücher, CDs und Videos und gibt diese auf Anfrage aus.
Zunächst ist zu überlegen, welche Daten gespeichert werden. Für Bücher wollen wir
Autor, Titel, Verlag und das Erscheinungsjahr festhalten. Eine CD wird mit Titel,
Name des Interpreten, Anzahl der Musiktitel und Gesamtspielzeit erfasst. Für Videos
werden Regisseur, Titel und die Länge des Films angegeben. Soll die Anwendung später als Bestandskatalog einer Buchhandlung oder Bibliothek eingesetzt werden, so ist
es sinnvoll, neben den bereits genannten Daten, den Preis und einen Kommentar zu
speichern. Auÿerdem sollte jedes Medium durch eine Artikelnummer eindeutig gekennzeichnet sein.
Es erscheint sinnvoll, die Daten eines Mediums als Einheit zu speichern. Hierzu werden
in der objektorientierten Programmierung Klassen genutzt. Es ist also nahe liegend, für
jede Medienart eine eigene Klasse zu entwickeln, in der alle relevanten Eigenschaften
gesammelt sind.
Damit die Daten nicht unberechtigt geändert werden, sind sie in der Klasse gekapselt.
D.h. die einzelnen Attribute sind nicht nach auÿen hin sichtbar und lassen sich nur über
entsprechende Methoden lesen und ändern. Damit andere Programme in der Lage sind
die Datenfelder auszulesen, wird für jedes Attribut eine get-Methode angelegt. Da sich
die meisten Attribute in unserem Beispiel später nicht mehr ändern, gibt es für sie
keine Methoden, die es ermöglichen die Attributwerte zu manipulieren. Lediglich der
Preis und die Kommentare können durch sogenannte set-Methoden abgeändert werden. Dies bedeutet aber auch, dass alle Attributwerte im Konstruktor gesetzt werden
müssen. Zusätzlich ist eine Methode druckeInfo() deniert, die die Details des jeweiligen Mediums ausgibt.
Obwohl die Klassen Buch, Cd und Video unterschiedliche Arten von Medien beschreiben, sind sie sich sehr ähnlich. Alle drei Klasse besitzen die Attribute code, titel, preis
und kommentar. Auÿerdem verfügen alle über die zugehörigen get- und set-Methoden,
die sogar identisch zu implementieren sind. Diese oensichtliche Code-Duplizierung
erhöht den Programmieraufwand unnötig und erschwert die Programmwartung, da
mögliche Änderungen an mindestens drei Stellen vorgenommen werden müssen. Zudem ist es wünschenswert, dass andere Programme nicht zwischen Büchern, CDs und
Videos unterscheiden brauchen, sondern diese allgemein als Medium betrachten können. Insbesondere dann, wenn das Angebot später um weitere Medientypen erweitert
werden soll.
13 Hierbei handelte es sich um eine veränderte Variante des Projektes DoME von D.J. Barnes und
M. Kölling [BK03, S.263 ].
35
Dieses Problem kann man durch Vererbung lösen. Anstatt die Klassen völlig unabhängig voneinander zu denieren, wird zuerst eine Klasse Medium beschrieben, die die
Gemeinsamkeiten der drei Klassen zusammenfasst. Die Klassen Buch, Cd und Video
werden von dieser Klasse abgeleitet und implementieren nur die typspezischen Eigenschaften von Büchern, CDs bzw. Videos. Somit ergibt sich die in Abb. 7 dargestellte
Klassenhierarchie. Der grundlegende Vorteil dieser Vorgehensweise ist, dass man gemeinsame Eigenschaften nur einmal beschreibt.
Sobald die Klassen Medium, Buch, Cd und Video deniert sind, können Instanzen der
Klassen erzeugt werden. Abgesehen von diesen Klassen wird aber noch eine weitere
Klasse benötigt, die die Objekte verwaltet. Die Klasse Datenbank verfügt über ein Attribut medien, das die einzelnen Medien-Objekte sammelt. Auÿerdem implementiert
sie eine Methode zum Erfassen neuer Objekte, die sowohl mit Objekten vom Typ Medi-
um, als auch mit Instanzen aller Unterklassen von Medium arbeiten kann. Die Methode
auisten() gibt eine Liste aller Medien auf der Konsole aus.
Abbildung 7: Klassendiagramm Mediendatenbank
Java
In der Klasse Medium werden alle Attribute deklariert, die verschiedene Medien gemein haben. Sie sind mit dem Schlüsselwort
protected
gekennzeichnet, um sie an
die Subklassen zu vererben. Zusätzlich zu den bereits genannten Attributen wird eine
Klassenvariable zaehler deniert, die zählt, wie viele Objekte erzeugt wurden.
36
Der zweistellige Konstruktor Medium(titel, preis) initialisiert die einzelnen Attribute.
Er verwendet die Klassenvariable zaehler, um automatisch eine Artikelnummer zu erzeugen. Die Werte von titel und preis werden, durch die Argumente des Konstruktors
festgelegt und der Kommentar wird zunächst mit einer leeren Zeichenkette initialisiert.
Auÿerdem muss im Konstruktor der Wert von zaehler erhöht werden. Auf diese Weise
wird ein Instanzenzähler implementiert, da Medium(titel, preis) immer dann aufgerufen wird, wenn man ein neues Objekt erzeugt.
Die get- und set-Methoden der Attribute sind mit dem Modizierer
public
gekenn-
zeichnet, so dass andere Programme die Möglichkeit haben sie zu verwenden.
Die Methode druckeInfo() gibt alle Datenfelder, die verschiedenartige Medien gemeinsam haben, auf die Konsole aus.
// Datei: Medien/Medium.java
public class Medium {
protected static int zaehler = 0;
protected int code ;
protected String titel;
protected double preis;
protected String kommentar;
public Medium(String titel, double preis){
this .zaehler += 1;
this .code = 100000 + this.zaehler;
this . titel = titel ;
this . preis = preis ;
this .kommentar = "";
}
public int getCode(){
return this.code;
}
public String getTitel(){
return this.titel ;
}
public double getPreis(){
return this.preis;
}
public void setPreis(double preis){
this . preis = preis ;
}
public String getKommentar(){
return this.kommentar;
}
public void setKommentar(String kommentar){
this .kommentar = kommentar;
}
public void druckeInfo(){
}
}
System.out.println("Code: "+this.code);
System.out.println("Titel: "+this. titel );
System.out.println("Preis: "+this.preis+" Euro");
System.out.println("Kommentar: "+this.kommentar);
System.out.println ();
37
Da die Klasse Buch von Medium abgeleitet wird, ist es nicht nötig, die Attribute co-
de, titel, preis und kommentar erneut zu deklarieren. Genau wie die zugehörigen getund set-Methoden erbt Buch diese Attribute von der Superklasse. Es müssen somit
nur die Attribute autor, verlag und erscheinungsjahr, einschlieÿlich der jeweiligen getMethoden, deniert werden.
Weiterhin implementiert Buch einen eigenen Konstruktor Buch(titel, autor, verlag,
jahr, preis). Dieser ruft zunächst den Konstruktor der Superklasse Medium auf und
übergibt ihm die Argumente titel und autor. Durch diesen Aufruf werden die geerbten
Attribute mit Werten belegt und die Klassenvariable zaehler wird erhöht.
Von der Superklasse Medium erbt Buch auÿerdem die Methode druckeInfo(). Diese
kann jedoch nicht alle Daten eines Buches ausgeben, da bspw. der Autor in der Superklasse nicht bekannt ist. Die Methode druckeInfo() muss also in Buch mit einer
anderen Funktionalität implementiert werden. Die neue Implementierung überschreibt
dann die Variante der Klasse Medium.
// Datei: Medien/Buch.java
public class Buch extends Medium{
protected String autor;
protected String verlag;
protected int erscheinungsjahr;
public Buch(String titel , String autor, String verlag , int jahr , double preis){
super(titel , preis );
this .autor = autor;
this .verlag = verlag;
this .erscheinungsjahr = jahr;
}
public String getAutor(){
return this.autor;
}
public String getVerlag(){
return this.verlag;
}
public int getErscheinungsjahr(){
return this.erscheinungsjahr;
}
public void druckeInfo(){
}
}
System.out.println("Code: "+this.code);
System.out.println("Titel: "+this. titel );
System.out.println("Autor: "+this.autor);
System.out.println("Verlag: "+this.verlag+", "+this.erscheinungsjahr);
System.out.println("Preis: "+this.preis+" Euro");
System.out.println("Kommentar: "+this.kommentar);
System.out.println ();
38
Für die Klassen Cd und Video gilt das Gleiche wie für Buch. Sie werden von Medium
abgeleitet und denieren lediglich weitere Attribute und Methoden, um die spezischen
Eigenschaften von CDs bzw. Videos zu implementieren.
Die Konstruktoren Cd(titel, interpret, titelanz, zeit, preis) und Video(titel, reg, zeit,
preis) enthalten als ersten Befehl den Aufruf des Konstruktors der Klasse Medium.
Dies bedeutet insbesondere, dass die Klassenvariable zaehler alle Medienobjekte zählt
- unabhängig davon, ob es sich um Bücher, CDs oder Videos handelt.
Genau wie in der Klasse Buch muss die Methode druckeInfo() in Cd und Video erneut
implementiert werden. Da sich die einzelnen Implementierungen voneinander unterscheiden, kann diese Aufgabe nicht komplett in die Superklasse verschoben werden.
// Datei: Medien/Cd.java
public class Cd extends Medium{
protected String interpret;
protected int titelanzahl;
protected int spieldauer;
public Cd(String titel , String interpret , int
super(titel , preis );
this . interpret = interpret ;
this . titelanzahl = titelanz ;
this .spieldauer = zeit ;
titelanz ,
int
zeit ,
double preis){
}
public String getInterpret(){
return this.interpret;
}
public int getTitelanzahl(){
return this.titelanzahl;
}
public int getSpieldauer(){
return this.spieldauer;
}
public void druckeInfo(){
}
}
System.out.println("Code: "+this.code);
System.out.println("Titel: "+this. titel );
System.out.println("Interpret: "+this.interpret );
System.out.println("Spieldauer: "+this.spieldauer+" min, "+this.titelanzahl+" Titel");
System.out.println("Preis: "+this.preis+" Euro");
System.out.println("Kommentar: "+this.kommentar);
System.out.println ();
// Datei: Medien/Video.java
public class Video extends Medium{
protected String regisseur;
protected int spieldauer;
public Video(String titel , String reg , int
super(titel , preis );
this . regisseur = reg;
this .spieldauer = zeit ;
zeit ,
double preis){
}
39
public String getRegisseur(){
return this.regisseur;
}
public int getSpieldauer(){
return this.spieldauer;
}
public void druckeInfo(){
}
}
System.out.println("Code: "+this.code);
System.out.println("Titel: "+this. titel );
System.out.println("Regisseur: "+this.regisseur);
System.out.println("Spieldauer: "+this.spieldauer+" min");
System.out.println("Preis: "+this.preis+" Euro");
System.out.println("Kommentar: "+this.kommentar);
System.out.println ();
Die Klasse Datenbank verwendet die Klasse Vector aus dem Paket java.util. Dieses
Paket muss zunächst durch eine
import -Anweisung
importiert werden. Vector im-
plementiert ein dynamisches Array, auf dessen Komponenten über einen ganzzahligen
Index zugegrien werden kann. Bei der Deklaration der Variable medien wird in spitzen Klammern angegeben, von welchem Typ die Komponenten des Vektors sind.
14
Der Konstruktor Datenbank() erzeugt ein Vector -Objekt, indem der Konstruktor Vec-
tor() der Klasse Vector aufgerufen wird. Dabei muss erneut angegeben werden, dass
die Komponenten vom Typ Medium sein sollen. Der erzeugte Vektor ist zunächst leer.
Die Methode erfasseMedium(med) fügt neue Objekte in den Vektor medien ein. Dazu
wird die Methode add(object) aus der Klasse Vector auf medien angewendet. add(med)
hängt das Objekt med ans Ende des Vektors an. Auf diese Art und Weise werden
Instanzen der Klasse Medium gesammelt.
Die Methode auisten() durchläuft die Sammlung und ruft für jede Komponente die
Methode druckeInfo() auf. Zuvor muss das Element jedoch mittels get(i) geholt werden. Die Methoden get(i) und size() sind in der Klasse Vector implementiert.
// Datei: Medien/Datenbank.java
import java.util.*;
public class Datenbank{
private Vector<Medium> medien;
public Datenbank(){
this .medien = new Vector<Medium>();
}
14 Die Klasse
Vector
steht in JDK 1.5 als generische Klasse zur Verfügung, d.h. sie kann mit ver-
schiedenen Arten von Objekten arbeiten. Dies vereinfacht die Programmierung, da man den statischen
Typ der Rückgabewerte der
der Klasse
Object
Vector -Methoden festlegen kann. Bisher wurden Objekte als Instanzen
übergeben und mussten explizit umgewandelt werden. Siehe auch [Mös98, S.91],
[Cla98, S.26] oder vergleiche [API1.4] und [API1.5].
40
public void erfasseMedium(Medium med){
this .medien.add(med);
}
public void auisten(){
Medium med;
for ( int i=0; i < medien.size ();
med =this.medien.get(i);
}
}
}
i++){
med.druckeInfo();
In der Klasse Datenbank werden die Eigenschaften und das Verhalten einer Datenbank
beschrieben. Die Datenbank selber existiert noch nicht. Deshalb wird im Testprogramm
zunächst eine Instanz der Klasse Datenbank erzeugt. Es ist möglich verschiedene Datenbanken anzulegen, dazu müssen lediglich weitere Instanzen der Klasse gebildet werden.
In die Datenbank werden durch die Methode erfasseMedium(med) nacheinander verschiedene Medien eingefügt. Dabei fällt auf, dass die Methode auch Objekte vom Typ
Buch, Cd oder Video verarbeiten kann. Dies ist möglich, da es sich um Subklassen der
Klasse Medium handelt. In der Deniton von erfasseMedium(med) ist med als Variable vom Typ Medium deklariert. Durch diese Festlegung wird lediglich der statische
Typ bestimmt. Zur Ausführungszeit kann med auch auf Objekte eines anderen, von
Medium abgeleiteten Typs verweisen. Die Polymorphie von Variablen reduziert den
15
Programmieraufwand erheblich, da man sonst mehrere Versionen
der Methode er-
fasseMedium(med) implementieren müsste. Zudem könnten Bücher, CDs und Videos
nicht innerhalb eines Vektors verwaltet werden.
Schlieÿlich wird der Inhalt der Datenbank ausgegeben. Die Methode auisten() ruft
für jedes Medium in der Datenbank die Methode druckInfo() auf. Welche Implementierung dieser Methode ausgeführt wird, legt der Übersetzer - in Abhängigkeit vom
dynamischen Typ der Variable - erst zur Laufzeit fest.
// Datei: Medien/TestMedienDB.java
public class TestMedienDB{
public static void main(String[] args){
Datenbank db = new Datenbank();
db.erfasseMedium(new Medium("Herr der Ringe", 11.00));
db.erfasseMedium(new Buch("Sprechen Sie Java","Hanspeter Moessenboeck", "dpunkt.verlag", 2001, 28.00));
db.erfasseMedium(new Cd("No Angel","Dido",13,90,15.00));
db.erfasseMedium(new Video("X−Men","Bryan Singer",100,21.00));
}
}
db. auisten ();
15
erfasseBuch(buch), erfasseCd(cd) und erfasseVideo(video).
41
Oberon-2
Die Anwendung Mediendatenbank lässt sich mit einigen Anpassungen auch in Oberon2 umsetzen.
Für jede Klasse wird ein eigenes Modul geschrieben, in dem die Datenfelder in einem
RECORD deklariert werden. Die instanzspezischen Methoden werden als typgebundene Prozeduren implementiert.
Da Oberon-2 nicht über einen eigenen Datentyp für Zeichenketten verfügt, werden
die Attribute titel, kommentar, autor usw. als
ARRAY 100 OF CHAR deklariert.
Dies bedeutet jedoch, dass die Einträge maximal 100 Zeichen lang sein dürfen. Für die
Wertzuweisung wird die vordenierte Prozedur
COPY(s, t) genutzt. Diese Prozedur
kopiert den Wert des Zeichenfeldes s in das Feld t.
Im Modul Medium wird als erste Prozedur eine Klassenmethode NEWMedium(titel,
preis) implementiert. Diese Methode ersetzt den in anderen objektorientierten Programmiersprachen üblichen Konstruktor. Anders als im Beispiel Konten ist der
NEW -
Operator diesmal in der Konstruktormethode enthalten, d.h. die Methode erzeugt und
initialisiert die Objekte. Anschlieÿend werden die Objekte als Rückgabewert an die
aufrufende Stelle zurückgeben. Das bedeutet, dass eine Variable des passenden Referenztyps das Objekt entgegennehmen muss. Die Klassen Buch, Cd und Video besitzen
mit NEWBuch(titel, autor, verlag, jahr, preis), NEWCd(titel, interpret, titelanz, zeit,
preis) bzw. NEWVideo(Titel, reg, zeit, preis) ebenfalls eine Klassenmethode, die den
Konstruktor ersetzt.
Aus Sicherheitsgründen sollten Attribute immer über Methoden angesprochen werden. Deshalb wurden get- und set-Methoden implementiert, obwohl die Datenfelder
öentlich sind. Anders als in Java sind die Methoden getTitel(titel), getKommen-
tar(kommentar) u.a. nicht als Funktionen implementiert. Da Arrays nicht Rückgabewert einer Funktion sein können, wird das Ergebnis an einen Referenzparameter
übergeben.
(* Datei: Medien/Medium.mod *)
MODULE Medium;
IMPORT Out;
TYPE
Medium* = RECORD
code * : LONGINT;
titel * : ARRAY 100 OF CHAR;
preis * : REAL;
kommentar* : ARRAY 100 OF CHAR;
END;
MediumRef* = POINTER TO Medium;
VAR zaehler* : INTEGER;
42
PROCEDURE NEWMedium*(titel : ARRAY 100 OF CHAR; preis : REAL) : MediumRef;
VAR tmp : MediumRef;
BEGIN
NEW(tmp);
INC(zaehler);
tmp.code := 100000 + zaehler;
COPY(titel, tmp.titel);
tmp.preis := preis ;
tmp.kommentar := "";
RETURN tmp;
END NEWMedium;
PROCEDURE (m : MediumRef) getCode*() : LONGINT;
BEGIN
RETURN m.code;
END getCode;
PROCEDURE (m : MediumRef) getTitel*(VAR titel : ARRAY 100 OF CHAR) ;
BEGIN
COPY(m.titel, titel);
END getTitel;
PROCEDURE (m : MediumRef) getPreis*() : REAL;
BEGIN
RETURN m.preis;
END getPreis;
PROCEDURE (m : MediumRef) setPreis*(preis : REAL);
BEGIN
m.preis := preis ;
END setPreis;
PROCEDURE (m : MediumRef) getKommentar*(VAR kommentar : ARRAY 100 OF CHAR);
BEGIN
COPY(m.kommentar, kommentar);
END getKommentar;
PROCEDURE (m : MediumRef) setKommentar*(kommentar : ARRAY 100 OF CHAR);
BEGIN
COPY(kommentar, m.kommentar);
END setKommentar;
PROCEDURE (m : MediumRef) druckeInfo*();
BEGIN
Out.String("Code: "); Out.Int(m.code, 7); Out.Ln;
Out.String("Titel: " ); Out.String(m.titel ); Out.Ln;
Out.String("Preis: " ); Out.Real(m.preis , 5); Out.String(" Euro"); Out.Ln;
Out.String("Kommentar: "); Out.String(m.kommentar); Out.Ln;
Out.Ln;
END druckeInfo;
BEGIN
zaehler := 0;
END Medium.
(* Datei: Medien/Buch.mod *)
MODULE Buch;
IMPORT Out, Medium;
TYPE
Buch* = RECORD (Medium.Medium)
autor : ARRAY 100 OF CHAR;
verlag : ARRAY 100 OF CHAR;
erscheinungsjahr : INTEGER;
END;
BuchRef* = POINTER TO Buch;
43
PROCEDURE NEWBuch*(titel, autor, verlag : ARRAY 100 OF CHAR; jahr : INTEGER; preis : REAL) : BuchRef;
VAR tmp : BuchRef;
BEGIN
NEW(tmp);
INC(Medium.zaehler);
tmp.code := 100000 + Medium.zaehler;
COPY(titel, tmp.titel);
COPY(autor, tmp.autor);
COPY(verlag, tmp.verlag);
tmp.erscheinungsjahr := jahr;
tmp.preis := preis ;
tmp.kommentar := "";
RETURN tmp;
END NEWBuch;
PROCEDURE (b : BuchRef) getAutor*(VAR autor : ARRAY 100 OF CHAR);
BEGIN
COPY(b.autor, autor);
END getAutor;
PROCEDURE (b : BuchRef) getVerlag*(VAR verlag : ARRAY 100 OF CHAR);
BEGIN
COPY(b.verlag, verlag);
END getVerlag;
PROCEDURE (b : BuchRef) getErscheinungsjahr*() : INTEGER;
BEGIN
RETURN b.erscheinungsjahr;
END getErscheinungsjahr;
PROCEDURE (b : BuchRef) druckeInfo*();
BEGIN
Out.String("Code: "); Out.Int(b.code , 7); Out.Ln;
Out.String("Titel: " ); Out.String(b. titel ); Out.Ln;
Out.String("Autor: "); Out.String(b.autor); Out.Ln;
Out.String("Verlag: "); Out.String(b.verlag ); Out.String(", " ); Out.Int(b.erscheinungsjahr ,4); Out.Ln;
Out.String("Preis: " ); Out.Real(b.preis , 5); Out.String(" Euro"); Out.Ln;
Out.String("Kommentar: "); Out.String(b.kommentar); Out.Ln;
Out.Ln;
END druckeInfo;
BEGIN
END Buch.
(* Datei: Medien/Cd.mod *)
MODULE Cd;
IMPORT Out, Medium;
TYPE
Cd* = RECORD (Medium.Medium)
interpret : ARRAY 100 OF CHAR;
titelanzahl , spieldauer : INTEGER;
END;
CdRef* = POINTER TO Cd;
PROCEDURE NEWCd*(titel, interpret : ARRAY 100 OF CHAR; titelanz, zeit : INTEGER; preis : REAL) : CdRef;
VAR tmp : CdRef;
BEGIN
NEW(tmp);
INC(Medium.zaehler);
tmp.code := 100000 + Medium.zaehler;
COPY(titel, tmp.titel);
COPY(interpret, tmp.interpret);
tmp.titelanzahl := titelanz ;
tmp.spieldauer := zeit ;
tmp.preis := preis ;
tmp.kommentar := "";
RETURN tmp;
END NEWCd;
44
PROCEDURE (c : CdRef) getInterpret*(VAR interpret : ARRAY 100 OF CHAR);
BEGIN
COPY(c.interpret, interpret);
END getInterpret;
PROCEDURE (c : CdRef) getTitelanzahl*() : INTEGER;
BEGIN
RETURN c.titelanzahl;
END getTitelanzahl;
PROCEDURE (c : CdRef) getSpieldauer*() : INTEGER;
BEGIN
RETURN c.spieldauer;
END getSpieldauer;
PROCEDURE (c : CdRef) druckeInfo*();
BEGIN
Out.String("Code: "); Out.Int(c.code , 7); Out.Ln;
Out.String("Titel: " ); Out.String(c. titel ); Out.Ln;
Out.String("Interpret: " ); Out.String(c. interpret ); Out.Ln;
Out.String("Spieldauer: "); Out.Int(c.spieldauer , 3); Out.String(" min, ");
Out.Int(c. titelanzahl ,3); Out.String(" Titel"); Out.Ln;
Out.String("Preis: " ); Out.Real(c.preis , 5); Out.String(" Euro"); Out.Ln;
Out.String("Kommentar: "); Out.String(c.kommentar); Out.Ln;
Out.Ln;
END druckeInfo;
BEGIN
END Cd.
(* Datei: Medien/Video.mod *)
MODULE Video;
IMPORT Out, Medium;
TYPE
Video* = RECORD (Medium.Medium)
regisseur : ARRAY 100 OF CHAR;
spieldauer : INTEGER;
END;
VideoRef* = POINTER TO Video;
PROCEDURE NEWVideo*(titel, regisseur : ARRAY 100 OF CHAR; zeit : INTEGER; preis : REAL) : VideoRef;
VAR tmp : VideoRef;
BEGIN
NEW(tmp);
INC(Medium.zaehler);
tmp.code := 100000 + Medium.zaehler;
COPY(titel, tmp.titel);
COPY(regisseur, tmp.regisseur);
tmp.spieldauer := zeit ;
tmp.preis := preis ;
tmp.kommentar := "";
RETURN tmp;
END NEWVideo;
PROCEDURE (v : VideoRef) getRegisseur*(VAR reg : ARRAY 100 OF CHAR);
BEGIN
COPY(v.regisseur, reg);
END getRegisseur;
PROCEDURE (v : VideoRef) getSpieldauer*() : INTEGER;
BEGIN
RETURN v.spieldauer;
END getSpieldauer;
45
PROCEDURE (v : VideoRef) druckeInfo*();
BEGIN
Out.String("Code: "); Out.Int(v.code , 7); Out.Ln;
Out.String("Titel: " ); Out.String(v. titel ); Out.Ln;
Out.String("Regisseur: "); Out.String(v. regisseur ); Out.Ln;
Out.String("Spieldauer: "); Out.Int(v.spieldauer ,3); Out.String(" min"); Out.Ln;
Out.String("Preis: " ); Out.Real(v.preis , 5); Out.String(" Euro"); Out.Ln;
Out.String("Kommentar: "); Out.String(v.kommentar); Out.Ln;
Out.Ln;
END druckeInfo;
BEGIN
END Video.
Oberon-2 verfügt zwar über sogenannte oene Arrays, diese dürfen aber nicht innerhalb
einer
RECORD -Deklaration verwendet werden. Deshalb musste im Modul Datenbank
auf ein statisches Array des Gröÿe 100 zurückgegrien werden, dass auf Objekte vom
Typ Medium verweist. Somit kann jede Datenbank maximal 100 Einträge speichern.
Damit die Methode auisten() nur den Teil des Arrays durchläuft, der tatsächlich
verwendet wird, speichert das Attribut length die Länge des belegten Abschnitts. Nur
das Modul Datenbank benutzt dieses Attribut, weshalb es nicht exportiert wird.
Die Methode NEWDatenbank() erzeugt zunächst eine Instanz der Klasse Datenbank
und speichert einen Adressverweis auf dieses Objekt in einer temporären Variable. Die
Felder des Arrays werden auf den Wert NIL gesetzt, d.h. sie verweisen auf kein Objekt.
Der Wert des Attributs length wird mit 0 initialisiert. Anschlieÿend wird das erzeugte
Objekt an die Stelle des Funktionsaufrufes zurückgegeben.
Durch die Methode erfasseMedium(med) wird das Objekt, dass med referenziert dem
nächsten freien Feld von medien zugewiesen und length um 1 erhöht. Ist kein freies
Feld verfügbar, gibt die Methode eine Fehlermeldung aus.
(* Datei: Medien/Datenbank.mod *)
MODULE Datenbank;
IMPORT Out, Medium;
TYPE
Datenbank* = RECORD
medien* : ARRAY 100 OF Medium.MediumRef;
length : INTEGER;
END;
DatenbankRef* = POINTER TO Datenbank;
PROCEDURE NEWDatenbank*() : DatenbankRef;
VAR tmp : DatenbankRef;
i : INTEGER;
BEGIN
NEW(tmp);
FOR i:= 0 TO 99 DO
tmp.medien[i] := NIL;
END;
tmp.length := 0;
RETURN tmp;
END NEWDatenbank;
46
PROCEDURE (db : DatenbankRef) erfasseMedium*(med : Medium.MediumRef);
BEGIN
IF db.length < 100 THEN
db.medien[db.length] := med;
INC(db.length);
ELSE
Out.String("Datenbank ist voll, Medium öknnte nicht eingefuegt werden.");
END;
END erfasseMedium;
PROCEDURE (db : DatenbankRef) auisten*();
VAR i : INTEGER;
BEGIN
FOR i:= 0 TO db.length−1 DO
db.medien[i ]. druckeInfo();
END;
END auisten;
BEGIN
END Datenbank.
Das Testprogramm importiert alle Module, die es verwendet, da sonst die darin denierten Namen nicht bekannt sind. Um die Komponenten eines Moduls zu nutzen,
muss der Name dieses Moduls immer mit angegeben sein. Dies bedeutet insbesondere, dass die Klassenmethoden NEWDatenbank(), NEWMedium(titel, preis) usw. nur
über den Modulnamen angesprochen werden können. Typgebundene Prozeduren, wie
z.B. erfasseMedium(med), sind an ein Objekt der Klasse gebunden. Deshalb kann der
Aufruf über den zugehörigen Variablennamen erfolgen.
(* Datei: Medien/TestMedienDB.mod *)
MODULE TestMedienDB;
IMPORT Out, Datenbank, Medium, Buch, Cd, Video;
PROCEDURE ProgMain*();
VAR db : Datenbank.DatenbankRef;
BEGIN
db := Datenbank.NEWDatenbank();
db.erfasseMedium(Medium.NEWMedium("Herr der Ringe", 11.00));
db.erfasseMedium(Buch.NEWBuch("OPP mit Oberon−2","Hanspeter Moessenboeck","dpunkt.verlag", 2001, 28.00));
db.erfasseMedium(Cd.NEWCd("No Angel", "Dido", 13, 90, 15.00));
db.erfasseMedium(Video.NEWVideo("X−Men", "Bryan Singer", 100, 21.00));
db. auisten ();
END ProgMain;
BEGIN
Out.Open;
END TestMedienDB.
47
4 Beispiel: Geometrische Figuren
Das letzte Beispiel stellt verschiedene Methoden zur Manipulation von geometrischen
Figuren zur Verfügung. Für jede Art von Figur kann man die Fläche berechnen, die
Figur verschieben oder strecken und sie zeichnen. Die Aufgaben sind für alle Figuren
gleich, jedoch ist die korrekte Implementierung einiger Methoden von der jeweiligen
Figur abhängig. Jede Figur benötigt somit eine eigene Klasse, in der charakteristische
Eigenschaften und das zugehörige Verhalten beschrieben sind. Damit Programme, die
Objekte dieser Klassen benutzen, nicht zwischen den unterschiedlichen Varianten unterscheiden müssen, wird eine allgemeine Klasse Figur eingeführt. Die konkreten Figuren Rechteck, Quadrat und Kreis werden aus dieser Klasse abgeleitet.
Bleibt die Frage, welche Komponenten in der Superklasse zu denieren sind. Ein ebenes Rechteck wird durch einen Punkt sowie Höhe und Breite eindeutig bestimmt. Für
Quadrat und Kreis genügen ein Punkt und die Höhe bzw. der Radius als Angaben.
Da jeder Figurtyp einen Referenzpunkt zur Bestimmung der Lage benötigt, scheint es
sinnvoll die x- und y-Koordinate dieses Punktes als Attribute in Figur einzuführen. Die
notwendigen Maÿangaben sind bei den einzelnen Figuren unterschiedlich und gehören
deshalb in die zugehörigen Subklassen.
Damit die Methoden getFlaeche(), verschieben(dx, dy) , strecken(faktor) und zeich-
nen() für eine Variable vom Typ Figur aufgerufen werden können, müssen sie in der
Klasse Figur deklariert sein. Dies liegt daran, dass zur Übersetzungszeit der statische
Typ der Variable
16
festlegt, welche Methoden ausführbar sind. Die Methode verschie-
ben(dx, dy) kann leicht implementiert werden. Alle anderen bereiten jedoch Probleme,
da sie auf Längenangaben angewiesen sind, die in Figur nicht zur Verfügung stehen.
Zur Lösung dieses Problems gibt es in objektorientierten Sprachen die Möglichkeit,
abstrakte Methoden ohne Anweisungsblock zu denieren. Eine Klasse, die abstrakte
Methoden enthält, ist damit automatisch eine abstrakte Klasse. Von solchen Klassen
sollten keine Instanzen erzeugt werden. Sie dienen ausschlieÿlich als Superklasse für
andere Klassen.
Die Klasse Figur wird somit als abstrakte Klasse implementiert. Die abstrakten Methoden getFlaeche(), strecken(faktor) und zeichnen() werden in den Klassen Rechteck,
Quadrat und Kreis überschrieben und erhalten so ihre Funktionalität. Insgesamt ergibt
sich die in Abb. 8 dargestellte Klassenhierarchie.
16 Siehe auch Abschnitt 2.8 über Polymorphismus.
48
Abbildung 8: Klassendiagramm geometische Figuren
Java
Abstrakte Klassen und Methoden sind in Java mit dem Schlüsselwort
abstract gekenn-
zeichnet. Sobald eine Klasse eine abstrakte Methode enthält, muss auch die Klasse als
abstrakt deniert werden. Der Java-Compiler verhindert dann, dass von dieser Klasse
eine Instanz erzeugt wird. Der Versuch, den
new -Operator auf eine abstrakte Klasse
anzuwenden, führt zu einem Fehler.
Neben den bereits genannten Methoden zur Manipulation wurden weitere Methoden
ergänzt, die Informationen über das Objekt liefern. Anhand dieser Methoden wird das
Prinzip des Information Hiding noch einmal deutlich. Die Attribute können nicht direkt angesprochen werden. Um die Informationen trotzdem verfügbar zu machen, sind
spezielle get-Methoden implementiert. Diese Methoden geben die Attributwerte jedoch
nicht als Integer oder Double zurück sondern als String. Änderung des Datentyps der
Attribute wirken sich deshalb nicht auf den Anwender aus.
//Datei: Figuren/Figur.java
import java.awt.*;
public abstract class Figur{
protected int xpos, ypos;
protected Figur(int x, int y){
this .xpos = x;
this .ypos = y;
}
49
public abstract String getTyp();
public String getPosition(){
return "("+this.xpos+";"+this.ypos+") ";
}
public abstract String getLaengen();
public abstract double getFlaeche();
public void verscheiben(int dx, int dy){
this .xpos += dx;
this .ypos += dy;
}
}
public abstract void strecken(double faktor);
public abstract void zeichnen(Graphics g);
Die Klassen Rechteck, Quadrat und Kreis deklarieren die spezischen Attribute der
Figurtypen und implementieren die abstrakten Methoden der Klasse Figur.
Die Methoden getPosition() und verschieben(dx, dy) können unverändert von der Superklasse geerbt werden.
Zum Zeichnen der Figuren wurden Methoden der Klasse Graphics aus den Paket ja-
17
va.awt genutzt.
//Datei: Figuren/Rechteck.java
import java.awt.*;
public class Rechteck extends Figur{
protected double breite, hoehe;
public Rechteck(int x, int y, double breite, double hoehe){
super(x,y);
this . breite = breite ;
this .hoehe = hoehe;
}
public String getTyp(){
return "Rechteck";
}
public String getLaengen(){
return "Breite = "+this.breite+" Hoehe = "+this.hoehe;
}
public double getFlaeche(){
return this.breite*this.hoehe;
}
public void strecken(double faktor){
this . breite *= faktor;
this .hoehe *= faktor;
}
public void zeichnen(Graphics g){
g.drawRect(this.xpos, this.ypos, (int)this . breite , ( int)this .hoehe);
}
}
17 Siehe [Abt02, S. 205 ] oder [API1.5].
50
//Datei: Figuren/Quadrat.java
import java.awt.*;
public class Quadrat extends Figur{
protected double hoehe;
public Quadrat(int x, int y, double hoehe){
super(x,y);
this .hoehe = hoehe;
}
public String getTyp(){
return "Quadrat";
}
public String getLaengen(){
return "Hoehe = "+this.hoehe;
}
public double getFlaeche(){
return this.hoehe*this.hoehe;
}
public void strecken(double faktor){
this .hoehe *= faktor;
}
public void zeichnen(Graphics g){
g.drawRect(this.xpos, this.ypos, (int)this .hoehe, ( int)this .hoehe);
}
}
//Datei: Figuren/Kreis.java
import java.awt.*;
public class Kreis extends Figur{
protected double radius;
public Kreis(int x, int y, double radius){
super(x,y);
this .radius = radius;
}
public String getTyp(){
return "Kreis";
}
public String getLaengen(){
return "Radius = "+this.radius;
}
public double getFlaeche(){
return 3.14*this.radius*this.radius;
}
public void strecken(double faktor){
this .radius *= faktor;
}
public void zeichnen(Graphics g){
g.drawOval(this.xpos−(int)this.radius, this.ypos−(int)this.radius, (int)(2*this.radius ), ( int)(2*this .radius ));
}
}
51
Im ersten Testprogramm werden Instanzen der Klassen Quadrat und Kreis erzeugt
und in Variablen des jeweiligen Typs gespeichert. Auf diese Objekte können die deklarierten Methoden wie gewohnt angewendet werden.
Die Methode zeichnen(g) benötigt ein Fenster in dem die Befehle der Klasse Graphics
18
zeichnen. Das Paket java.awt stellt diese und andere grasche Komponenten
zur Ver-
fügung. Das Programm TestFigur1 verwendet die Klasse Frame.
Zunächst wird die Methode paint überschrieben. Sie speziziert, was im Fenster darzustellen ist. Der Konstruktor der Klasse TestFigur1 legt die Gestalt des Fensters fest
und aktiviert den Button zum Schlieÿen. Im Hauptprogramm muss das Fenster dann
noch erzeugt werden.
//Datei: Figuren/TestFigur1.java
import java.awt.*;
import java.awt.event.*;
public class TestFigur1 extends Frame{
// die Methode paint wird automatisch ausgefuehrt, wenn eine grasche Komponente gezeichnet wird
public void paint (Graphics g){
Quadrat testQ = new Quadrat(100,100,10);
Kreis testK = new Kreis(500,200,80);
for ( int i = 0; i < 18; i++) {
}
}
testQ.zeichnen(g);
testQ.strecken (1.2);
testK.zeichnen(g);
testK.verscheiben (10,0);
// folgende Programmteile werden zum Zeichnen benoetigt
// Konstruktor initialisiert das Fenster , in dem gezeichnet wird
public TestFigur1(){
super("Figuren"); // Titel des Fensters
setVisible (true); // Fenster anzeigen
setSize (1000,600); // Groesse des Fensters
addWindowListener(new WindowAdapter() {
public void windowClosing ( WindowEvent e) {
System.exit(0);
}
});
}
public static void main(String args[]){
TestFigur1 frame = new TestFigur1(); // Fenster wird erzeugt
}
}
Die Testprogramme TestFigur2 und TestFigur3 nutzen aus, dass Variablen vom Typ
der Superklasse auf Objekte der Subklassen verweisen können. Beide Programme benötigen deshalb nur eine einzige Variable, die verschiedene Figur-Objekte in einem
Array speichert. Für die einzelnen Komponenten des Arrays können die in Figur denierten Methoden unabhängig von der Art des Objektes aufgerufen werden. Welche
Implementierung tatsächlich ausgeführt wird, entscheidet sich erst zur Laufzeit.
18 Siehe [Abt02, Grasche Benutzeroberächen].
52
//Datei: Figuren/TestFigur2.java
public class TestFigur2{
public static void main(String args[]){
Figur [] test = new Figur[3];
test [0] = new Rechteck(100,100,40,60);
test [1] = new Quadrat(200,200,40);
test [2] = new Kreis(300,300,50);
for ( int i=0; i<test.length ; i++){
System.out.print(test [ i ]. getTyp()+" −−> ");
System.out.print(test [ i ]. getPosition ());
System.out.print(test [ i ]. getLaengen());
System.out.println(" Flaeche: "+test[i ]. getFlaeche());
test [ i ]. strecken (1.5);
test [ i ]. verscheiben(10,−5);
}
}
}
System.out.print(test [ i ]. getTyp()+" −−> ");
System.out.print(test [ i ]. getPosition ());
System.out.print(test [ i ]. getLaengen());
System.out.println(" Flaeche: "+test[i ]. getFlaeche());
System.out.println ();
//Datei: Figuren/TestFigur3.java
import java.awt.*;
import java.awt.event.*;
public class TestFigur3 extends Frame{
public void paint (Graphics g){
Figur [] test = new Figur[6];
test [0] = new Rechteck(200,100,110,200);
test [1] = new Quadrat(225,125,60);
test [2] = new Rechteck(310,200,200,100);
test [3] = new Rechteck(450,150,30,50);
test [4] = new Kreis(280,300,50);
test [5] = new Kreis(430,300,50);
for ( int i=0; i<test.length ; i++){
}
}
test [ i ]. zeichnen(g);
public TestFigur3(){
super("Figuren"); // Titel des Fensters
setVisible (true); // Sichtbarkeit des Fensters
}
setSize (800,500); // Groesse des Fensters
// aktiviert Button zum Schliessen des Fenstern
addWindowListener(new WindowAdapter() {
public void windowClosing ( WindowEvent e) {
System.exit(0);
}
});
public static void main(String args[]){
TestFigur3 frame = new TestFigur3();
}
}
53
Oberon-2
In Oberon-2 existiert kein Schlüsselwort, um abstrakte Methoden zu kennzeichnen.
Sie werden stattdessen mit einem leeren Methodenrumpf deklariert. Wenn eine solche
Methode zur Ausführung gelangt, geschieht einfach nichts. Deshalb ist es besonders
wichtig, dass alle abstrakten Methoden in Subklassen überschrieben werden. Dies erfordert besondere Aufmerksamkeit vom Programmierer, da der Compiler abstrakte
Methoden, die nicht überschrieben wurden, nicht als Fehler meldet.
Für Figur ist kein Konstruktor deniert um zu verhindern, dass ein Anwender eine
Instanz der Klasse anlegt. Mittels
NEW
ist es zwar weiterhin möglich, es sollte je-
doch vermieden werden, da die abstrakte Klasse keine vollständige Funktionalität zur
Verfügung stellt.
(* Datei: Figuren/Figur.mod *)
MODULE Figur;
IMPORT Out;
TYPE
Figur* = RECORD
xpos*, ypos * : INTEGER;
END;
FigurRef* = POINTER TO Figur;
PROCEDURE (f : FigurRef) printTyp*();
END printTyp;
PROCEDURE (f : FigurRef) printPosition*();
BEGIN
Out.String(" −−> (");
Out.Int(f .xpos,4);
Out.String(";");
Out.Int(f .ypos,4);
Out.String(") ");
END printPosition;
PROCEDURE (f: FigurRef) printLaengen*();
END printLaengen;
PROCEDURE (f : FigurRef) printFlaeche*();
END printFlaeche;
PROCEDURE (f : FigurRef) verschieben*(dx, dy : INTEGER);
BEGIN
INC(f.xpos, dx);
INC(f.ypos, dy);
END verschieben;
PROCEDURE (f : FigurRef) strecken*(faktor : REAL);
END strecken;
PROCEDURE (f : FigurRef) zeichnen*();
END zeichnen;
BEGIN
END Figur.
54
Die get-Methoden der Klassen Figur, Rechteck, Quadrat und Kreis sind in Oberon-2
als print-Methoden implementiert, d.h. die Daten werden von der Methode direkt auf
die Konsole ausgegeben. Für unsere Zwecke genügte diese Variante und man umgeht
die umständliche Verwendung von Zeichenfeldern.
Zum Zeichnen der Figuren verwendet zeichnen() Befehle einer Turtle-Grak, die im
19
Modul MKTurtleLight
deniert sind.
(* Datei: Figuren/Rechteck.mod *)
MODULE Rechteck;
IMPORT Out, Figur, MKTurtleLight;
TYPE
Rechteck* = RECORD (Figur.Figur)
breite * , hoehe * : REAL;
END;
RechteckRef* = POINTER TO Rechteck;
PROCEDURE NEWRechteck*(x, y : INTEGER; breite, hoehe : REAL) : RechteckRef ;
VAR tmp : RechteckRef;
BEGIN
NEW(tmp);
tmp.xpos := x;
tmp.ypos := y;
tmp.breite := breite ;
tmp.hoehe := hoehe;
RETURN tmp;
END NEWRechteck;
PROCEDURE (r : RechteckRef) printTyp*();
BEGIN
Out.String("Rechteck");
END printTyp;
PROCEDURE (r : RechteckRef) printLaengen*();
BEGIN
Out.String(" Breite = ");
Out.Real(r.breite ,5);
Out.String(" Hoehe = ");
Out.Real(r.hoehe,5);
END printLaengen;
PROCEDURE (r : RechteckRef) printFlaeche*();
BEGIN
Out.String(" Flaeche = ");
Out.Real(r.breite *r.hoehe,10);
END printFlaeche;
PROCEDURE (r : RechteckRef) strecken*(faktor : REAL);
BEGIN
r . breite := r . breite *faktor;
r .hoehe := r.hoehe*faktor;
END strecken;
19 Das Modul MKTurtleLight stammt von M. Kühn und ist unter www.kuehnsoft.de/mkturtle kostenfrei erhältlich.
55
PROCEDURE (r : RechteckRef) zeichnen*();
VAR i : INTEGER;
BEGIN
(* Gehe zum Punkt (r.xpos; r.ypos) *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Gehe(r.xpos);
MKTurtleLight.Drehe(90);
MKTurtleLight.Gehe(r.ypos);
MKTurtleLight.Drehe(−90);
MKTurtleLight.StiftRunter;
(* Zeichne Rechteck *)
MKTurtleLight.Gehe(r.breite);
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(r.hoehe);
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(r.breite);
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(r.hoehe);
MKTurtleLight.Drehe(−90);
(* Gehe zum Koordinatenursprung *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(r.ypos);
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(r.xpos);
MKTurtleLight.Drehe(180);
MKTurtleLight.StiftRunter;
END zeichnen;
BEGIN
END Rechteck.
(* Datei: Figuren/Quadrat.mod *)
MODULE Quadrat;
IMPORT Out, Figur, MKTurtleLight;
TYPE
Quadrat* = RECORD (Figur.Figur)
hoehe * : REAL;
END;
QuadratRef* = POINTER TO Quadrat;
PROCEDURE NEWQuadrat*(x, y : INTEGER; hoehe : REAL) : QuadratRef;
VAR tmp : QuadratRef;
BEGIN
NEW(tmp);
tmp.xpos := x;
tmp.ypos := y;
tmp.hoehe := hoehe;
RETURN tmp;
END NEWQuadrat;
PROCEDURE (q : QuadratRef) printTyp*();
BEGIN
Out.String("Quadrat");
END printTyp;
PROCEDURE (q : QuadratRef) printLaengen*();
BEGIN
Out.String(" Hoehe = ");
Out.Real(q.hoehe,5);
END printLaengen;
PROCEDURE (q : QuadratRef) printFlaeche*();
BEGIN
Out.String(" Flaeche = ");
Out.Real(q.hoehe*q.hoehe, 10);
END printFlaeche;
56
PROCEDURE (q : QuadratRef) strecken*(faktor : REAL);
BEGIN
q.hoehe := q.hoehe*faktor;
END strecken;
PROCEDURE (q : QuadratRef) zeichnen*();
VAR i : INTEGER;
BEGIN
(* Gehe zum Punkt (q.xpos; q.ypos) *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Gehe(q.xpos);
MKTurtleLight.Drehe(90);
MKTurtleLight.Gehe(q.ypos);
MKTurtleLight.Drehe(−90);
MKTurtleLight.StiftRunter;
(* Zeichne Quadrat *)
FOR i:= 1 TO 4 DO
MKTurtleLight.Gehe(q.hoehe);
MKTurtleLight.Drehe(−90);
END;
(* Gehe zum Koordinatenursprung *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(q.ypos);
MKTurtleLight.Drehe(−90);
MKTurtleLight.Gehe(q.xpos);
MKTurtleLight.Drehe(180);
MKTurtleLight.StiftRunter;
END zeichnen;
BEGIN
END Quadrat.
(* Datei: Figuren/Kreis.mod *)
MODULE Kreis;
IMPORT Out, Figur, MKTurtleLight;
TYPE
Kreis* = RECORD (Figur.Figur)
radius * : REAL;
END;
KreisRef* = POINTER TO Kreis;
PROCEDURE NEWKreis*(x, y : INTEGER; radius : REAL) : KreisRef ;
VAR tmp : KreisRef;
BEGIN
NEW(tmp);
tmp.xpos := x;
tmp.ypos := y;
tmp.radius := radius;
RETURN tmp;
END NEWKreis;
PROCEDURE (k : KreisRef) printTyp*();
BEGIN
Out.String("Kreis");
END printTyp;
PROCEDURE (k : KreisRef) printLaengen*();
BEGIN
Out.String(" Radius = ");
Out.Real(k.radius,5);
END printLaengen;
57
PROCEDURE (k : KreisRef) printFlaeche*();
BEGIN
Out.String(" Flaeche = ");
Out.Real(3.14*k.radius*k.radius , 10);
END printFlaeche;
PROCEDURE (k : KreisRef) strecken*(faktor : REAL);
BEGIN
k.radius := k.radius*faktor;
END strecken;
PROCEDURE (k : KreisRef) zeichnen*();
VAR i : INTEGER;
schritt : REAL;
BEGIN
(* Gehe zum Punkt (k.xpos; k.ypos) *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Gehe(k.xpos+k.radius);
MKTurtleLight.Drehe(90);
MKTurtleLight.Gehe(k.ypos);
MKTurtleLight.StiftRunter;
(* Zeichne Kreis *)
schritt := (3.14* k.radius)/180;
FOR i:= 1 TO 360 DO
MKTurtleLight.Gehe(schritt);
MKTurtleLight.Drehe(1);
END;
(* Gehe zum Koordinatenursprung *)
MKTurtleLight.StiftHoch;
MKTurtleLight.Drehe(90);
MKTurtleLight.Gehe(k.radius+k.xpos);
MKTurtleLight.Drehe(90);
MKTurtleLight.Gehe(k.ypos);
MKTurtleLight.Drehe(90);
MKTurtleLight.StiftRunter;
END zeichnen;
BEGIN
END Kreis.
Damit die Turtle-Grak richtig arbeitet, muss sie zunächst aktiviert werden. Anschlieÿend kann die Methode zeichnen() für beliebige Figur -Objekte aufgerufen werden.
Nach dem Abarbeiten der Programmbefehle sollte die Turtle wieder deaktiviert werden.
Von den Klassen Rechteck, Quadrat und Kreis können beliebig viele Instanzen angelegt werden. Man kann sie sowohl mit einer Variablen jedes eigenen Typs als auch in
Variable vom Typ Figur referenzieren. Die Referenztypen der Klassen können als Komponententyp für Arrays genutzt werden. Ein
ARRAY n OF Figur.FigurRef
kann Ob-
jekte der einzelnen Klassen sammeln. Die einzelnen Komponenten werden über einen
Index angesprochen.
58
(* Datei: Figuren/TestFigur1.mod *)
MODULE TestFigur1;
IMPORT Out, Figur, Kreis, Quadrat, Rechteck, MKTurtleLight;
PROCEDURE ProgMain*();
VAR testQ : Quadrat.QuadratRef;
testK : Kreis.KreisRef;
i : INTEGER;
BEGIN
MKTurtleLight.TurtleEin;
testQ := Quadrat.NEWQuadrat(−350,200,10);
testK := Kreis.NEWKreis(50,50,80);
FOR i:= 0 TO 18 DO
testQ.zeichnen();
testQ.strecken (1.2);
testK.zeichnen();
testK.verschieben (10,0);
END;
MKTurtleLight.TurtleAus;
END ProgMain;
BEGIN
Out.Open
END TestFigur1.
(* Datei: Figuren/TestFigur2.mod *)
MODULE TestFigur2;
IMPORT Out, Figur, Kreis, Quadrat, Rechteck;
PROCEDURE ProgMain*();
VAR test : ARRAY 3 OF Figur.FigurRef;
i : INTEGER;
BEGIN
test [0] := Rechteck.NEWRechteck(100,100,40,60);
test [1] := Quadrat.NEWQuadrat(200,200,40);
test [2] := Kreis.NEWKreis(300,300,50);
FOR i:=0 TO LEN(test)−1 DO
test [ i ]. printTyp();
test [ i ]. printPosition ();
test [ i ]. printLaengen();
test [ i ]. printFlaeche ();
Out.Ln;
test [ i ]. strecken (1.5);
test [ i ]. verschieben(10,−5);
test [ i ]. printTyp();
test [ i ]. printPosition ();
test [ i ]. printLaengen();
test [ i ]. printFlaeche ();
Out.Ln; Out.Ln;
END;
END ProgMain;
BEGIN
Out.Open
END TestFigur2.
59
(* Datei: Figuren/TestFigur3.mod *)
MODULE TestFigur3;
IMPORT Out, Figur, Kreis, Quadrat, Rechteck, MKTurtleLight;
PROCEDURE ProgMain*();
VAR test : ARRAY 6 OF Figur.FigurRef;
i : INTEGER;
BEGIN
MKTurtleLight.TurtleEin;
test [0] :=
test [1] :=
test [2] :=
test [3] :=
test [4] :=
test [5] :=
Rechteck.NEWRechteck(−110,100,110,200);
Quadrat.NEWQuadrat(−85,75,60);
Rechteck.NEWRechteck(0,0,200,100);
Rechteck.NEWRechteck(140,50,30,50);
Kreis.NEWKreis(−30,−100,50);
Kreis.NEWKreis(120,−100,50);
FOR i:=0 TO LEN(test)−1 DO
test [ i ]. zeichnen();
END;
MKTurtleLight.TurtleAus;
END ProgMain;
BEGIN
Out.Open
END TestFigur3.
60
Anhang
A Python-Quellcode
A.1 Beispiel: Konto
Aggregierung, Kapselung und Konstruktor
Python kennt keine begin-end -Klammern. Anweisungsblöcke werden durch dieselbe Einzugstiefe gekennzeichnet.
Die Kopfzeile class Klassenname: ist der Beginn einer Klassendenition. Der Klassenkörper
besteht aus einer oder mehreren eingerückten Anweisungen.
Die __init__-Methode gehört zu einer Menge von Spezialmethoden in Python. Sie wird bei
der Instanzbildung automatisch als Konstruktormethode ausgeführt. Variablen, die innerhalb
dieser Methode initialisiert werden, bilden die Attribute der Klasse.
Methoden werden im Klassenkörper als Funktionen mit dem Schlüsselwort def deklariert. An
den ersten Parameter self wird beim Aufruf automatisch das aufrufende Objekt übergeben.
#Datei: Konto3/Konto.pyw
#Beispiel: Aggregierung, Kopplung, Kapselung und Konstruktor
# −−> Aggregierung
class Konto:
# −−> Konstruktor
def __init__(self, nummer):
# Attriubte
self .kontonummer = nummer
self .saldo = 0.0
# Methoden
def getKontonummer(self):
return self.kontonummer
def getSaldo( self ):
return self.saldo
def einzahlen( self , betrag):
self .saldo = self .saldo + betrag # −−> Kopplung
def auszahlen(self , betrag):
self .saldo = self .saldo − betrag # −−> Kopplung
def auszug(self ):
print "Kontonummer: ", self.kontonummer, " Saldo: ", self.saldo, " Euro"
Damit man die denierte Klasse in anderen Programmen verwenden kann, muss das zugehörige Modul mittels from Modulname import * importiert werden.
Zur Instanzbildung dient der Klassenname als Funktionsname, um die Konstuktormethode
aufzurufen. Die angegebenen Parameter werden als Initialwerte genutzt.
Python-Module exportieren alle Namen. Es gibt keine Möglichkeit, zu spezizieren, welche
Namen auÿerhalb des Moduls nicht sichtbar sein sollen. Datenkapselung ist somit nur eine Konvention. Der Programmierer kann das Geheimnisprinzip gewährleisten, indem er auf
Datenattribute grundsätzlich nur über Methoden zugreift.
61
#Datei: Konto3/KontoTest.pyw
#Beispiel: Aggregierung, Kopplung, Kapselung und Konstruktor
from Konto import *
meinKonto = Konto(4531088) # Objekt erzeugen und initialisieren
meinKonto.saldo = 200.00 # Zugri auf Attribute
print "Kontonummer: ", meinKonto.kontonummer, " Saldo: ", meinKonto.saldo
meinKonto.einzahlen(300.00) # Methodenaufruf
meinKonto.auszug()
deinKonto = Konto(1733065) # neue Instanz
deinKonto.einzahlen(1000.00) # Datenkapselung ist Konvention
print "Kontonummer: ", meinKonto.getKontonummer(), " Saldo: ", meinKonto.getSaldo()
deinKonto.auszug()
deinKonto.einzahlen(300.00)
deinKonto.auszug()
Klassenvariablen
In Python werden Variablen nicht deklariert, sondern durch Zuweisungen deniert.
Instanzvariablen können nur im Konstruktor erzeugt werden. Alle anderen Variablen, die innerhalb einer Klasse deniert werden, sind Klassenvariablen. Der Zugri auf Klassenvariablen
erfolgt über den Namen der Klasse.
#Datei: Konto4/Konto.pyw
#Beispiel: Klassenvariablen
class Konto:
zaehler = 0 # −−> Klassenvariable
def __init__(self, nummer):
Konto.zaehler = Konto.zaehler + 1
self .kontonummer = nummer # −−> Instanzvariablen
self .saldo = 0.0
def getKontonummer(self):
return self.kontonummer
def getSaldo( self ):
return self.saldo
def einzahlen( self , betrag):
self .saldo = self .saldo + betrag
def auszahlen(self , betrag):
self .saldo = self .saldo − betrag
def auszug(self ):
print "Kontonummer: ", self.kontonummer, " Saldo: ", self.saldo, "Euro"
#Datei: Konto4/KontoTest.pyw
#Beispiel: Klassenvariablen
from Konto import *
# Zugri auf Klassenvariablen ueber Klassennamen
print"Anzahl Konten: ", Konto.zaehler
meinKonto = Konto(4531088)
print "Anzahl Konten: ", Konto.zaehler
deinKonto = Konto(1733065)
print "Anzahl Konten: ", Konto.zaehler
62
Manipulierbarkeit
#Datei: Konto5/KontoTest.pyw
#Beispiel: Manipulierbarkeit
from Konto import *
def ueberweisen(betrag, kontoA, kontoZ):
kontoA.auszug()
kontoZ.auszug()
kontoA.auszahlen(betrag)
kontoZ.einzahlen(betrag)
print "Von Konto ",kontoA.getKontonummer(),
print " wurden ",betrag," Euro ",
print "auf Konto ",kontoZ.getKontonummer()," ueberwiesen."
kontoA.auszug()
kontoZ.auszug()
meinKonto = Konto(4531088)
meinKonto.einzahlen(300.00)
deinKonto = Konto(1733065)
deinKonto.einzahlen(100.00)
# Zuweisung
unserKonto = deinKonto
# Vergleich
if meinKonto == deinKonto:
print "Mein Konto und dein Konto sind identisch."
else :
print "Mein Konto und dein Konto sind verschieden."
if unserKonto == deinKonto:
print "Unser Konto und dein Konto sind identisch."
else :
print "Unser Konto und dein Konto sind verschieden."
# Parameter
ueberweisen(30.00, meinKonto, deinKonto)
Vererbung
In der Kopfzeile der Klassendenition kann hinter dem Namen der Klasse in Klammern angegeben werden, von welchen Klassen sie abgeleitet wird. Die abgeleitete Klasse erbt dann alle
Attribute und Methoden der Superklasse.
Der Konstruktor wird in der Subklasse neu geschrieben, um weitere Attribute zu ergänzen. Die Konstrkutormethode der Superklasse kann man durch den Befehl Superklassenname.__init__(self, ...) aufrufen.
In einem Modul können mehrere Klassen deniert werden.
# −*− coding: cp1252 −*−
#Datei: Konto6/Konto.pyw
#Beispiel: Vererbung
class Konto:
zaehler = 0
def __init__(self, nummer):
Konto.zaehler = Konto.zaehler + 1
self .kontonummer = nummer
self .saldo = 0.0
def getKontonummer(self):
return self.kontonummer
63
def getSaldo( self ):
return self.saldo
def einzahlen( self , betrag):
self .saldo = self .saldo + betrag
def auszahlen(self , betrag):
self .saldo = self .saldo − betrag
def auszug(self ):
print "Kontonummer: ", self.kontonummer, " Saldo: ", self.saldo, "Euro"
# −−> Vererbung
class Sparkonto(Konto):
# Konstruktor der Klasse Sparkonto
def __init__(self, nummer, zins):
Konto.__init__(self, nummer) # Konstruktor der Superklasse Konto wird aufgerufen
self . zinssatz = zins # zusaetzliches Attribut
# neue Methoden fuer Zugri auf Attribut zinssatz
def getZinssatz( self ):
return self . zinssatz
def setZinssatz( self ,
zins ):
self . zinssatz = zins
# neue Methode
def verzinsen( self ):
self .saldo = self .saldo + ( self . zinssatz * self .saldo)/100
Um Objekte der Subklasse zu erzeugen, wird deren Name als Funktionsaufruf verwendet.
Für Instanzen der Subklasse können alle Methoden aufgerufen werden, die in der Superklasse
deniert sind.
#Datei: Konto6/KontoTest.pyw
#Beispiel: Vererbung
from Konto import *
meinSparkonto = Sparkonto(5613990,3.4)
# jede Instanz der Klasse Sparkonto hat 3 Attribute
print "Kontonummer: ",meinSparkonto.getKontonummer(), # geerbtes Attribut
print " Saldo: ", meinSparkonto.getSaldo(), " Euro", # geerbtes Attribut
print " Zinssatz: ", meinSparkonto.getZinssatz() # neu deklariertes Attriubut
# Methoden einzahlen(betrag), auszahlen(betrag) und auszug() koennen verwendet werden
meinSparkonto.einzahlen(250.00) # geerbte Methoden
meinSparkonto.auszug()
meinSparkonto.auszahlen(10.00)
meinSparkonto.auszug()
# ausserdem gibt es fuer Instanzen der Klasse Sparkonto die Methode verzinsen()
meinSparkonto.verzinsen() # neu implementierte Methode
meinSparkonto.auszug()
64
Reimplementierung
Wird innerhalb der Subklasse eine Methode der Superklasse mit gleichem Namen und gleicher
Signatur erneut deniert, dann überschreibt sie die geerbte Methode. Die verdeckte Methode
der übergeordneten Klasse kann über den Namen der Superklasse angesprochen werden.
#Datei: Konto7/Konto.pyw
#Beispiel: Reimplementierung
class Konto:
zaehler = 0
def __init__(self, nummer):
Konto.zaehler = Konto.zaehler + 1
self .kontonummer = nummer
self .saldo = 0.0
def getKontonummer(self):
return self.kontonummer
def getSaldo( self ):
return self.saldo
def einzahlen( self , betrag):
self .saldo = self .saldo + betrag
def auszahlen(self , betrag):
self .saldo = self .saldo − betrag
def auszug(self ):
print "Kontonummer: ", self.kontonummer, " Saldo: ", self.saldo, "Euro"
class Girokonto(Konto):
def __init__(self, nummer, limit):
Konto.__init__(self, nummer)
if limit < 0 :
limit = −limit
self . kreditlimit = limit
# Methode fuer schreibenden Zugri ueberprueft ob limit positiv ist
def getLimit( self ):
if limit < 0 :
limit = −limit
return self. kreditlimit
def setLimit( self ,
limit ):
self . kreditlimit = limit
# −−> Reimplementierung
def auszahlen(self , betrag):
if betrag <= self.saldo + self . kreditlimit :
Konto.auszahlen(self , betrag)
else :
print "Limit ueberschritten!"
65
#Datei: Konto7/KontoTest.pyw
#Beispiel: Reimplementierung
from Konto import *
meinKonto = Konto(4531088)
meinKonto.einzahlen(400.00);
meinKonto.auszug();
print "Versuche 100 Euro abzuheben."
meinKonto.auszahlen(100.00); # Methode auszahlen(betrag) der Klasse Konto
meinKonto.auszug();
print "Versuche 500 Euro abzuheben."
meinKonto.auszahlen(500.00); # Methode auszahlen(betrag) der Klasse Konto
meinKonto.auszug();
print
meinGirokonto = Girokonto(1733065,100)
meinGirokonto.einzahlen(400.00);
meinGirokonto.auszug();
print "Versuche 100 Euro abzuheben."
meinGirokonto.auszahlen(100.00); # Methode auszahlen(betrag) der Klasse Girokonto
meinGirokonto.auszug();
print "Versuche 500 Euro abzuheben."
meinGirokonto.auszahlen(500.00); # Methode auszahlen(betrag) der Klasse Girokonto
meinGirokonto.auszug();
Polymorphismus
Da Variablen in Python nicht mit einem bestimmten Datentyp deklariert werden, sind alle
Variablen polymorph. Dies bedeutet, dass eine Variable auf Objekte beliebigen Typs verweisen
kann. Python merkt sich während der Ausführungszeit, welche Objektarten ein Programm
benutzt.
#Datei: Konto8/KontoTest.pyw
#Beispiel: Polymorphismus
from Konto import *
meinKonto = Konto(4531088) # meinKonto zeigt auf ein Objekt vom Typ Konto
meinKonto.einzahlen(300.00)
meinKonto.auszug()
meinKonto.auszahlen(500.00) # Methode auszahlen(betrag) der Klasse Konto wird aufgerufen
meinKonto.auszug()
print
meinKonto = Girokonto(5613990,100) # meinKonto zeigt jetzt auf ein Objekt vom Typ Girokonto
meinKonto.einzahlen(300.00)
meinKonto.auszug()
meinKonto.auszahlen(500.00) # Methode auszahlen(betrag) der Klasse Girokonto wird aufgerufen
# −−> dynamische Bindung
meinKonto.auszug()
print
meinKonto = Sparkonto(1733065,1.25) # meinKonto zeigt jetzt auf ein Objekt vom Typ Sparkonto
meinKonto.einzahlen(400.00)
meinKonto.auszug()
meinKonto.auszahlen(500.00) # Methode auszahlen(betrag) der Klasse Sparkonto wird aufgerufen,
# die sie von der Klasse Konto geerbt hat
# meinKonto.verzinsen() Fehler: die Klasse Konto kennt die Methode verzinsen() nicht
Überladen
Das Überladen von Methoden ist in Python nicht möglich.
66
A.2 Beispiel: Mediendatenbank
# Datei: Medien/Medium.pyw
class Medium:
zaehler = 0
def __init__(self, titel ,
preis ):
Medium.zaehler += 1
self .code = 100000 + Medium.zaehler
self . titel = titel
self . preis = preis
self .kommentar = ''
def getCode(self):
return self.code
def getTitel( self ):
return self. titel
def getPreis( self ):
return self. preis
def setPreis( self , preis ):
self . preis = preis
def getKommentar(self):
return self.kommentar
def setKommentar(self, kommentar):
self .kommentar = kommentar
def druckeInfo():
print "Code: ", self .code
print "Titel: " , self . titel
print "Preis: " , self . preis , " Euro"
print "Kommentar: ", self.kommentar
print
class Buch(Medium):
def __init__(self, titel , autor, verlag , jahr ,
Medium.__init__(self, titel, preis)
self .autor = autor
self . verlag = verlag
self .erscheinungsjahr = jahr
preis ):
def getAutor(self ):
return self.autor
def getVerlag( self ):
return self. verlag
def getErscheinungsjahr(self ):
return self.erscheinungsjahr
def druckeInfo( self ):
print "Code: ", self .code
print "Titel: " , self . titel
print "Autor: ", self .autor
print "Verlag: ", self . verlag , ", " , self .erscheinungsjahr
print "Preis: " , self . preis , " Euro"
print "Kommentar: ", self.kommentar
print
67
class Cd(Medium):
def __init__(self, titel ,
interpret , titelanz , zeit , preis ):
Medium.__init__(self, titel, preis)
self . interpret = interpret
self . titelanzahl = titelanz
self . spieldauer = zeit
def getInterpret( self ):
return self. interpret
def getTitelanzahl( self ):
return self. titelanzahl
def getSpieldauer( self ):
return self. spieldauer
def druckeInfo( self ):
print "Code: ", self .code
print "Titel: " , self . titel
print "Interpret: " , self . interpret
print "Spieldauer: ", self . spieldauer , " min, ", self . titelanzahl , " Titel"
print "Preis: " , self . preis , " Euro"
print "Kommentar: ", self.kommentar
print
class Video(Medium):
def __init__(self, titel , reg ,
zeit , preis ):
Medium.__init__(self, titel, preis)
self . regisseur = reg
self . spieldauer = zeit
def getRegisseur( self ):
return self. regisseur
def getSpieldauer( self ):
return self. spieldauer
def druckeInfo( self ):
print "Code: ", self .code
print "Titel: " , self . titel
print "Regisseur: ", self . regisseur
print "Spieldauer: ", self . spieldauer , " min"
print "Preis: " , self . preis , " Euro"
print "Kommentar: ", self.kommentar
print
# Datei: Medien/Datenbank.pyw
from Medium import *
class Datenbank:
def __init__(self):
self .medien = []
def erfasseMedium(self, med):
self .medien.append(med)
def auisten ( self ):
for i in range(0, len( self .medien), 1):
self .medien[i ]. druckeInfo()
68
# Datei: Medien/TestMedienDB.pyw
from Datenbank import *
from Medium import *
db = Datenbank()
db.erfasseMedium(Buch("Sprechen Sie Java","Hanspeter Moessenboeck", "dpunkt.verlag",2001,28.00))
db.erfasseMedium(Cd("No Angel", "Dido", 13, 90, 15.00))
db.erfasseMedium(Video("X−Men", "Bryan Singer", 100, 21.00))
db. auisten ()
A.3 Beispiel: Geometrische Figuren
Abstrakte Methoden werden mit Hilfe der pass -Anweisung implementiert. Da der Methodenrumpf nicht leer bleiben kann, nutzt man diese leere Platzhalteranweisung, die keine Aktion
bewirkt.
Obwohl die Klasse zu einer abstrakten Klasse wird, wenn sie abstrakte Methoden enthält,
können Instanzen der Klasse erzeugt werden.
# Datei: Figuren/Figur.pyw
from turtle import *
class Figur:
def __init__(self,x,y):
self .xpos = x
self .ypos = y
def getTyp(self):
pass
def getPosition( self ):
return "(%d;%d)" %(self.xpos, self.ypos)
def getLaengen(self):
pass
def getFlaeche( self ):
pass
def verschieben( self , dx, dy):
self .xpos += dx
self .ypos += dy
def strecken( self , faktor ):
pass
def zeichnen( self ):
pass
class Rechteck(Figur):
def __init__(self,x,y,breite,hoehe):
Figur.__init__(self,x,y)
self . breite = breite
self .hoehe = hoehe
def getTyp(self):
return "Rechteck"
def getLaengen(self):
return "Breite = %d Hoehe = %d" %(self.breite, self.hoehe)
69
def getFlaeche( self ):
return self. breite * self .hoehe
def strecken( self , faktor ):
self . breite *= faktor
self .hoehe *= faktor
def zeichnen( self ):
up()
goto([ self .xpos, self .ypos])
down()
forward( self . breite )
right(90)
forward( self .hoehe)
right(90)
forward( self . breite )
right(90)
forward( self .hoehe)
right(90)
class Quadrat(Figur):
def __init__(self,x,y,hoehe):
Figur.__init__(self,x,y)
self .hoehe = hoehe
def getTyp(self):
return "Quadrat"
def getLaengen(self):
return "Hoehe = %d" %(self.hoehe)
def getFlaeche( self ):
return self.hoehe**2
def strecken( self , faktor ):
self .hoehe *= faktor
def zeichnen( self ):
up()
goto([ self .xpos, self .ypos])
down()
for i in range (0,4,1):
forward( self .hoehe)
right(90)
class Kreis(Figur):
PI = 3.14
def __init__(self,x,y,radius):
Figur.__init__(self,x,y)
self .radius = radius
def getTyp(self):
return "Kreis"
def getLaengen(self):
return "Radius = %d" %(self.radius)
def getFlaeche( self ):
return Kreis.PI*self.radius**2
def strecken( self , faktor ):
self .radius *= faktor
70
def zeichnen( self ):
schritt = Kreis.PI* self .radius/180
up()
goto([ self .xpos+self.radius, self .ypos])
down()
left (90)
for i in range (0,360,1):
forward(schritt )
left (1)
right(90)
# Datei: Figuren/TestFigur1.pyw
from Figur import *
testQ = Quadrat(−150,100,20)
testK = Kreis(200,0,80)
for i in range
(0,15,1):
testQ.zeichnen()
testQ.strecken (1.2)
for i in range
(0,10,1):
testK.zeichnen()
testK.verschieben(10,0)
# Datei: Figuren/TestFigur2.pyw
from Figur import *
test = [Rechteck(100,100,40,40), Quadrat(200,200,40), Kreis(300,300,50)]
for i in range (0,3,1):
print test[ i ]. getTyp()," −−> ",
print test[ i ]. getPosition (),
print test[ i ]. getLaengen(),
print "Flaeche = ", test[i ]. getFlaeche()
test [ i ]. strecken (1.5)
test [ i ]. verschieben(10,−5)
print test[ i ]. getTyp()," −−> ",
print test[ i ]. getPosition (),
print test[ i ]. getLaengen(),
print "Flaeche = ", test[i ]. getFlaeche()
# Datei: Figuren/TestFigur3.pyw
from Figur import *
test
test
test
test
test
test
test
= []
+= [Rechteck(−110,100,110,200)]
+= [Quadrat(−85,75,60)]
+= [Rechteck(0,0,200,100)]
+= [Rechteck(140,50,30,50)]
+= [Kreis(−30,−100,50)]
+= [Kreis(120,−100,50)]
for i in range
(0,6,1):
test [ i ]. zeichnen()
71
B Delphi-Quellcode
B.1 Beispiel: Konto
Aggregierung und Kopplung
Klassen werden in einer Unit beschrieben. Der Name der Unit muss sich dabei vom Klassennamen unterscheiden.
Die Denition der Klasse erfolgt als Typdeklaration, eingeleitet durch das Schlüsselwort type.
Auÿerdem wird mittels Klassenname = class angegeben, dass es sich um eine Klassendenition handelt. In der Klassenbeschreibung müssen die zugehörigen Attribute und Methoden
deklariert werden.
Die Implementierung der Methoden erfolgt gesondert im Implementationsteil der Unit. Für
jede Methode, die deklariert worden ist, muss hier der Methodenrumpf geschrieben werden.
Dabei wird explizit angegeben, zu welcher Klasse die Methode gehört.
{Datei: Konto1/KontoU.pas}
{Beispiel : Aggregierung und Kopplung}
unit KontoU;
interface
{ −−> Aggregierung}
type
Konto = class
public
{Attribute}
kontonummerF : Integer;
saldoF : Double;
{Methoden}
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double);
procedure auszug;
end;
implementation
{Implementierung der Methoden}
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
{−−> Kopplung}
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
{−−> Kopplung}
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
Das Testprogramm ist eine einfache Konsolenanwendung, die alle notwendigen Units unter
uses importiert.
Variablen werden auÿerhalb des Hauptprogrammes deklariert und müssen später initialisiert
werden.
72
Die Funktion create erzeugt ein Objekt der angegebenen Klasse und liefert dessen Speicheradresse als Rückgabewert. Sie wird genutzt, um die deklarierten Variablen mit Werten zu
belegen.
Verweist eine Variable auf eine Instanz einer Klasse, dann können deren Attribute und Methoden über den Namen der Variable angesprochen werden.
{Datei: Konto1/KntoTest.dpr}
{Beispiel : Aggregierung und Kopplung}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas';
var meinKonto, deinKonto : Konto; {Variablen vom Typ Konto}
begin
meinKonto := Konto.create; {Objekt vom Typ Konto wird erzeugt}
meinKonto.kontonummerF := 4531088; {Zugri auf Attribute}
meinKonto.saldoF := 200.00;
Writeln('Konotnummer: ', meinKonto.kontonummerF,' Saldo: ', meinKonto.saldoF:8:2);
meinKonto.einzahlen(300.00); {Methodenaufruf}
meinKonto.auszug;
Writeln;
deinKonto := Konto.create; {deinKonto zeigt auf neue Intanz der Klasse Konto}
deinKonto.kontonummerF := 1733065;
deinKonto.saldoF := 1000.00;
deinKonto.auszug;
deinKonto.auszahlen(100.00);
deinKonto.auszug;
Readln
end.
Kapselung
Die Klassendenition wird in mehrere Bereiche unterteilt. Der erste Teil - gekennzeichnet
durch das Schlüsselwort private - enthält alle Attribute und Methoden, auf die man von anderen Units aus nicht zugreifen kann. Sie sind somit gekapselt. Im public -Teil sind diejenigen
Komponenten deklariert, die uneingeschränkt sichtbar sein sollen.
Anstelle der für gekapselte Attribute notwendigen get- und set-Methoden werden in Delphi
Eigenschaften - sogenannte properties - verwendet. Jede Eigenschaft hat einen read - und einen
write -Zweig. Dort können Datenfelder oder Methoden eingetragen werden, die angeben, was
getan wird, wenn der Wert der Eigenschaft gesetzt oder gelesen werden soll. Der write -Zweig
ist optional.
73
{Datei: Konto2/KontoU.pas}
{Beispiel : Kapselung}
unit KontoU;
interface
type
Konto = class
{ −−> Datenkapselung }
private
kontonummerF : Integer;
saldoF : Double;
public
property kontonummer : Integer { Property fuer sicheren Zugri auf Attibut kontonummerF }
read kontonummerF write kontonummerF;
property saldo : Double { Property fuer sicheren Zugri auf Attribut saldoF }
read saldoF write saldoF;
{ urspruengliche Methoden }
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double);
procedure auszug;
end;
implementation
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
Anstelle der Attribute nutzt das Testprogramm die denierten Properties, um die Datenfelder
zu initialisieren bzw. zu lesen.
{Datei: Konto2/KontoTest.dpr}
{Beispiel : Kapselung}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas';
var meinKonto : Konto;
begin
meinKonto := Konto.create;
{ meinKonto.kontonummerF := 4531088; !!Fehler
Writeln(meinKonto.kontonummerF); !!Fehler }
meinKonto.kontonummer := 4531088; { schreibender Zugri auf Attribute ueber Properties }
meinKonto.saldo := 0.00;
Writeln('Kontonummer: ', meinKonto.kontonummer,' Saldo: ', meinKonto.saldo:8:2);
Readln
end.
74
Konstruktor
Zur Denition eines Konstruktors wird das Schlüsselwort constructor vor die Methodendeklaration gesetzt. Der Konstruktor heiÿt üblicherweise create. Wie alle anderen Methoden
muss er im Implementierungsteil beschrieben werden.
{Datei: Konto3/KontoU.pas}
{Beispiel : Konstruktor}
unit KontoU;
interface
type
Konto = class
private
kontonummerF : Integer;
saldoF : Double;
public
{−−> Konstruktor}
constructor create(nummer : Integer);
property kontonummer : Integer
read kontonummerF ; {write entfernt}
property saldo : Double
read saldoF ; {write entfernt}
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double);
procedure auszug;
end;
implementation
{Implementierung des Konstruktors}
constructor Konto.create(nummer : Integer);
begin
kontonummerF := nummer;
saldoF := 0.0;
end;
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
75
{Datei: Konto3/KontoTest.dpr}
{Beispiel : Konstruktor}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas';
var meinKonto : Konto;
begin
{ bisher :
meinKonto := Konto.create; // Objekt erzeugen
meinKonto.kontonummer := 4531088; // Kontonummer initialisieren
meinKonto.saldo := 0.00; //Kontostand initialisieren }
{ jetzt :
}
meinKonto := Konto.create(4531088); {Konstruktor erzeugt Objet und initialisiert Attribute}
meinKonto.auszug;
Readln
end.
Klassenvariablen
Variablen, die auÿerhalb der Klassendenition deklariert sind, können als Klassenvariablen
verwendet werden. Sie gehören jedoch zur Unit und nicht zur Klasse, deshalb werden sie
auÿerhalb der Unit über den Unitnamen angesprochen.
{Datei: Konto4/KontoU.pas}
{Beispiel : Klassenvariablen}
unit KontoU;
interface
type
Konto = class
private
kontonummerF : Integer; { −−> Instanzvariablen }
saldoF : Double;
public
constructor create(nummer : Integer);
property kontonummer : Integer
read kontonummerF ;
property saldo : Double
read saldoF ;
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double);
procedure auszug;
end;
var zaehler : Cardinal = 0; {−−> Variable der Unit, ersetzt Klassenvariable }
implementation
constructor Konto.create(nummer : Integer);
begin
zaehler := zaehler + 1;
kontonummerF := nummer;
saldoF := 0.00;
end;
76
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
{Datei: Konto4/KontoTest.dpr}
{Beispiel : Klassenvariablen}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas';
var meinKonto, deinKonto : Konto;
begin
{ Zugri auf Klassenvariable ueber Namen der Unit }
Writeln('Anzahl Konten: ', KontoU.zaehler);
meinKonto := Konto.create(4531088);
Writeln('Anzahl Konten: ', KontoU.zaehler);
deinKonto := Konto.create(1733065);
Writeln('Anzahl Konten: ', KontoU.zaehler);
Readln
end.
77
Manipulierbarkeit
{Datei: Konto5/KontoTest.dpr}
{Beispiel : Manipulierbarkeit}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas';
var meinKonto, deinKonto, unserKonto : Konto;
procedure ueberweisen(betrag : Double; kontoA, kontoZ : Konto);
begin
kontoA.auszug;
kontoZ.auszug;
kontoA.auszahlen(betrag);
kontoZ.einzahlen(betrag);
Write('Von Konto ',kontoA.kontonummer);
Write(' wurden ',betrag:8:2,' Euro ');
Writeln('auf Konto ',kontoZ.kontonummer,' ueberwiesen.');
kontoA.auszug;
kontoZ.auszug
end;
begin
meinKonto := Konto.create(4531088);
meinKonto.einzahlen(300.00);
deinKonto := Konto.create(1733065);
meinKonto.einzahlen(100.00);
{ Zuweisung }
unserKonto := deinKonto;
{ Vergleich }
if meinKonto = deinKonto
then Writeln('Mein Konto und dein Konto sind identisch.')
else Writeln('Mein Konto und dein Konto sind verschieden.');
if unserKonto = deinKonto
then Writeln('Unser Konto und dein Konto sind identisch.')
else Writeln('Unser Konto und dein Konto sind verschieden.');
{ Parameter }
ueberweisen(30.00,meinKonto,deinKonto);
Readln
end.
78
Vererbung
Damit die Attribute, Methoden und Eigenschaften der Superklasse in den Subklassen sichtbar
sind, müssen sie public oder protected deklariert werden. Dabei bedeutet protected, dass
man auf diese Komponenten ausschlieÿlich in Subklassen zugreifen kann. In anderen Programmen oder Klassen sind sie nicht sichtbar. Komponenten die man als private deniert
sind in der Subklasse nur dann sichtbar, wenn sie sich in der selben Unit bendet.
{Datei: Konto6/KontoU.pas}
{Beispiel : Vererbung}
unit KontoU;
interface
type
Konto = class
protected
kontonummerF : Integer; { Attribute sollen vererbt werden }
saldoF : Double;
public
constructor create(nummer : Integer);
property kontonummer : Integer
read kontonummerF ;
property saldo : Double
read saldoF ;
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double);
procedure auszug;
end;
var zaehler : Cardinal = 0;
implementation
constructor Konto.create(nummer : Integer);
begin
zaehler := zaehler + 1;
kontonummerF := nummer;
saldoF := 0.00;
end;
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
Nach dem Schlüsselwort class kann man in Klammern angeben, von welcher Klasse die neue
Klasse abgeleitet wird. Die Subklasse kann in der selben Unit deniert werden wie die Superklasse. Steht die Superklasse in einer anderen Unit, muss diese importiert werden.
Der Konstuktor der Superklasse wird vom Konstruktor der Subklasse verdeckt. Soll er trotzdem aufgerufen werden, muss das Schlüsselwort inherited vor dem Methodenaufruf stehen.
79
{Datei: Konto6/SparkontoU.pas}
{Beispiel : Vererbung}
unit SparkontoU;
interface
uses
SysUtils , KontoU ;
type
Sparkonto = class(Konto)
private
zinssatzF : Double; { zusaetzliches Attribut }
public
constructor create(nummer : Integer; zins : Double);
{ neue Property fuer den Zugri auf Attribut zinssatzF }
property zinssatz : Double
read zinssatzF write zinssatzF;
{ neue Methode }
procedure verzinsen;
end;
implementation
constructor Sparkonto.create(nummer : Integer; zins : Double);
begin
inherited create(nummer); { Konstruktor der Superklasse Konto wird aufgerufen }
zinssatzF := zins ;
end;
procedure Sparkonto.verzinsen;
begin
saldoF := saldoF + (zinssatzF*saldo)/100;
end;
end.
{Datei: Konto6/KontoTest.dpr}
{Beispiel : Vererung}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas',
SparkontoU in 'SparkontoU.pas';
var meinSparkonto : Sparkonto;
begin
meinSparkonto := Sparkonto.create(5613990 , 3.4);
{ Jede Instanz der Klasse Sparkonto hat 3 Attribute }
Write('Kontonummer: ',meinSparkonto.kontonummer); { geerbtes Attribut }
Write(' Saldo: ',meinSparkonto.saldo:8:2,' Euro'); { geerbtes Attribut }
Writeln(' Zinssatz: ', meinSparkonto.zinssatz :4:2); { neu deklariertes Attribut }
{ Methoden einzahlen(betrag), auszahlen(betrag) und auszug koennen verwendet werden }
meinSparkonto.einzahlen(250.00); {geerbte Methoden }
meinSparkonto.auszug;
meinSparkonto.auszahlen(10.00);
meinSparkonto.auszug;
{ ausserdem gibt es fuer Instanzen der Klasse Sparkonto die Methode verzinsen }
meinSparkonto.verzinsen; { neu implementierte Methode }
meinSparkonto.auszug;
Readln
end.
80
Reimplementierung
Wird innerhalb der Subklasse eine Methode der Superklasse mit gleichem Namen und gleicher
Signatur erneut deniert, dann überschreibt sie die geerbte Methode. Auf verdeckte Komponenten der übergeordneten Klasse kann man durch Verwendung des Schlüsselworts inherited
zugreifen.
Methoden werden in ObjectPascal üblicherweise statisch gebunden, d.h. der Compiler legt
zur Übersetzungszeit fest, welche Implementierung ausgeführt wird. Sollen Methoden wie in
anderen objektorientierten Programmiersprachen dynamisch gebunden werden, so muss dies
durch das Schlüsselwort dynamic expilizit gefordert werden.
{Datei: Konto7/KontoU.pas}
{Beispiel : Reimplementierung}
unit KontoU;
interface
type
Konto = class
protected
kontonummerF : Integer;
saldoF : Double;
public
constructor create(nummer : Integer);
property kontonummer : Integer
read kontonummerF ;
property saldo : Double
read saldoF ;
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double); dynamic;
procedure auszug;
end;
var zaehler : Cardinal = 0;
implementation
constructor Konto.create(nummer : Integer);
begin
zaehler := zaehler + 1;
kontonummerF := nummer;
saldoF := 0.00;
end;
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
Das Schlüsselwort override weist daraufhin, dass diese Methode eine andere Implementierung
überschreibt.
81
{Datei: Konto7/GirokontoU.pas}
{Beispiel : Reimplementierung}
unit GirokontoU;
interface
uses
KontoU;
type
Girokonto = class(Konto)
private
limitF : Double;
procedure setLimit(limit : Double);
public
constructor create(nummer : Integer; limit : Double);
property limit : Double
read limitF write setLimit;
procedure auszahlen(betrag : Double); override;
end;
implementation
constructor Girokonto.create(nummer : Integer; limit : Double);
begin
inherited create(nummer);
if limit < 0
then limit := −limit;
limitF := limit ;
end;
{ Methode fuer schreibenden Zugri ueberprueft ob limit positiv ist }
procedure Girokonto.setLimit(limit : Double);
begin
if limit < 0
then limit := −limit;
limitF := limit
end;
{ −−> Reimplementierung }
procedure Girokonto.auszahlen(betrag : Double);
begin
if betrag <= saldoF + limitF
then inherited auszahlen(betrag)
else Writeln('Limit ueberschritten!')
end;
end.
{Datei: Konto7/KontoTest.dpr}
{Beispiel : Reimplementierung}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas',
GirokontoU in 'GirokontoU.pas';
var meinKonto : Konto;
meinGirokonto : Girokonto;
begin
meinKonto := Konto.create(4531088);
meinKonto.einzahlen(400.00);
meinKonto.auszug;
Writeln('Versuche 100 Euro abzuheben.');
meinKonto.auszahlen(100.00); { Methode auszahlen(betrag) der Klasse Konto}
82
meinKonto.auszug;
Writeln('Versuche 500 Euro abzuheben.');
meinKonto.auszahlen(500.00); { Methode auszahlen(betrag) der Klasse Konto}
meinKonto.auszug;
Writeln;
meinGirokonto := Girokonto.create(1733065,100);
meinGirokonto.einzahlen(400.00);
meinGirokonto.auszug;
Writeln('Versuche 100 Euro abzuheben.');
meinGirokonto.auszahlen(100.00); { Methode auszahlen(betrag) der Klasse Girokonto }
meinGirokonto.auszug;
Writeln('Versuche 500 Euro abzuheben.');
meinGirokonto.auszahlen(500.00); { Methode auszahlen(betrag) der Klasse Girokonto }
meinGirokonto.auszug;
Readln
end.
Polymorphismus
Die Deklaration der Variablen gibt ihren statischen Typ an. Eine Variable kann während der
Ausführungszeit auch auf Instanzen der Subklassen verweisen.
Damit zur Laufzeit die richtigen Aktionen ausgeführt werden, müssen Methoden, die mehrfach
implementiert sind, dynamisch gebunden werden.
{Datei: Konto8/KontoTest.dpr}
{Beispiel : Polymorphismus}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas',
SparkontoU in 'SparkontoU.pas',
GirokontoU in 'GirokontoU.pas';
var meinKonto : Konto; { Variable vom Typ Konto }
begin
meinKonto := Konto.create(4531088); { meinKonto zeigt auf ein Objekt vom Typ Konto}
meinKonto.einzahlen(300.00);
meinKonto.auszug;
meinKonto.auszahlen(500.00); { Methode auszahlen(betrag) der Klasse Konto wird aufgerufen }
meinKonto.auszug;
Writeln;
meinKonto := Girokonto.create(1733065,100); { meinKonto zeigt jetzt auf ein Objekt vom Typ Girokonto}
meinKonto.einzahlen(300.00);
meinKonto.auszug;
meinKonto.auszahlen(500.00); { Methode auszahlen(betrag) der Klasse Girokonto wird aufgerufen }
{ −−> dynamische Bindung }
meinKonto.auszug;
Writeln;
meinKonto := sparkonto.create(1733065,1.25); { meinKonto zeigt jetzt auf ein Objekt vom Typ Sparkonto }
meinKonto.einzahlen(400.00);
meinKonto.auszug;
meinKonto.auszahlen(500.00); { Methode auszahlen(betrag) der Klasse Sparkonto wird aufgerufen, }
{ die sie von der Klasse Konto geerbt hat }
meinKonto.auszug;
{ meinKonto.verzinsen; // Fehler: die Klasse Konto kennt die Methode verzinsen() nicht }
Readln
end.
83
Überladen
Methoden können mit gleichen Namen, aber unterschiedlichen Signaturen mehrfach innerhalb
einer Klasse deklariert werden. Solche Methoden werden mit dem Zusatz overload versehen.
{Datei: Konto9/KontoU.pas}
{Beispiel : Ueberladen}
unit KontoU;
interface
type
Konto = class
protected
kontonummerF : Integer;
saldoF : Double;
public
{ bisheriger Konstruktor mit 1 Parameter }
constructor create(nummer : Integer); overload;
{ weiterer Konstruktor mit 2 Parameter }
constructor create(nummer : Integer; betrag : Double); overload ;
property kontonummer : Integer
read kontonummerF ;
property saldo : Double
read saldoF ;
procedure einzahlen(betrag : Double);
procedure auszahlen(betrag : Double); dynamic;
procedure auszug;
end;
var zaehler : Cardinal = 0;
implementation
constructor Konto.create(nummer : Integer);
begin
zaehler := zaehler + 1;
kontonummerF := nummer;
saldoF := 0.00;
end;
constructor Konto.create(nummer : Integer ; betrag : Double);
begin
zaehler := zaehler + 1;
kontonummerF := nummer;
saldoF := betrag;
end;
procedure Konto.einzahlen(betrag : Double);
begin
saldoF := saldoF + betrag
end;
procedure Konto.auszahlen(betrag : Double);
begin
saldoF := saldoF − betrag
end;
procedure Konto.auszug;
begin
Writeln('Kontonummer: ',kontonummerF,' Saldo: ',saldoF:8:2,' Euro')
end;
end.
84
{Datei: Konto9/SparkontoU.pas}
{Beispiel : Ueberladen}
unit SparkontoU;
interface
uses SysUtils, KontoU ;
type
Sparkonto = class(Konto)
private
zinssatzF : Double;
public
{ bisheriger Konstruktor mit 2 Parameter }
constructor create(nummer : Integer; zins : Double); overload;
{ neuer Konstruktor mit 3 Parameter }
constructor create(nummer : Integer; betrag, zins : Double); overload;
property zinssatz : Double
read zinssatzF write zinssatzF;
procedure verzinsen;
end;
implementation
constructor Sparkonto.create(nummer : Integer; zins : Double);
begin
inherited create(nummer); { ruft 1−stelligen Konstruktor der Superklasse auf }
zinssatzF := zins ;
end;
constructor Sparkonto.create(nummer : Integer; betrag : Double; zins : Double);
begin
inherited create(nummer , betrag); { ruft 2− stelligen Konstruktor der Superklasse auf }
zinssatzF := zins ;
end;
procedure Sparkonto.verzinsen;
begin
saldoF := saldoF + (zinssatzF*saldo)/100;
end;
end.
{Datei: Konto9/GirokontoU.pas}
{Beispiel : Ueberladen}
unit GirokontoU;
interface
uses KontoU;
type
Girokonto = class(Konto)
private
limitF : Double;
procedure setLimit(limit : Double);
public
{ bisheriger Konstruktor mit 2 Parameter }
constructor create(nummer : Integer; limit : Double); overload;
{ neuer Konstruktor mit 3 Parameter }
constructor create(nummer : Integer; betrag, limit : Double); overload;
property limit : Double
read limitF write setLimit;
procedure auszahlen(betrag : Double); override;
end;
implementation
85
constructor Girokonto.create(nummer : Integer; limit : Double);
begin
inherited create(nummer); { ruft 1− stelligen Konstruktor der Superklasse auf }
if limit < 0
then limit := −limit;
limitF := limit ;
end;
constructor Girokonto.create(nummer : Integer; betrag : Double; limit : Double);
begin
inherited create(nummer,betrag); { ruft 2− stelligen Konstruktor der Superklasse auf }
if limit < 0
then limit := −limit;
limitF := limit ;
end;
procedure Girokonto.setLimit(limit : Double);
begin
if limit < 0
then limit := −limit;
limitF := limit
end;
procedure Girokonto.auszahlen(betrag : Double);
begin
if betrag <= saldoF + limitF
then saldoF := saldoF − betrag
else Writeln('Limit ueberschritten!')
end;
end.
{Datei: Konto9/KontoTest.dpr}
{Beispiel : Ueberladen}
program KontoTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
KontoU in 'KontoU.pas',
SparkontoU in 'SparkontoU.pas',
GirokontoU in 'GirokontoU.pas';
var meinKonto, deinKonto : Konto;
meinSparkonto, deinSparkonto : Sparkonto;
meinGirokonto, deinGirokonto : Girokonto;
begin
meinKonto := Konto.create(4531088);
meinKonto.auszug;
meinKonto.einzahlen(300.00);
meinKonto.auszug;
Writeln;
deinKonto := Konto.create(1733065, 200.00);
deinKonto.auszug;
deinKonto.einzahlen(300.00);
deinKonto.auszug;
Writeln;
meinSparkonto := Sparkonto.create(5613990,1.25);
meinSparkonto.auszug;
meinSparkonto.einzahlen(600.00);
meinSparkonto.auszug;
Writeln;
deinSparkonto := Sparkonto.create(7835406,500.0,1.3);
deinSparkonto.auszug;
86
deinSparkonto.einzahlen(600.00);
deinSparkonto.auszug;
Writeln;
meinGirokonto := Girokonto.create(2571183,400.00);
meinGirokonto.auszug;
meinGirokonto.einzahlen(250.00);
meinGirokonto.auszug;
Writeln;
deinGirokonto := Girokonto.create(6464951,600.00,400.00);
deinGirokonto.auszug;
deinGirokonto.einzahlen(250.00);
deinGirokonto.auszug;
Writeln;
Readln
end.
B.2 Beispiel: Mediendatenbank
unit MediumU;
interface
type
Medium = class
protected
codeF : Integer ;
titelF : String;
preisF : Double;
kommentarF : String;
public
constructor create(titel : String; preis : Double);
property code : Integer read codeF;
property titel : String read titelF;
property preis : Double read preisF write preisF;
property kommentar : String read kommentarF write kommentarF;
procedure druckeInfo; dynamic;
end;
var zaehler : Integer =0;
implementation
constructor Medium.create(titel : String; preis : Double);
begin
zaehler := zaehler +1;
self .codeF := 100000 + zaehler;
self . titelF := titel ;
self .preisF := preis ;
self .kommentarF := '';
end;
procedure Medium.druckeInfo;
begin
Writeln('Code: ', self .codeF);
Writeln('Titel: ' , self . titelF );
Writeln('Preis: ', self .preisF , ' Euro');
Writeln('Kommentar: ', self.Kommentar);
Writeln;
end;
end.
87
unit BuchU;
interface
uses MediumU;
type
Buch = class(Medium)
protected
autorF : String;
verlagF : String;
erscheinungsjahrF : Integer ;
public
constructor create(titel, autor, verlag : String; jahr : Integer ; preis : Double);
property autor : String read autorF;
property verlag : String read verlagF;
property erscheinungsjahr : Integer read erscheinungsjahrF;
procedure druckeInfo; override;
end;
implementation
constructor Buch.create(titel, autor, verlag : String; jahr : Integer ; preis : Double);
begin
inherited create( titel , preis );
self .autorF := autor;
self .verlagF := verlag;
self .erscheinungsjahrF := jahr;
end;
procedure Buch.druckeInfo;
begin
Writeln('Code: ', self .codeF);
Writeln('Titel: ' , self . titelF );
Writeln('Autor: ', self .autorF);
Writeln('Verlag: ', self .verlagF, ' , ' , self .erscheinungsjahrF);
Writeln('Preis: ', self .preisF :6:2, ' Euro');
Writeln('Kommentar: ', self.Kommentar);
Writeln;
end;
end.
unit CdU;
interface
uses MediumU;
type
Cd = class(Medium)
protected
interpretF : String;
titelanzahlF : Integer ;
spieldauerF : Integer ;
public
constructor create(titel, interpret : String; titelanz ,
property interpret : String read interpretF;
property titelanzahl : Integer read titelanzahlF;
property spieldauer : Integer read spieldauerF;
procedure druckeInfo; override;
end;
implementation
constructor Cd.create(titel, interpret : String; titelanz ,
begin
inherited create( titel , preis );
self .interpretF := interpret ;
self . titelanzahlF := titelanz ;
self .spieldauerF := zeit ;
end;
88
zeit : Integer ; preis : Double);
zeit : Integer ; preis : Double);
procedure Cd.druckeInfo;
begin
Writeln('Code: ', self .codeF);
Writeln('Titel: ' , self . titelF );
Writeln('Interpret: ' , self .interpretF );
Writeln('Spieldauer: ', self .spieldauerF, ' min, ',
Writeln('Preis: ', self .preisF :6:2, ' Euro');
Writeln('Kommentar: ', self.Kommentar);
Writeln;
end;
end.
self . titelanzahlF , ' Titel ' );
unit VideoU;
interface
uses MediumU;
type
Video = class(Medium)
protected
regisseurF : String;
spieldauerF : Integer ;
public
constructor create(titel, reg : String; zeit : Integer ; preis : Double);
property regisseur : String read regisseurF;
property spieldauer : Integer read spieldauerF;
procedure druckeInfo; override;
end;
implementation
constructor Video.create(titel, reg : String; zeit : Integer ; preis : Double);
begin
inherited create( titel , preis );
self .regisseurF := reg;
self .spieldauerF := zeit ;
end;
procedure Video.druckeInfo;
begin
Writeln('Code: ', self .codeF);
Writeln('Titel: ' , self . titelF );
Writeln('Regisseur: ', self .regisseurF );
Writeln('Spieldauer: ', self .spieldauerF, ' min');
Writeln('Preis: ', self .preisF :6:2, ' Euro');
Writeln('Kommentar: ', self.Kommentar);
Writeln;
end;
end.
unit DatenbankU;
interface
uses MediumU;
type
Datenbank = class
private
medien : array of Medium;
public
constructor create;
procedure erfasseMedium(med : Medium);
procedure auisten;
end;
implementation
89
constructor Datenbank.create();
begin
SetLength(self.medien,0);
end;
procedure Datenbank.erfasseMedium(med : Medium);
begin
SetLength(self.medien, Length(self.medien)+1);
self .medien[Length(self.medien)−1] := med;
end;
procedure Datenbank.auisten;
var i : Integer ;
begin
for i:=0 to Length(self.medien)−1 do
self .medien[i ]. druckeInfo;
end;
end.
program TestMedienDB;
{$APPTYPE CONSOLE}
uses
SysUtils,
MediumU in 'MediumU.pas',
BuchU in 'BuchU.pas',
DatenbankU in 'DatenbankU.pas',
CdU in 'CdU.pas',
VideoU in 'VideoU.pas';
var db : Datenbank;
begin
db := Datenbank.create;
db.erfasseMedium(Buch.create('Sprechen Sie Java', 'Hanspeter Moessenboeck', 'dpunkt.verlag', 2001, 28.00));
db.erfasseMedium(Cd.create('No Angel', 'Dido' , 13, 90, 15.00));
db.erfasseMedium(Video.create('X−Men', 'Bryan Singer', 100, 21.00));
db. auisten ;
Readln
end.
90
B.3 Beispiel: Geometrische Figuren
Abstrakte Methoden werden durch das Schüsselwort abstract als solche gekennzeichnet und
im Implementationsteil ausgelassen. Für abstrakte Methoden wird durch virtual erzwungen,
dass sie dynamisch gebunden werden.
{Datei: Figuren/Figur.pas}
unit FigurU;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls;
type
Figur = class
protected
xpos, ypos : Integer ;
constructor Create(x,y : Integer);
public
function getTyp : String; virtual; abstract;
function getPosition : String;
function getLaengen : String; virtual; abstract;
function getFlaeche : Double; virtual; abstract;
procedure verschieben(dx, dy : Integer);
procedure strecken(faktor : Double); virtual; abstract;
procedure zeichnen(can : TCanvas); virtual; abstract;
end;
implementation
constructor Figur.create(x,y : Integer );
begin
self .xpos := x;
self .ypos := y;
end;
function Figur.getPosition : String;
begin
Result := ' (' +IntToStr(self.xpos)+';'+IntToStr(self.ypos)+')';
end;
procedure Figur.verschieben(dx, dy : Integer);
begin
self .xpos := self .xpos + dx;
self .ypos := self .ypos + dy;
end;
end.
{Datei: Figuren/RechteckU.pas}
unit RechteckU;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, FigurU;
type
Rechteck = class(Figur)
public
breite , hoehe : Double;
constructor create(x,y : Integer ; b,h : Double);
function getTyp : string; override;
function getLaengen : String; override;
function getFlaeche : Double; override;
procedure strecken(faktor : Double); override;
procedure zeichnen(canvas : TCanvas); override;
end;
91
implementation
constructor Rechteck.create(x,y: Integer; b,h : Double);
begin
inherited create(x,y);
self . breite := b;
self .hoehe := h;
end;
function Rechteck.getTyp : String;
begin
Result := 'Rechteck';
end;
function Rechteck.getLaengen : String;
begin
Result := ' Breite = '+FloatToStr(self.breite)+' Hoehe = '+FloatToStr(self.hoehe);
end;
function Rechteck.getFlaeche : Double;
begin
Result := self . breite * self .hoehe;
end;
procedure Rechteck.strecken(faktor : Double);
begin
self . breite := self . breite * faktor ;
self .hoehe := self .hoehe * faktor;
end;
procedure Rechteck.zeichnen(canvas : TCanvas);
begin
canvas.Rectangle(self .xpos, self .ypos, self .xpos+Round(self.breite), self.ypos+Round(self.hoehe));
end;
end.
{Datei: Figuren/QuadratU.pas}
unit QuadratU;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, FigurU;
type
Quadrat = class(Figur)
protected
hoehe : Double;
public
constructor create(x, y : Integer ; h : Double);
function getTyp : string; override;
function getLaengen : String; override;
function getFlaeche : Double; override;
procedure strecken(faktor : Double); override;
procedure zeichnen(canvas : TCanvas); override;
end;
implementation
constructor Quadrat.create(x,y : Integer; h : Double);
begin
inherited create(x,y);
self .hoehe := h;
end;
function Quadrat.getTyp : String;
begin
Result := 'Quadrat';
end;
92
function Quadrat.getLaengen : String;
begin
Result := ' Hoehe = '+FloatToStr(self.hoehe);
end;
function Quadrat.getFlaeche : Double;
begin
Result := self .hoehe * self .hoehe;
end;
procedure Quadrat.strecken(faktor : Double);
begin
self .hoehe := self .hoehe * faktor;
end;
procedure Quadrat.zeichnen(canvas : TCanvas);
var hoehe : Integer ;
begin
hoehe := Round(self.hoehe);
canvas.Rectangle(self .xpos, self .ypos, self .xpos+hoehe, self.ypos+hoehe);
end;
end.
{Datei: Figuren/KreisU.pas}
unit KreisU;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, FigurU;
type
Kreis = class(Figur)
protected
radius : Double;
public
constructor create(x,y : Integer ; r : Double);
function getTyp : string; override;
function getLaengen : String; override;
function getFlaeche : Double; override;
procedure strecken(faktor : Double); override;
procedure zeichnen(canvas : TCanvas); override;
end;
implementation
constructor Kreis.create(x,y : Integer ; r : Double);
begin
inherited create(x,y);
self .radius := r;
end;
function Kreis.getTyp : String;
begin
Result := 'Kreis' ;
end;
function Kreis.getLaengen : String;
begin
Result := ' Radius = '+FloatToStr(self.radius);
end;
function Kreis.getFlaeche : Double;
begin
Result := 3.14 * self .radius * self .radius;
end;
procedure Kreis.strecken(faktor : Double);
begin
self .radius := self .radius * faktor ;
end;
93
procedure Kreis.zeichnen(canvas : TCanvas);
var radius : Integer ;
begin
radius := Round(self.radius);
canvas. Ellipse ( self .xpos−radius, self .ypos−radius, self .xpos+radius, self .ypos+radius);
end;
end.
{Datei: Figuren/TestFigur1U.pas }
unit TestFigur1U;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, FigurU, RechteckU, QuadratU, KreisU, ExtCtrls;
type
TForm1 = class(TForm)
PaintBox1: TPaintBox;
procedure PaintBox1Paint(Sender: TObject);
private
{ Private−Deklarationen }
public
{ Public−Deklarationen }
end;
var
Form1: TForm1;
testQ : Quadrat;
testK : Kreis;
implementation
{$R *.dfm}
procedure TForm1.PaintBox1Paint(Sender: TObject);
var i : Integer ;
begin
testQ := Quadrat.create(100,100,10);
testK := Kreis.create (500,200,80);
Canvas.Brush.Style := bsClear;
for i:= 0 to 18 do
begin
testQ.zeichnen(Canvas);
testQ.strecken (1.2);
testK.zeichnen(Canvas);
testK.verschieben (10,0);
end;
end;
end.
{Datei: Figuren/TestFigur2.dpr}
program TestFigur2;
{$APPTYPE CONSOLE}
uses
SysUtils,
FigurU in 'FigurU.pas',
RechteckU in 'RechteckU.pas',
QuadratU in 'QuadratU.pas',
KreisU in 'KreisU.pas';
var test : array [0..2] of Figur;
i : Integer ;
94
begin
test [0] := Rechteck.create (100,100,40,60);
test [1] := Quadrat.create(200,200,40);
test [2] := Kreis.create (300,300,50);
for i:=0 to 2 do
begin
Write(test[i ]. getTyp, '−−>');
Write(test[i ]. getPosition );
Write(test[i ]. getLaengen);
Writeln(' Flaeche = ', test[i ]. getFlaeche :8:2);
test [ i ]. strecken (1.5);
test [ i ]. verschieben(10,−5);
Write(test[i ]. getTyp, '−−>');
Write(test[i ]. getPosition );
Write(test[i ]. getLaengen);
Writeln(' Flaeche = ', test[i ]. getFlaeche :8:2);
Writeln;
end;
Readln;
end.
{ Datei: Figuren/TestFigur3U.pas}
unit TestFigur3U;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, FigurU, RechteckU, QuadratU, KreisU;
type
TForm1 = class(TForm)
PaintBox1: TPaintBox;
procedure PaintBox1Paint(Sender: TObject);
private
{ Private−Deklarationen }
public
{ Public−Deklarationen }
end;
var
Form1: TForm1;
test : array [0..5] of Figur;
i : Integer ;
implementation
{$R *.dfm}
procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
test [0] := Rechteck.create(200,100,110,200);
test [1] := Quadrat.create(225,125,60);
test [2] := Rechteck.create(310,200,200,100);
test [3] := Rechteck.create (450,150,30,50);
test [4] := Kreis.create (280,300,50);
test [5] := Kreis.create (430,300,50);
for i:=0 to 5 do test[ i ]. zeichnen(Canvas);
end;
end.
95
Literatur
[Abt02]
D. Abts:
Grundkurs Java. Von den Grundlagen bis zu Datenbank- und Netzanwendungen,
3. Auage, Vieweg, Braunschweig/Weisbaden, 2002.
[Bal99]
H. Balzert:
Lehrbuch der Objektmodellierung. Analyse und Entwurf,
Spektrum Akademischer Verlag, Heidelberg/Berlin, 1999.
[BK03]
[BP03]
Objektorientierte Programmierung mit Java.
Eine praxisnahe Einführung mit BlueJ, Pearson Studium, München, 2003.
D. Bell/M. Parr: Java für Studenten. Grundlagen der Programmierung,
D. J. Barnes/M. Kölling:
3. überarbeitete Auage, Pearson Studium, München, 2003.
[C+05]
[Cla98]
V. Chandrasekhara/A. Eberhart/H. Hellbrück/S. Kraus/U. Walther:
Java 5.0. Konzepte, Grundlagen und Erweiterungen in 5.0, Hanser Verlag, Wien, 2005.
U. Claussen: Objektorientiertes Programmieren. Mit Beispielen und Übungen in C++,
2. Auage, Springer Verlag, Berlin/Heidelberg, 1998.
[CH95]
[Ebn00]
[ER98]
[HM99]
[Kan89]
[Lan00]
[Lis00]
[LS96]
C. K. M. Crutzen/H.-W. Hein: Objektorientiertes Denken als didaktische Basis der
Informatik, In: [Sch95, S. 149-158].
M. Ebner: Delphi 5. nachschlagen und verstehen, Addison-Wesley, München, 2000.
G. Ehmayer/S. Reich: Java in der Anwendungsentwicklung. Objektorientierung, Verteilung,
Datenbanken, dpunkt.verlag, Heidelberg, 1998.
T. Himstedt/K. Mätzel: Mit Python programmieren. Einführung und Anwendung
skriptorientierter Programmierung, dpunkt.verlag, Heidelberg, 1999.
I. Kant: Kritik der reinen Vernunft, Reclam, Stuttgart, 1989.
I. van Laningham: Jetzt lerne ich Python. Der schnelle Einstieg in die wunderbare Welt
der Programmierung, Markt+Technik Verlag, München, 2000.
R. Lischner: Delphi in a Nutshell. A Desktop Quick Reference, O'Reilly, Sebastopol, 2000.
A. Lüscher/A. Straubinger: Objektorientierte Technologien. Eine Einführung,
vdf Hochschulverlag, Zürich, 1996.
[LA00]
[Mös98]
Einführung in Python, O'Reilly, Köln, 2000.
H. Mössenböck: Objektorientierte Programmierung in Oberon-2, 3. Auage, Springer,
M. Lutz/D. Ascher:
Berlin/Heidelberg/New York, 1998.
[Mös01]
H. Mössenböck:
Sprechen sie Java? Eine Einführung in das systematische Programmieren,
dpunkt.verlag, Heidelberg, 2001.
[M+95]
J. R. Mühlbacher/B. Leisch/U. Kreuzeder:
Programmieren mit Oberon-2 unter Windows,
Carl Hanser Verlag, München/Wien, 1995.
[Sch95]
[Wit66]
Innovative Konzepte für die Ausbildung, Springer Verlag, Berlin, 1995.
L. Wittgenstein: Tractatus logico-philosophicus. Logisch-philosophische Abhandlung,
S. Schubert (Hrsg.):
Suhrkamp Verlag, Frankfurt a.M., 1966.
[Zep04]
[API1.4]
Objektorientierte Programmiersprachen. Einführung und Vergleich
von Java, C++, C#, Ruby, Spektrum Akademischer Verlag, München, 2004.
Java 1.4.2 API Dokumentation
K. Zeppenfeld:
http://java.sun.com/j2se/1.4.2/docs/api/index.html (31.07.2006)
[API1.5]
Java 1.5 API Dokumentation
http://java.sun.com/j2se/1.5.0/docs/api/index.html (31.07.2006)
96
Herunterladen