is-Logo Objektorientierte Programmierung (OOP)
Implementierung

S. Spolwig


[Home | Programmiersprachen | Delphi]
 

Hinweise zur Implementation in OOP mit DELPHI

  1. Attribute
  2. Methoden
  3. GUI - Fenster
  4. Vererbung
  5. Aggregation
  6. Assoziation
    a) gerichtete Assoziation
    b) bidirektionale Assoziation
  7. Einfache Nutzung von Serverobjekten

Verschiedene Sprachen - verschiedene Möglichkeiten

Sprachen, die einen objektorientierten Entwurf umzusetzen vermögen, warten mit unterschiedlichen Konzepten (objektbasiert, klassenbasiert, objektorientiert) und Möglichkeiten dafür auf. So wird beispielsweise die Mehrfachvererbung nicht von allen unterstützt. C++ hat bietet die Möglichkeit, echte Aggregationen durch Variablensemantik zu realisieren und einfache  Aggregationen mit Zeigersemantik. Java hat kein spezielles Konzept dafür; Assoziationen und Aggregationen sehen daher ziemlich gleich aus, was dazu führt, dass zumindest ein namhafter Autor (Balzert, 1999) in einem neuen Lehrbuch auf die Aggregation völlig verzichtet. Delphi erlaubt Referenzen im Datenraum der Unit, also auch außerhalb des Klassenkonstruktes.
Ein objektorientierter Entwurf sollte angemessen in die Programmsprache übersetzt werden. Dabei dient die Spezifikation der Klassen und ihrer Methoden sozusagen als Programmieranweisung, die so präzise formuliert sein muss, dass sie als ‚Lohnauftrag nach Indien‘ geschickt werden kann.

Im folgenden wird auf  spezielle Probleme bei der Implementierung mit DELPHI eingegangen.

1.  Attribute

Die Attribute eines Objekts müssen natürlich veränderbar sein, denn sie stellen den jeweiligen Zustand eines Objekts dar.

TPerson = class (TObject)
           private
             Name,
             Vorname : string[15];
           public
             constructor Create; virtual;
             procedure SetName (n: string); virtual;
             ...
          end;

Die Attribute der Klasse werden i. d. R. als private deklariert. Damit wird erreicht, dassdie Attributwerte von außen, also von anderen Objekten, nicht direkt manipuliert werden können. Das dient der Erhöhung der Softwaresicherheit und Qualität. Wünschenswert ist sogar, dass die Attribute gar nicht bekannt bzw. einsehbar wären (Geheimnisprinzip), um jeden verbotenen Zugriff zu verhindern. Der Zugriff auf die Attribute darf daher nur durch die öffentlichen Methoden (public) gestattet sein. Die Methoden schützen damit die Attribute (Datenkapselung). Aus diesem Grunde bilden Attribute und Methoden, d. h. die erlaubten Operationen in einer Klasse, eine Einheit.

2.  Methoden

Jede Klasse verfügt über eine Reihe von Standardmethoden, die immer gleich sind. Sie werden entweder in der Klasse selbst deklariert oder können aus einer Oberklasse geerbt werden. Diese Methoden sind i. d. R. public. Andere, die nur internen Hilfszwecken dienen werden unter private deklariert.

 

3.  Hinweise zum GUI

Das GUI-Fenster heißt standardmäßig FensterFrm, die Unit uFenster (natürlich nur, wenn es nur ein Fenster gibt).

Im GUI-Fenster finden wir zwei Arten von Methoden: die Ereignisprozeduren, die vom System selbst angelegt und vom Programmierer durch Doppelklick erzeugt werden. Sie sind am Parameter (Sender : TObject ) zu  erkennen. Daneben gibt es häufig Prozeduren, die Teile des Ablaufs der Anwendung betreffen. Aus Gründen der Klarheit der Softwarearchitektur sollte man sie auch getrennt einsetzen.

Ein Beispiel:

unter public deklariert

procedure TFensterFrm.OkBtnClick (Sender: TObject);  // Die Ereignismethode      
(* -------------------------------------------- *)
begin
  NeuenPatientenErfassen;
end;

unter private deklariert

procedure TFenster.NeuenPatientenErfassen;          // Private Anwendungsmethode
(* --------------------------------------- *)  
var NeuerPatient : TPatient;

