2 Das Geheimnisprinzip

2.1 Kapselung
2.2 Sichtbarkeit in Delphi-Klassen
2.3 Zugriffsmethoden
2.4 Bruchrechnung

Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel


2.1 Kapselung

Strukturierte Programmierung ist der Versuch, ein Programm möglichst vollständig in einzelne Module zu zerlegen, die jeweils genau voneinander abgegrenzte Teilaufgaben erledigen sollen. Diese Module sollen ausschließlich über wohldefinierte Schnittstellen miteinander kommunizieren. Mit diesem Leitgedanken haben wir schon früher Prozeduren und Funktionen eingeführt:
Das Ziel solcher Bemühungen ist es stets, auch bei größeren Programmprojekten den Überblick über den Quelltext zu behalten. Wenn es gelingt, ein Programm in einzelne Module zu zerlegen, deren Aufgaben und Schnittstellen wohldefiniert und logisch voneinander unabhängig sind, dann kann man diese Module auch weitgehend unabhängig voneinander testen, was die Fehlersuche ungemein erleichtert. Wenn man schon von einigen Teilen des Programms sagen kann, dass sie korrekt sind, dann kann der gesuchte Fehler schon nicht mehr in diesen Teilen stecken - wobei man natürlich um einen prinzipiellen Irrtumsvorbehalt nicht herum kommt.

In diesem Zusammenhang haben wir auch den Begriff der lokalen Variablen kennengelernt: Prozeduren und Funktionen können interne, sozusagen "eigene" Variablen definieren, die nur für die Laufzeit der Prozedur oder Funktion vorhanden sind. Als Paradebeispiel kann hier die Laufvariable einer FOR-Schleife dienen, die in der Regel als lokale Variable definiert wird. Der Vorteil ist, dass solche lokale Variablen nur dort auftauchen, wo sie gebraucht werden, und außerhalb dieses Bereichs völlig unsichtbar sind. Diese Praxis entspricht dem allgemeinen Geheimnis-Prinzip:

Jedes Modul soll nur so viele Details über seine Wirkungsweise "veröffentlichen", wie zur Erfüllung seiner Aufgabe unbedingt nötig ist.
Was vor den Augen der Außenwelt verborgen werden kann, soll verborgen werden.

In Delphi ist mit dem UNIT-Konzept eine weitere Variante dieses Geheimnisprinzips verwirklicht. Eine Unit ist eine Quelltextdatei. Zunächst erzeugt Delphi stets nur eine Unit für jedes Projekt, nämlich Unit1.pas. Wir können aber ein Projekt in viele Units zerlegen. Dies ist z.B. immer dann sinnvoll, wenn in einem Programm eigene Klassen gebildet werden. Werden solche Klassen in eigene Units ausgelagert, dann werden sie transportabel: eine Unit kann auch in einem anderen Projekt eingesetzt werden. Betrachten wir als Beispiel die Auslagerung der eigenen Klasse in unserem "Bruch"-Projekt:



Aufgabe:


  1. Eine Klasse im Geschenkkarton

    Stellen Sie eine Kopie des Projektes"bruch_1" her und speichern Sie sie in einem eigenen Verzeichnis unter dem Namen "bruch_2" ab. (Wir verzichten hier zunächst auf die Ergebnisse unserer Arbeiten über "gemischte Zahlen".)

    Erzeugen Sie in dem neuen Projekt eine eigene Unit (Delphi-Menüpunkt "Datei | Neu | Neue Unit") und benennen Sie sie mit Hilfe des Menüpunktes "Speichern unter..." um in "kl_bruch.pas". Fügen Sie dann dem "USES"-Eintrag im Kopf der Unit1 den Eintrag "kl_bruch" hinzu. Verlagern Sie nun die Deklaration der Klasse TBruch aus der Unit1 in die kl_bruch-Unit, und zwar dort in den interface-Abschnitt. Verlagern Sie ebenso die Implementierung der Methoden dieser Klasse in die neue Unit, diese jedoch in den implementation-Abschnitt.

    Auch wenn Sie alles richtig gemacht haben, wird Delphi Ihnen einen Fehler melden, wenn Sie versuchen, das Projekt zu kompilieren: die Funktion IntToStr wird als unbekannt reklamiert! Um dies zu beheben, müssen Sie im interface -Abschnitt den Eintrag "uses SysUtils;" ergänzen. Bei dieser Gelegenheit können Sie auch noch den Eintrag "override;" hinter dem Destruktor Destroy ergänzen, um die lästige Compiler-Warnung über das Verbergen der ererbten Methode zu vermeiden. Die Virtualisierung von AsString hingegen ist hier (noch) nicht nötig.

    Überzeugen Sie sich nun davon, dass diese neue Variante unseres Bruch-Projekts komplett funktionsfähig ist.
    [Lösungsvorschlag]


