Klassische Programmiersprachen: Strukturierte Datentypen ohne Schutz
Dieses Beispiel soll die Problematik verdeutlichen, die zur Einführung der Objektorientierten Programmierung und zur Kapselung der Daten (Geheimnisprinzip) geführt haben.
Schon bevor es Objektorientierte Programmierung gab, kannte man Strukurierte (Zusammengesetzte) Datentypen. Beispielsweise konnte ein zusammengesetzter Datentyp Flasche als eine Struktur definiert werden, die aus zwei ganzen Zahlen, den Komponenten leergewicht und fuellstand besteht.
Wird eine Variable dieses Datentyps erzeugt, bekommt man eine Referenz, einen Verweis auf einen Speicherbereich zurück.
Mit dieser Referenz konnten beliebige Operationen ausgeführt werden. Es gab keinen ausgeprägten Schutz vor Fehlbedienung.
Die Situation lässt sich mit Java anschaulich nachstellen.
[Mindmap zur didaktischen Analyse]
Quellcode
[MenschFlasche/Mensch.java] | [MenschFlasche/Flasche.java] |
---|---|
public class Mensch{ |
public class Flasche{ |
Nach dem Aufruf von Mensch.handelt() wird das Problem sichtbar: Negativer Wert für Füllstand!
Dem Programmierer der Operation Mensch.handelt() war es möglich, in einer Flasche einen inkonsistenten Zustand (negativer Füllstand) zu erzeugen.
Objektorientierte Programmierung: Privatisierung der Daten, Schutz durch zugeordnete Operationen
Variablen (Attribute) und Unterprogramme (Operationen, Methoden) werden grundsätzlich einer Klasse zugeordnet.
Die Attribute werden mit private (-) gekennzeichnet, dadurch können nur noch Operationen, die zur Klasse gehören, darauf zugreifen.
Operationen, die anderen Klassen zugänglich sein sollen werden mit public (+) gekennzeichnet.
In diesen Operationen können Zusicherungen z.B. {>=0} implementiert werden.
Bei auftretenden Wertefehlern kann unmittelbar eine Reaktion, z.B. eine Ausgabe auf Konsole oder das Auslösen einer Exception (Fehlernachricht) erfolgen.
Quellcode
[MenschFlasche2/Mensch.java] | [MenschFlasche2/Flasche.java] |
---|---|
public class Mensch{ |
public class Flasche{ |
OOP: Konstruktoren, Überladen von Operationen
Volumen und Leergewicht sind bislang bei der Initialisierung fest vorgegeben. Sollen beliebige Flaschengrössen erzeugt werden können, kann ein Konstruktor mit Parametern Flasche(lgewicht:GZ,volumen:GZ) dafür definiert werden.
Sollen weiter wie bisher Flaschen mit new Flasche() erzeugt werden können, muss ein weiterer Konstruktor Flasche() ohne Parameter hinzugefügt werden.
Es gibt jetzt zwei Konstruktoren mit dem selben Namen aber unterschiedlichen Parametern in einer Klasse. Die Konstruktor-Operation wird "überladen". Man spricht dabei auch von Statischer Polymorphie: Polymorphie bedeutet Vielgestaltigkeit, eine Operation hat mehrere Gestaltungen, statisch bedeutet zur Compile-Zeit kann bereits entschieden werden, welche Operation ausgeführt werden wird.
[MenschFlasche3/Mensch.java] | [MenschFlasche3/konstruktoren.txt] |
---|---|
public class Mensch{ |
public Flasche(){} // Konstruktor |
Erstellen Sie ein Projekt OOPFlasche, Implementieren den Code für die Klassen Mensch und Flasche und testen Sie ihn.
Erstellen Sie eine public Operation ermittleGewicht():GZ, die das Gewicht einer Flasche ermittelt und zurück gibt.
Lösung anzeigen..
Erstellen Sie ein Struktogramm für ermittleGewicht():GZ. Lösung..
OOP: Vererbung, Überschreiben von Operationen, protected
Die Flasche wird um einen Verschluss erweitert. Eine neue Klasse VerschlussFlasche erbt und erweitert die Eigenschaften der Klasse Flasche. Ein zusätzliches Attribut offen:Boolean gibt an, ob sie geöffnet oder geschlossen ist.
VerschlussFlasche benötigt eigene Konstruktoren und Operationen zum Öffnen und Schliessen. Die Operationen zeigeInfo() und trinken(n:GZ) müssen modifiziert werden, dabei werden die geerbten Operationen gleichen Namens überschrieben, es wird unterschiedlicher Code in Abhängigkeit der Klasse ausgeführt, zu dem ein Objekt gehört.
Die geerbten Attribute sind privat, deshalb kann in der Unterklasse nicht direkt darauf zugegriffen werden. Deshalb werden dazu die geerbten Operationen der Oberklasse mit dem Operator super verwendet.
Quellcode
[MenschFlasche4/Mensch.java] | [MenschFlasche4/VerschlussFlasche.java] |
---|---|
public class Mensch{ |
public class VerschlussFlasche extends Flasche{ |
Ergänzen Sie das Projekt um die Klasse VerschlussFlasche, testen und Inspizieren Sie das VerschlussFlaschen-Objekt nach dem Aufruf von Mensch.handelt(). Lösung..
Zugriff auf Oberklassen-Attribute: get- und set-Methoden, protected
Die Operation zeigeInfo() gibt die Information zweizeilig aus, um eine schönere Ausgabe zu erhalten, sollte ein Zugriff auf die Attribute der Oberklasse möglich sein.
Zwei Möglichkeiten zur Lösung gibt es, um aus der VerschlussFlaschenklasse den Zugriff auf die Attribute der Flaschenklasse zu ermöglichen:
Implementierung von get- und set-Operationen in Flasche Bsp:
get-Methode | set-Methode |
---|---|
public int getLeergewicht(){ return leergewicht; } |
public void setLeegewicht(int l){ if(l<0){ System.out.println("Keine negativen Werte!"); } else{ leergewicht=l; } } |
Die Attribute in Flasche protected statt private kennzeichnen.
Diskutieren Sie die Vor- und Nachteile beider Möglichkeiten.
Implementieren Sie eine Lösung mit protected.
MenschKisteFlasche-Szenarien
Das Projekt wird um eine Kiste mit 24 Flaschen erweitert:
Ein unfertiges BlueJ-Projekt ist schon vorgegeben, das vervollständigt werden soll: MenschKisteFlascheVorgab.zip
Letztlich soll diese Ausgabe bei Mensch.handelt() erzeugt werden:
Leergewicht: 150 Fuellstand: 500 Flasche ist zu Leergewicht: 150 Fuellstand: 100 Flasche ist offen XOOOOOOOOOOOOOOOOOOOOOOO Volumen: 11500 oXOOOOOOOOOOOOOOOOOOOOOO Volumen: 11200
Die Operation Kiste.getFlasche():VFlasche gibt die erste verfügbare geschlossene Flasche im Kasten zurück. Ergänzen Sie den Quellcode und erstellen Sie ein Struktogramm. Lösung..
Die Operation Kiste.nimmFlasche(f:VFlasche):Boolean nimmt die Flasche f und stellt sie an die erste freie Stelle im Kasten zurück, gibt dann true zurück. Falls es keinen Platz gibt wird false zurück gegeben. Ergänzen Sie den Quellcode und erstellen Sie ein Struktogramm.Lösung..
Die Operation Kiste.zeigeInfo() zeigt den Inhalt der Kiste und die Summe aller Fuellstände der Flaschen als Volumen auf der Konsole an, siehe Ausgabe oben. Leere Stellen in der Kiste werden mit 'X', geschlossene Flaschen mit 'O' und offene Flaschen mit 'o' dargestellt.
Wie Sie sicher beim ersten Test von Mensch.handelt() feststellen konnten wurde eine Exception ausgelöst weil eine Operation auf eine nicht vorhandene Flasche ausgeführt werden sollte. Ausserdem hat der Mensch beim erfolgreichen Zurückstellen der Flasche diese immer noch in der Hand.
Erweitern Sie die Klasse Mensch um sinnvolle Operationen, die die Realität besser nachbilden.
BlueJ-Lösung: MenschKisteFlasche.zip
Gui mit IntelliJ
Ein (unfertiges) Beispielprojekt: MenschKisteFlascheGUI.zip