MezData-Logo

Datenkapselung Flaschenbeispiel

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.

KlassendiagrammSchon 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{
  Flasche dieFlasche; // ein Verweis-Attribut dieFlasche definieren
  void handelt(){    
    dieFlasche = new Flasche(); // eine Flaschen-Objekt erzeugen und die Objekt-ID speichern
    System.out.println("Leergewicht: "+dieFlasche.leergewicht+" Fuellstand: "+dieFlasche.fuellstand);
    dieFlasche.fuellstand -= 600; // das Objekt-Attribut aendern
    System.out.println("Leergewicht: "+dieFlasche.leergewicht+" Fuellstand: "+dieFlasche.fuellstand);
  }
}
public class Flasche{
  int leergewicht = 150; 
  int fuellstand = 500;
}

Nach dem Aufruf von Mensch.handelt() wird das Problem sichtbar: Negativer Wert für Füllstand!

MenschFlaschenobjekt

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

KlassendiagrammVariablen (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{
  private Flasche dieFlasche;
  public void handelt(){    
    dieFlasche = new Flasche();
    dieFlasche.zeigeInfo();
    dieFlasche.trinken(300);
    dieFlasche.zeigeInfo();
    dieFlasche.trinken(300);
  }
}
public class Flasche{
  private int leergewicht = 150; // {>=0} Zusicherung als Kommentar erinnert
  private int fuellstand  = 500; // {>=0} den Programmierer sie zu implementieren
  
  public void zeigeInfo(){
    System.out.println("Leergewicht: "+leergewicht+" Fuellstand: "+fuellstand);
  }
  public void trinken(int n){ // Zusicherungen einhalten
    if(n<0){
      System.out.println("Nicht in die Flasche spucken!");
    }
    else if(n>fuellstand){
      System.out.println("Es waren nur noch "+fuellstand+" vorhanden");
      fuellstand = 0;
    }
    else{
      fuellstand = fuellstand -n;
    }
  }
}

OOP: Konstruktoren, Überladen von Operationen

Klassendiagramm 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{
  private Flasche dieFlasche;// ->Flaschenobjekt
  public void handelt(){    
    dieFlasche = new Flasche(200,700);
    dieFlasche.zeigeInfo();
    dieFlasche.trinken(400);
    dieFlasche.zeigeInfo();
    dieFlasche.trinken(400);
  }
}
  public Flasche(){}      // Konstruktor
  public Flasche(int lgewicht, int volumen){  // Konstruktor
    if(lgewicht<0 || volumen<0){
      System.out.println("Keine Flaschen mit negativen Werten!");
    }
    else{
      leergewicht = lgewicht;
      fuellstand = volumen;
    }
  }

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

KlassendiagrammDie 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{
  //private Flasche dieFlasche;// ->Flaschenobjekt
  private VerschlussFlasche dieVerschlussFlasche;
  public void handelt(){    
    dieVerschlussFlasche = new VerschlussFlasche(200,700);
    dieVerschlussFlasche.zeigeInfo();
    dieVerschlussFlasche.trinken(400);
    dieVerschlussFlasche.zeigeInfo();
    dieVerschlussFlasche.oeffne();
    dieVerschlussFlasche.trinken(400);
    dieVerschlussFlasche.zeigeInfo();
  }
}
public class VerschlussFlasche extends Flasche{
  private boolean offen;
  public VerschlussFlasche(){}
  public VerschlussFlasche(int lgewicht, int volumen){
    super(lgewicht,volumen); // Konstruktor der Oberklasse aufrufen
  }
  public void zeigeInfo(){ // Ueberschreiben der Operation
    super.zeigeInfo();
    if(offen){
      System.out.println("Flascherl is offen");
    }
    else{
      System.out.println("Flascherl is zu");
    }
  }
  public void oeffne(){
    offen = true;
  }
  public void schliesse(){
    offen = false;
  }
  public void trinken(int n){ // Ueberschreiben der Operation
    if(offen){
      super.trinken(n);
    }
    else{
      System.out.println("Des Flascherl ist zu!");
    }
  }
}

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:

Klassendiagramm

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.. Struktogramm

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.. Struktogramm

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