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