Mit dieser Auslagerung unserer Klasse TBruch in eine eigene Unit ist außerhalb der Unit kl_bruch nur noch das sichtbar, was im interface-Teil dieser Unit explizit aufgeführt ist. Insbesondere verschwinden alle Implementierungsdetails aus dem Blickfeld jedes externen Beobachters. Das Unit-Konzept zwingt also den Programmierer zu einer "ordentlichen" Schnittstellen-Beschreibung und damit zu einer logisch klaren Strukturierung seines Programms.




2.2 Sichtbarkeit in Delphi-Klassen

So überzeugend die Umsetzung des Geheimnisprinzips im Unit-Konzept von Delphi ja auch ist, aber bei unserer Klasse TBruch gibt es da doch noch einen herben Schönheitsfehler: zumindest eine der Methoden hat eigentlich nichts in der Schnittstelle verloren. Können Sie sich denken, welche damit gemeint ist?

Wenn Sie bei den Methoden von TBruch nochmals kritisch untersuchen, welchen Nutzen sie für den anwendenden Programmierer haben, dann könnte Ihnen auffallen, dass die Funktion ggT eigentlich nur intern benutzt wird: sie wird ausschließlich dazu gebraucht, einen Bruch zu kürzen. Und der Anwender unserer Klasse braucht sie bestimmt nicht: wenn er einen Bruch kürzen will, wird er die Methode kuerze dieses Bruches aufrufen. Wie aber dieser Bruch das dann macht, ist dem anwendenden Programmierer völlig egal - was nach dem Geheimnisprinzip ein zulässiger, ja sogar ein wünschenswerter Standpunkt ist! Die Methode ggT hat damit den Charakter einer internen Hilfsfunktion, und mit solchen sollte man sein Klassen-Interface nicht belasten! Je einfacher nämlich das Interface ausfällt, um so leichter ist es, den Überblick zu bewahren.

Um dieser Forderung gerecht werden zu können, kann man die Eigenschaften und Methoden einer Klasse in verschiedene "Sichtbarkeitsbereiche" einordnen. Die wichtigsten dieser Sichtbarkeitsbereiche sind in Delphi durch die Schlüsselworte "protected" und "public" bezeichnet. Im "protected"-Abschnitt werden sollte Teile der Klassendeklaration aufgeführt, die nur intern wichtig sind, jedoch vor dem Anwender der Klasse verborgen bleiben sollen. Im "public"-Abschnitt wird die eigentliche "User-Schnittstelle" (also das Interface im engeren Sinne) aufgeführt. In unserem Beispiel sollte also die Methode ggT im protected-Bereich der TBruch-Deklaration verborgen werden.

Wenn wir aber schon beim Verbergen sind: die Datenfelder zaehler und nenner braucht der Benutzer eigentlich auch nicht zu sehen. Wenn ihn der Wert des Bruches interessiert, dann kann er die Ausgabefunktion AsString aufrufen. Ansonsten gibt es für ihn derzeit noch keinen Anlass, sich für die "internen Parameter eines Bruches" zu interessieren. Lediglich beim Erzeugen eines Bruches muss er Initialisierungswerte übergeben, aber ab dann "kümmert sich der Bruch selber um seinen Zähler und seinen Nenner".