begin
  if NameEdt.Text <> ''
  then
    begin
      NeuerPatient := TPatient.Create;
      Datenaktualisieren (NeuerPatient);
      Patientenliste.Append (NeuerPatient);
   end;
end;

Die Ereignismethode OKBtnClick ruft nur die Anwendungsprozedur auf. Damit wird deutlich, dass GUI-Komponenten nur reine Aufruffunktionen erledigen, also nur Ereignisse anstoßen, eine Nachricht abschicken und sich nicht um interne Abläufe des Datenmodells kümmern.

 

4. Vererbung

Wenn eine Oberklasse ihre Attribute unter private deklariert hat, dann sind sie eben auch nicht für die abgeleiteten Unterklassen sichtbar. Da sie aber natürlich dort gebraucht werden, sieht DELPHI dafür  die Anweisung protected vor.

Wenn ein Exemplar einer Unterklasse erzeugt wird, muss man dafür sorgen, dass das Exemplar, das aus der Oberklasse geerbt wird, ebenfalls mit erzeugt wird. (Sonst gibt es einen Absturz.) Für ererbte Teile gibt es die Anweisung inherited.

TPatient = class (TPerson)
            private
              Krankenkasse: string[25];
              ...
            public
             constructor Create; override; // weil der geerbte überschrieben wird
             ...
           end;

....

constructor TPatient.Create;
(* -------------------------------------------------------------------- *)
begin
  inherited Create; // die Person erzeugen, die man erbt
  Init;
end;  

 

5. Aggregation

Für die Aggregation gilt:

  • Komponenten sind vom Aggregat existenzabhängig
    – Erzeugung des Aggregats erzeugt auch die Komponenten
    – Löschung des Aggregats löscht auch die Komponenten
     

  • Die echte Aggregation (Komposition) übernimmt die dynamische Weiterleitung von Botschaften an die Komponenten  z. B. Aggregat.Zeigen stößt Komponente.Zeigen an.

Deshalb muss in der Create-Methode auch die Komponente Adresse erzeugt werden.

TPerson = class (TObject)
            Adresse : TAdresse;
            Name    : string[15];
            ...
           public
            constructor Create; virtual;
            procedure Init; virtual;
            procedure SetName (n: string); virtual;
           ...
          end;   

constructor TPerson.Create;
(* ----------------------------------------------- *)
begin
  inherited Create;            // erst mal die geerbte Person erzeugen
  Adresse := TAdresse.Create;  // dann die eingefügte Adresse
  Init;
end;

 

und wenn Person initialisiert wird, muss auch automatisch hierbei die Adresse initialisiert werden.

procedure TPerson.Init;
(* ----------------------------------------------- *)
begin
  Name := '';
  Vorname := '';
  Geburtstag := '';
  Adresse.Init;
end;

 

6. Assoziation

Für die Assoziation gilt:

  • Zwei völlig von einander unabhängige Objekte (Exemplare), die auf der gleichen Abstraktionsebene stehen, werden unter einem bestimmten Gesichtpunkt mit einander verknüpft.

 
Während in der OOA vorzugsweise zunächst nur ungerichtete Assoziationen gezeichnet werden sollten,

 

 

kann im Entwurf eine gerichtete Assoziation bereits Hinweise auf die Richtung des Zugriffs und die Art der Implementation geben.

Beispiel: Dem Kunden wird ein Kundenbetreuer zugeordnet.

a) gerichtete Assoziation

Dazu ist es erforderlich, dass in der Klientenklasse

  1. ein Referenzattribut auf das Serverobjekt vereinbart wird

  2. Methoden eingeführt  werden, die die Beziehungen aufbauen und wieder entfernen können.

TKunde = class (TObject)
           private
            Nummer  : integer;
            Name    : string[15];
            Betreuer: TBetreuer;                  // Referenzattribut
           public
            constructor Create;
            procedure SetLink (betr : TBetreuer); // Verbindung herstellen
            function  GetLink : TBetreuer;        // Ref. holen
            procedure RemoveLink;                 // Ref. löschen
            procedure SetNummer (n: integer);
            ...
          end;

 

Die Set-/GetMethoden werden wie üblich implementiert; RemoveLink so:

procedure RemoveLink;
(* -------------------------------------------------------------------- *)
begin
  Betreuer:= NIL;
end;

 