Das Verbergen dieser Datenfelder vor den Augen des Anwenders hat noch einen angenehmen Nebeneffekt: nun können die darin abgelegten Werte nicht mehr von außen geändert werden! Bei unserem einfachen Objekt TBruch wäre das noch nicht so gefährlich, aber bei komplexeren Objekten ist es wichtig, dass deren Eigenschaften nicht willkürlich von außen manipuliert werden können. Um sicher zu stellen, dass das Objekt stets in einem konsistenten Zustand bleibt, verbirgt man die Datenfelder der Eigenschaften daher zumeist, indem man sie als "protected" deklariert.

Wenn wir dies in unserer Klasse TBruch umsetzen, erhalten wir die folgende Deklaration:
       TBruch = class(TObject)
                  protected
                    zaehler : Integer;
                    nenner  : Integer;
                    function   ggT(a, b: Integer): Integer;
                  public
                    constructor Create(i_zaehler, i_nenner: Integer);
                    destructor Destroy; override;
                    function   AsString: String;
                    procedure  Kuerze;
                end;
Ein TBruch-Objekt kennt natürlich auch diejenigen seiner Bestandteile, die als "protected" deklariert sind: so kann z.B. in der Implementierung der Methode kuerze die "geschützte" Funktion ggT aufgerufen werden. Hingegen sind von der Unit1 aus nur noch diejenigen Anteile der TBruch-Objekte sichtbar, die als "public" deklariert wurden: wenn wir dort versuchen, br1.ggT aufzurufen, dann wird der Compiler einen Fehler melden, nämlich:
       Undefinierter Bezeichner: 'ggT'
Als "protected" deklarierte Teile einer Klassendefinition sind nur innerhalb der Klasse selbst sowie innerhalb aller davon abgeleiteten Klassen sichtbar; hingegen können als "public" deklarierte Teile von allen Stellen des Programmtextes gesehen werden, von denen aus die Klasse bzw. eine Instanz der Klasse sichtbar ist.



Aufgabe:


  1. Interaktive Bruch-Erzeugung

    Stellen Sie eine Kopie des Projektes"bruch_2" her und speichern Sie sie in einem eigenen Verzeichnis unter dem Namen "bruch_3" ab. Ergänzen Sie dann die Deklaration von TBruch durch "protected" und "public". Testen Sie die neue Version.

    Erweitern Sie das Programm um eine weitere Instanz von TBruch mit Namen br2, die ebenfalls in der OnCreate-Methode des Formulars initialisiert wird. Vergessen Sie auch nicht, br2 in der OnClose-Methode des Formulars wieder zu entsorgen!

    Fügen Sie 4 IntEdit-Felder und 2 Knöpfe hinzu, die die Eingabe von Zähler und Nenner der beiden Brüche ermöglichen. Die Klick-Prozeduren der Knöpfe sollen dabei die aktuelle Instanz, die durch br1 bzw. br2 referenziert wird, löschen und dann eine neue mit den Daten aus den entsprechenden IntEdit-Feldern erzeugen. Erweitern Sie die Knöpfe "Kürzen" und "Ausgabe" so, dass sie nun stets beide Brüche bearbeiten.

    Testen Sie das Programm! Dabei sollte Ihnen auffallen, dass unser Programm nur mit solchen Brüchen umgehen kann, bei weder Zähler noch Nenner negativ sind. Ansonsten versagt die Prozedur kuerze! Suchen Sie den genauen Fehlerort und reparieren Sie den Schaden!
    [Lösungsvorschlag]





2.3 Zugriffsmethoden

Das Programm aus der letzten Aufgabe ist einer Hinsicht ein wenig unbefriedigend: wenn man den "Kürze"-Button klickt, passiert auf der Oberfläche nichts. Zwar können wir uns davon überzeugen, dass intern tatsächlich beide Brüche gekürzt wurden, aber der Benutzer sollte dies doch auch äußerlich irgendwie zu sehen bekommen.

Der naheliegende Versuch, nach dem Kürzen eben beide Brüche auch gleich auszugeben, führt nur zu neuen Irritationen. Jetzt haben wir zwei verschiedene Repräsentationen der beiden Brüche in unserem Programmfenster, die uns unterschiedliche Informationen anbieten: im Ausgabefeld stehen die gekürzten Brüche (in Textform), während in den IntEdit-Felder die zuvor eingegebenen ungekürzten Werte von Zähler und Nenner beider Brüche stehen.

Schön wäre es, wenn wir nun auf die Textausgabe ganz verzichten könnten, und stattdessen den Wert des Bruches mit Hilfe der zugehörigen IntEdit-Felder darstellen könnten. Dazu müssten wir aber die Werte von Zähler und Nenner aus den Bruchobjekten auslesen können - aber die entsprechenden Eigenschaftsvariablen haben wir doch kürzlich erst extra versteckt!

Die Lösung bieten sogenannte Zugriffsmethoden: dies sind Methoden, mit denen man die Werte von Eigenschaften auslesen bzw. setzen kann. Für unsere Zwecke genügt es zunächst, dass wir die Werte von Zähler und Nenner auslesen können. Dazu erweitern wir TBruch um zwei Funktionen "GetZaehler" und "GetNenner", die die aktuellen Werte von zaehler bzw. nenner zurückliefern. Dass diese Funktionen in dem public-Abschnitt der Deklaration gehören, sollte Ihnen schon klar sein.



Aufgabe:


  1. Automatische Ausgabe

    Stellen Sie eine Kopie des Projektes"bruch_3" her und speichern Sie sie in einem eigenen Verzeichnis unter dem Namen "bruch_4" ab. Erweitern Sie dann die Deklaration von TBruch um die beiden Zugriffsfunktionen "GetZaehler" und "GetNenner". Sorgen Sie dafür, dass nach dem Kürzen die neuen Werte für Zähler und Nenner beider Brüche in den entsprechenden IntEdit-Feldern angezeigt werden, indem Sie die Klick-Prozedur dieses Knopfes geeignet erweitern. Damit werden diejenigen Komponenten, die bisher der Ausgabe dienten, überflüssig; entfernen Sie sie! Testen Sie die neue Version.
    [Lösungsvorschlag]




So weit, so gut. Aber unsere Brüche sind bei Licht besehen noch ziemlich dumm: sie verfügen über keinerlei Kooperationsmöglichkeiten, es sind keine Verknüpfungen möglich, die Bruchrechnung ist bisher nicht mehr als eine kühne Vision. Also an die Arbeit! Wir wollen als erstes die Aufgabe in Angriff nehmen, in unserem Programm zwei Brüche miteinander zu multiplizieren und das Ergebnis anzeigen zu lassen - vollständig gekürzt natürlich!

Als erstes brauchen wir einen dritten Bruch, der das Ergebnis aufnehmen soll. Das ist einfach: wir deklarieren eine Variable br3 in unserem Formular und initialisieren sie mit dem Wert 0/1. Aber dann beginnen die Probleme wieder: Wie soll diese Instanz von TBruch nun eigentlich einen neuen Zähler und einen neuen Nenner zugewiesen bekommen, um das Ergebnis der Multiplikation der Brüche br1 und br2 darstellen zu können? Was wir hier brauchen, ist ein schreibender Zugriff auf die Eigenschaftsvariablen zaehler und nenner.