Bis hierher sind das alles nur die technischen Vorbereitungen (Deklaration). Die Verknüpfung selbst wird in einer operativen Einheit (z. B. GUI) bei Bedarf hergestellt

TFensterFrm = class(TForm)
                SteuerPnl : TPanel;
                ...
               private
                Kunde   : TKunde;    // Referenzattribute auf die Objekte
                Betreuer: TBetreuer;
                procedure Init;
                ..
              end;
IMPLEMENTATION
...
procedure TFensterFrm.ErfassenBtnClick(Sender: TObject);
(* --------------------------------------------------------------- *)
begin
  Kunde    := TKunde.Create;    // Clientobjekt erzeugen
  Betreuer := TBetreuer.Create; // Serverobjekt erzeugen
  PersDatenAktualisieren;       // Daten aus dem Fenster übernehmen
  BetreuerDatenAktualisieren;   // "" 

  Kunde.SetLink(Betreuer);      // Assoziation herstellen

  Kundenliste.Append (Kunde);   // komplett in die Liste übernehmen
end;

 

Das Lesen und Anzeigen des dazugehörigen Betreuers läuft wieder über die Referenz in Kunde:

procedure TFensterFrm.ErsterBtnClick(Sender: TObject);
(* ------------------------------------------------------------------- *)
begin
  Kundenliste.First;
  Kunde := Kundenliste.GetElement;
  KundenMaskeAktualisieren;

  Betreuer:= Kunde.GetLink;   // Referenz auf das Serverobjekt holen

  BetreuerMaskeAktualisieren;  // und jetzt kann es angezeigt werden
end;

 

b) bidirektionale Assoziation

Dieser Abschnitt ist bisher aus guten Gründen nicht veröffentlicht worden, soll aber wegen mehrfacher Nachfragen angefügt werden.

In leicht lesbaren Einführungen heißt es oft, in der OOP treten die Objekte mit einander in Beziehung und tauschen Nachrichten aus. Das suggeriert, daß die Objekte scheinbar beliebig in Kontakt treten können und sich gegenseitig kennen. Wäre dem so, dann könnte man in einem Programm unterschiedliche Objekte einfach aufeinander loslassen und die erledigen dann irgendwas. Eine derartige Programmstruktur könnte man (temporär) netzartig nennen. Leider ist das nicht so einfach.

Wenn zwei Objekte sich gegenseitig ansprechen können, nennt man das eine bidirektionale Assoziation. In Programmstrukturen, die hierarchisch aufgebaut sind und daher relativ sicher und übersichtlich, vermeidet man nach Möglichkeit bidirektionale Assoziationen. Wenn in einem Entwurf derartige Situationen auftreten, deutet es oft darauf hin, daß der Entwurf in der OOA nicht sauber aufgelöst ist.

Sollte aus sehr guten Gründen dann doch einmal eine derartige Beziehung implementiert werden, bietet Delphi nur eine eingeschränkte Möglichkeit an. Den Import der gegenseitigen Units im Interface-Teil wie üblich quittiert Delphi mit der Fehlermeldung 'Überkreuzender Bezug zweier Units ..' und bietet dafür den Import im Implementation-Teil an.
 
Beispiel:

Eine Firma stellt den Mitarbeitern einen Firmenwagen. Beim Mitarbeiter soll der KFZ-Typ gespeichert werden und beim KFZ der Mitarbeiter. Man soll also fragen können: Welches Auto hat Müller? Wer fährt den BMW?

  

   Dieses Beispiel soll nur zur Demonstration einer möglichen Implementierung dienen, keinesfalls aber als Vorlage für einen Entwurf!

 

In uMitarbeiter ist die Vorgehensweise gezeigt. Um den Linkaufbau variabel zu halten, soll der Parameter vom Typ TObject sein. Der Operator IS führt eine dynamische Typprüfung durch und AS die erforderliche Typumwandlung zur Laufzeit.

procedure TMitarbeiter.SetLink (obj : TObject);
(* -------------------------------------------------------------------- *)
begin
  if obj IS TAuto    // Typprüfung
  then ...
end;

Die Klasse TAuto wird analog implementiert.

Die GUI-KLasse bietet  keine besonderen Probleme. Es können ein oder auch mehrere Objekte assoziiert werden.
 