Üblicherweise löst man das Problem ebenfalls durch entsprechende Zugriffsmethoden. Im vorliegenden Fall könnte man dem Interface von TBruch z.B. zwei Prozeduren "SetZaehler(neu_z: Integer)" und "SetNenner(neu_n: Integer)" hinzufügen, die nichts anderes tun, als die übergebenen Werte in die entsprechenden Eigenschafts-Variablen zu übertragen. Hat sich dann aber der ganze Aufwand des Verbergens der Eigenschaftsvariablen überhaupt gelohnt? Effektiv könnte dann doch "jeder" wieder von außen auf diese Felder zugreifen, und zwar sowohl lesend (über "GetZaehler" und "GetNenner") als auch schreibend (über "SetZaehler" und "SetNenner"). Es liegt auf der Hand, dass das Ganze dann eher ein Schildbürgerstreich gewesen wäre.

In vielen Fällen geht man trotzdem diesen zunächst etwas beschwerlich erscheinenden Weg über die Zugriffsfunktionen, weil sich nämlich in den Methoden für den schreibenden Zugriff noch eventuelle Zusatzarbeiten unterbringen lassen, die garantieren, dass das Objekt stets in einem konsistenten Zustand bleibt. Bisher ist so etwas für unser TBruch-Objekte noch nicht ersichtlich, aber ein genauerer Blick zeigt, dass solche Überlegungen auch hier durchaus sinnvoll sein können. Wir wollen von den Zugriffsmethoden nämlich verlangen, dass sie stets die folgenden Randbedingungen für "sinnvolle Bruch-Objekte" einhalten:
  1. Der Nenner eines Bruches soll stets ungleich Null sein.
  2. Das Vorzeichen eines Bruches soll stets im Zähler verwaltet werden.
Genau genommen läuft das also auf die Forderung hinaus, dass der Nenner stets positiv sein soll. Da es damit denkbar wird, dass die Zugriffsmethoden ein Minuszeichen aus dem Nenner in den Zähler verlagern müssen, ist es sinnvoll, die beiden oben vorgeschlagenen "Set...-Prozeduren" zu einer einzigen Zugriffsmethode zusammenzufassen, die dann zwei Argumente hat und bei jedem Aufruf sowohl zaehler als auch nenner setzt. Damit setzt sie also den Bruch insgesamt auf einen neuen Wert, weshalb wir ihr den Namen "Reset" geben wollen.



Aufgabe:


  1. Einem Bruch einen neuen Wert zuweisen

    Stellen Sie eine Kopie des Projektes"bruch_4" her und speichern Sie sie in einem eigenen Verzeichnis unter dem Namen "bruch_5" ab. Erweitern Sie dann die Deklaration von TBruch um die Methode Reset(n_zae, n_nen: Integer), die die Eigenschaftsvariablen zaehler und nenner gemäß den Daten in n_zae und n_nen setzt und dabei aber die obigen Konventionen beachtet, also sicherstellt, dass der Nenner immer echt positiv bleibt.

    Mit Hilfe der Methode Reset können Sie nun die Klick-Prozeduren der beiden Knöpfe zum Setzen der Werte von br1 und br2 vereinfachen: es ist nun nicht mehr nötig, diese Objekte stets zu entsorgen, um sie dann in der nächsten Prgrammzeile wieder neu zu erzeugen! Stattdessen können wir ihnen mit Reset nun einfach einen neuen (Bruch-)Wert zuweisen! Prüfen Sie auch den Fall, dass der Benutzer für einen Bruch einen positiven Zähler und einen negativen Nenner eingibt, oder gar einen negativen Zähler und einen negativen Nenner!

    Fügen Sie eine dritte TBruch-Variable br3 (initialisiert mit "0/1"), zwei weitere IntEdit-Felder für deren Zähler und Nenner und einen Knopf mit Aufschrift "Klonen" hinzu. Die Klick-Prozedur dieses Knopfes soll br3 den Wert von br1 übernehmen lassen und die IntEdit-Felder, die die Daten von br3 enthalten, aktualisieren.
    [Lösungsvorschlag]