UNIT uMitarbeiter;
(* ******************************************************************** *)
(* K L A S S E : TMitarbeiter                                           *)
(* -------------------------------------------------------------------- *)
(* Version     : 0.9                                                    *)
(* Autor       : S. Spolwig, OSZ-Handel I, 10997 Berlin                 *)
(* Aufgabe     : bildet das Objekt Mitarbeiter ab,                      *)
(*               speichert aktuellen Firmenwagen                        *)
(* Compiler    : DELPHI 6.0                                             *)
(* Aenderung   : V. 1.1  30-DEZ-06                                      *)
(* ******************************************************************** *)

INTERFACE
(* ========================== Export ================================== *)
type
   TMitarbeiter = class (TObject)
    private
     Name,
     Vorname  : string[15];
     AutoID   : string[10];   // Firmenwagen
                
    public
     constructor Create;
     procedure Init;
     procedure SetName (n: string);
     procedure SetVorName (vn: string);
     function  GetName : string;
     function  GetVorName : string;
     function  GetAutoID : string;

     procedure SetLink (obj : TObject);
     function  GetLink : TObject;
     procedure RemoveLink;
   end; (* TMitarbeiter *)

(* -------------------- B e s c h r e i b u n g -------------------------

Oberklasse     : -
Bezugsklassen  : TAuto

Methoden
--------
Create
  Auftrag: Mitarbeiter erzeugen und Initialisieren
  vorher : -
  nachher: Mitarbeiter ist erzeugt

Init
  Auftrag: Mitarbeiter Initialisieren
  vorher : ist erzeugt
  nachher: Name ist 'Müller'; andere Attribute sind leer.

Set...
  Auftrag: Attribut schreiben
  vorher : Mitarbeiter ist vorhanden.
  nachher: Attribut ist gesetzt

Get...
  Anfrage: Attribut aus dem Objekt lesen
  vorher : Mitarbeiter ist vorhanden.
  nachher: -

SetLink (obj : TObject);
  Auftrag: Link mit externem Objekt herstellen
  vorher : obj ist vorhanden.
  nachher: Linkobjekt erzeugt; ev. Zugriffe

GetLink : TObject;
  Anfrage: nach Linkobjekt
  vorher : Linkobjekt ist vorhanden.
  nachher: -

RemoveLink
  Auftrag: Link mit externem Objekt unterbrechen
  vorher : obj ist vorhanden.
  nachher: Linkobjekt ist NIL
----------------------------------------------------------------------- *)

IMPLEMENTATION
(* ==================================================================== *)
uses uAuto;           // importiert Bezugsklasse 
var  Link : TAuto;    // Linkobjekt; Mitarbeiter kennt Auto

constructor TMitarbeiter.Create;
(* -------------------------------------------------------------------- *)
begin
  inherited Create;
  Init;
end;

procedure TMitarbeiter.Init;
(* -------------------------------------------------------------------- *)
begin
  Name       := 'Müller';
  Vorname    := '';
  AutoID     := '';
end;

procedure TMitarbeiter.SetName (n: string);
(* -------------------------------------------------------------------- *)
begin
  Name := n;
end;

procedure TMitarbeiter.SetVorName (vn: string);
(* -------------------------------------------------------------------- *)
begin
  VorName := vn;
end;

function TMitarbeiter.GetName : string;
(* -------------------------------------------------------------------- *)
begin
  result := Name;
end;

function TMitarbeiter.GetVorName : string;
(* -------------------------------------------------------------------- *)
begin
  result := Vorname;
end;

function TMitarbeiter.GetAutoID: string;
(* -------------------------------------------------------------------- *)
begin
  result := AutoID;
end;

(* -------------------------------------------------------------------- *)
(* --------------------- Der Link auf Auto   -------------------------- *)
(* -------------------------------------------------------------------- *)

procedure TMitarbeiter.SetLink (obj : TObject);
(* -------------------------------------------------------------------- *)
begin
  if obj is TAuto           // Typprüfung
  then
    begin
      Link := obj AS TAuto; // Typwandlung
      AutoID := Link.GetID; // Zuweisung aus der Assoziation 
    end;
end;

function TMitarbeiter.GetLink : TObject;
(* -------------------------------------------------------------------- *)
begin
  Result := Link;
end;

procedure TMitarbeiter.RemoveLink;
(* -------------------------------------------------------------------- *)
begin
  Link := NIL;
end;

END.
 
unit uFenster;
// ***********************************************************************
// K L A S S E : TFensterFrm
// -----------------------------------------------------------------------
// Version     : 0.9
// Autor       : S. Spolwig, OSZ-Handel I,  10997 Berlin
// Aufgabe     : GUI fuer Demo mit bidirektionaler Assoziation 
//               verschiedener Objekte: Person - Firmenwagen
//               
//               Welches Auto hat Müller ?
//               Wer fährt den BMW ?
// Compiler    : DELPHI 6
// Aenderung   : V. 0.9	- 30-DEZ-06
// ***********************************************************************

interface
// =======================================================================
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls,
  uAuto,
  uMitarbeiter;

type
  TForm1 = class(TForm)
    AutoBtn   : TButton;
    PersonBtn : TButton;
    PersonPnl : TPanel;
    AutoPnl   : TPanel;
    procedure FormCreate(Sender: TObject);
    procedure PersonBtnClick(Sender: TObject);
    procedure AutoBtnClick(Sender: TObject);
  end;

var
  Form1       : TForm1;
  Mitarbeiter : TMitarbeiter;
  Auto,
  pkw         : TAuto;

implementation
// =======================================================================
{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
(* -------------------------------------------------------------------- *)
begin
  Mitarbeiter := TMitarbeiter.Create;
  Auto        := TAuto.Create;
  //  pkw         := TAuto.Create;          // geht auch mit mehreren
end;

procedure TForm1.AutoBtnClick(Sender: TObject);
(* -------------------------------------------------------------------- *)
begin
  Auto.SetLink(Mitarbeiter);               // Auto ----> Mitarbeiter
  PersonPnl.Caption := Auto.GetBesitzer;   // Auto hat jetzt den Namen
end;

procedure TForm1.PersonBtnClick(Sender: TObject);
(* -------------------------------------------------------------------- *)
begin
  Mitarbeiter.SetLink(Auto);               // Mitarbeiter ---> Auto
  AutoPnl.Caption := Mitarbeiter.GetAutoID;// Mitarbeiter hat jetzt AutoID
end;

END.
 

 

7.  Die einfache Nutzung von Serverobjekten

Die Beziehungen zwischen Klassen sollen im Normalfall auch innerhalb der Klasse implementiert sein. DELPHI und auch andere Sprachen verfügen auf Grund ihres Konzepts über einen Datenraum der die gesamte Unit umfasst, also weitergeht als das darin befindliche Klassenkonstrukt.

Insbesondere bei Hauptprogrammen und Objekten mit Steuerungsfunktion (GUI-Fenster) bei denen ein anderes unabhängiges Objekt der Fachklasse nicht als Komponentenobjekt (Attribut) eingebunden werden soll, bietet sich eine einfache Nutzung des Serverobjekts als Vereinbarung an, hier z. B. wenn das Fenster geschlossen wird, der Kunde jedoch weiterleben soll, um in einem neuen anderen Fenster dargestellt zu werden.

                                          

UNIT uSteuerung;
INTERFACE
Uses uKunde;  // import: TKunde

type TSteuerFrm = class(TForm)
                     SteuerPnl : TPanel;                   
                     NeuBtn    : TButton;
                     ...
                  end;

var AktuellerKunde : TKunde;           // Referenz mit Objektnamen vereinbaren

IMPLEMENTATION

procedure TSteuerFrm.OkBtnClick(Sender: TObject);
(* ------------------------------------------------------------------- *)
begin
  DatenAktualisieren(AktuellerKunde);  // Referenzieren und
end;                                   // Daten aus der Eingabemaske uebernehmen

Hier ist die Vereinbarung der Referenzierung im Deklarations-/Interface-Teil vorgenommen. Ebenso könnte die Referenzierung bzw. die Instanzierung im Implementation-Teil gemacht werden, wenn es angebracht scheint. Damit läge dann die Nutzung versteckt im privaten Teil und ist nach außen nicht sichtbar.

In besonderen Situationen könnte das Serverobjekt auch direkt in seiner eigenen Klasse vereinbart sein, erzeugt und exportiert werden. Dann lässt es sich durch einfachen Import nutzen. Jedoch stellt diese Variante den Export einer globalen Variablen dar mit allen damit verbundenen Nachteilen.



 

©    04. Oktober 2008    Siegfried Spolwig

Seitenanfang