2.4 Bruchrechnung


Eigentlich wollten wir ja zwei Brüche miteinander multiplizieren, aber schon die Zuweisung eines neuen Wertes an ein TBruch-Objekt erwies sich als recht schwierig. Nachdem nun diese Probleme aber bewältigt sind, können wir uns wieder unserem ursprünglichen Ziel zuwenden.

Um die Brüche br1 und br2 miteinander zu multiplizieren und das Ergebnis in br3 abzulegen, ist folgendes Vorgehen sinnvoll:
  1. Der Wert von br1 wird nach br3 kopiert.
  2. br3 "multipliziert sich" mit br2.
Den ersten Teil der Aufgabe haben wir schon im vorigen Abschnitt geschafft. Für den zweiten fehlt uns noch eine passende Methode, die diese Aufgabe erledigt. Nennen wir sie "MultipliziertSichMit" und übergeben wir als Parameter das TBruch-Objekt, das den Multiplikator enthält. Die Deklaration lautet dann also:
       procedure MultipliziertSichMit(b: TBruch);
Schreibfaule werden natürlich Anstoß nehmen an diesem Bezeichner-Bandwurm, aber wenn man sich darauf einlässt, kommt man zu so selbsterklärenden Programmzeilen wie
       br3.MultipliziertSichMit(br2);
- und klarer kann man ja kaum noch sagen, was da eigentlich passiert! Es ist erstaunlich, welch "sprechenden Programmtext" man mit der Notation der objekt-orientierten Programmierung oft erhalten kann, wenn man sich ein wenig Mühe gibt bei der Wahl der Bezeichner. Der Aufwand lohnt sich: wo es klappt, kommt man dem Ideal des "sich selbst dokumentierenden Quelltextes" ein deutliches Stück näher!

Nun fehlt nur noch die Implementierung dieser Methode. Dazu machen wir uns zuerst einmal klar, wie eigentlich zwei Brüche miteinander multipliziert werden: der Zähler des Produkts ergibt sich als das Produkt der Zähler der einzelnen Brüche, der Nenner des Produkts ist das Produkt der einzelnen Nennern. (Bitte beachten Sie, dass für die Addition keine analoge Regel gilt ;-))
Damit erhält man zum Beispiel die folgende Implementierung:
       procedure TBruch.MultipliziertSichMit(b: TBruch);
         begin
         zaehler := zaehler * b.GetZaehler;
         nenner  := nenner  * b.GetNenner;
         end;
Nun hat unsere TBruch-Klasse also gelernt, wie eine Multiplikation unter Brüchen vor sich geht. Analog können Sie die anderen Grundrechenarten implementieren.



Aufgabe:


  1. Was Sie schon immer können sollten!

    Stellen Sie eine Kopie des Projektes"bruch_5" her und speichern Sie sie in einem eigenen Verzeichnis unter dem Namen "bruch_6" ab. Erweitern Sie dann die Deklaration von TBruch um die oben beschriebene Methode MultipliziertSichMit, die die Multiplikation zweier TBruch-Objekte ermöglicht.

    Verändern Sie die Aufschrift auf dem Knopf "Klonen" in "Berechnen", und sorgen Sie dafür, dass die zugehörige Klick-Prozedur nun in br3 das Produkt von br1 und br2 erzeugt. Das Ergebnis soll dann auch gleich wieder in den entsprechenden IntEdit-Feldern angezeigt werden. Testen Sie Ihr Programm!

    Fügen Sie entsprechende Erweiterungen für die Addition ("VergroessertSichUm"), die Subtraktion ("VerkleinertSichUm") und die Division ("DividiertSichDurch") hinzu. Die Auswahl zwischen diesen Rechenarten können Sie mit einer "RadioGroup" treffen. Wie diese zu benutzen ist, finden Sie in der Delphi-Hilfe!
    [Lösungsvorschlag]






Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel