Claus Schönleber

What the heck are Objects? - Part I

Objektorientierte Programmierung, Basics - oder: OOP in 120 Minuten
Version 0.2 (11/2005)
  Work | Home ]

Inhalt

  • Der Begriff "Objekt"
  • Wozu?
  • Das erste Objekt
  • Kapselung
  • Methoden

Die Beispiele sind in C++. In einem späteren Text mag es dann auch Java-Beispiele geben.

Vorkenntnisse:

Es sollten gute Kenntnisse in einer prozeduralen Programmiersprache (z.B. C oder Pascal) vorhanden sein.

 

Objekte?

Ein Begriff aus den 80er Jahren ist inzwischen in aller Munde und in vielen Quelltexten, manchmal korrekt, manchmal vermurkst: Objektorientierte Programmierung.

Natürlich hört man, wenn man die Prinzipien jemandem erklärt, oft die Bermerkung: "Wozu das? Braucht man doch gar nicht! Ging und geht doch auch so!"

Ja, stimmt. Aber mit derselben Begründung könnte man noch in Maschinensprache programmieren, oder, um mit geläufigeren Bildern zu sprechen, noch mit der Pferdekutsche reisen. Ging doch auch früher. Und das ist kein dummes Argument. Es ging und geht ja tatsächlich.

Trotzdem gibt es natürlich Entwicklungen, Modernisierungen, neue Erkenntnisse. Und die mündeten eben in einem neuen Paradigma, einem Modell, das statt des maschinenhaften Vorgehens eines prozeduralen Ansatzes die Vorstellung realisierte, dass in der wirklichen Welt auch keine Maschinen Abläufe abarbeiten, sondern Objekte durch Kommunikation miteinander interagieren. Damit sollte eine größere Komlpexität erreicht werden.

Allerdings muß man hier streng unterscheiden zwischen Sprachen, die objektorientierte Methoden anbieten und echt objektorientierten Sprachen. Es ist möglich, in jeder Programmiersprache objektorientiert zu programmieren. Es ist halt nur mehr oder weniger aufwendig. Für Neugierige ganz grob erklärt: Man definiere einen record (in C: struct) und wenn man während der Laufzeit einen solchen braucht, erzeuge man einen Zeiger (pointer) auf einen solchen record-Typ. Man muss dann noch in einer gewissen Weise mit den zugehörigen Funktionen und Prozeduren umgehen, aber im Prinzip war´s das (auch das läßt sich mit Zeigern auf die Funktionen und Prozeduren lösen).

C++ Prozedurale Sprache mit objektorientierten Elementen.
Java, Smalltalk80 Echte objektorientierte Sprachen

C++ ist dabei die eher hemdsärmeligere Sprache, Java die hohe Theorie. Naja, das ist etwas lax ausgedrückt, aber es trifft den Kern.

Um es kurz zu umreißen: Der objektorientierte Ansatz funktioniert recht gut solange ein Programm läuft. Aber wie startet man sowas? Jeder, der gerade Java lernt und schon mal C oder Pascal gesehen hat, kennt das ziemlich dumme Gefühl: "Wie starte ich das blöde Ding?". Und das ist nicht der einzige Haken. Jedesmal, wenn in einem der einschlägigen Java-Bücher der Vergleich zu C++ betrieben wird, liest man Sätze wie "Während in C++ ... zulässig ist, geht das in Java natürlich nicht, weil das kein sauberer, objektorientierter Ansatz ist." Und dann liest man eine Fußnote im Sinne von: "Aber wenn Sie´s doch mal brauchen, geht das schon, es heißt nur anders." Und meistens ist es dann etwas umständlicher.

Das beweist, daß des objektorientierte Modell ein sehr gutes Modell ist, aber die Welt, die dadurch abgebildet werden soll, eben nicht so rein und ordentlich ist, wie es das Modell voraussetzt. Aus diesem Grunde fühlen sich Praktiker auch oftmals in C++ wohler, da kann man halt auch mal schweinern, wenn man´s unbedingt braucht.

Wozu also?

Wo aber sitzt denn nun das schneidende Argument, das wirklich überzeugt? Die Begründung findet sich nicht in den Hobby-PCs oder in den kleinen Softwarehäusern, die kleine und mittlere Programme schreiben. Erst ab einer gewissen Komplexität zeigt die OOP (objektorientierte Programmierung) ihre Stärken. Stellen wir uns ein Projekt vor, das insgesamt einen Umfang von 50.000 Zeilen oder noch mehr: 200.000 Zeilen hat. Das schafft auch nicht mehr ein Programmierer, dazu braucht man eine Truppe. Oftmals sitzen die noch nicht mal an einem Ort, sondern in der Welt verstreut.

Worauf muß man in so einem Falle achten?

  1. Bessere Modularität: Wenn viele Programmierer zusammenarbeiten, müssen die Kuchenstücke streng getrennt verteilt sein. Und kein Programmierer A darf die Strukturen und Abläufe eines Prorammierers B manipulieren, weil er zu faul oder zu eingebildet ist, vernünftige Strukturen oder Abläufe zu formulieren. Es sind also Sicherungsmechanismen notwendig, durch welche die benutzten Strukturen vor unzulässigen Zugriffen während der Laufzeit geschützt werden. Es müssen deswegen nicht nur die Datenstrukturen, sondern auch die Manipulatoren (Methoden) sauber definiert sein, ähnlich wie im Autobau, wo jeder Zulieferer nicht nur Maße, sondern auch Funktionalität strengstens einzuhalten hat.
  2. Strengere Kapselung: Schon durch die Einführung von lokalen Variablen und Parameteübergabemethoden an Module wurde verucht, Programmierer vor sich selbst oder Teamkollegen zu schützen. Aber es wird nicht gerne benutzt. Der OO-Ansatz geht noch weiter, indem er nicht einzelne und unzusammenhängende Datenstrukturen und Module absichert, sondern das manipulierte Objekt selbst mit seinen Attributen und Methoden als Ganzes kapselt und damit viel umfassender schützt. Das wiederum unterstützt dezentrale Teamarbeit sehr.
  3. Schnittstellen: Es ist bekanntermaßen ein Kreuz, mit lokalen Variablen zu arbeiten. Globale sind viel bequemer. Naja, aber auch Autofahren ohne Gurt ist bequemer. In beiden Fällen gilt: Stimmt, solange nix passiert. Programmiert aber ein Team, zudem noch lokal getrennt, so ist es völlig unmöglich, sich überhaupt nur ansatzweise über die benutzten Variablennamen abzusprechen. Man braucht also diese ungeliebten und bescheuerten Schnittstellen. Kennen Sie ja, das sind die Dinger in der Deklarationszeile der Funktion oder Prozedur, die man so oder anders "übergeben" kann. Das gilt jetzt nicht nur für Datenstrukturen, sondern auch für Methoden. Durch dei Übergabe eines Objektes wird beispielsweise auch eine Schnittstelle zu dessen Methoden geschaffen.
  4. Zuverlässigkeit: Selbst wenn im besten Falle alles ohne 1 bis 3 geht. Sobald das Programm in die freie Wildbahn gerät (also zum Kunden), fällt alles in sich zusammen. Denn auch wenn Ihr Test-Team nichts gefunden hat - Ihr Kunde wird garantiert sehr schnell die erste Schwachstelle finden. Viel Spaß beim Kundengespräch. Dem kann man vorbeugen durch Ausnutzung aller gebotenen Möglichkeiten der OOP.
  5. Wartbarkeit: Ein schnell hingerotztes Programm mag zwar funktionieren und auf Parties bei unwissenden Gästen eine gewisse herablassende Bewunderung erzeugen (denn IT-Leute sind freaks, Clowns, die man nur in der Manege bewundert, nicht außerhalb des Zeltes), aber die nachher Ihre Firma in den Ruin treiben, weil die Wartungskosten unerträglich in die Höhe schnellen. Und ich garantiere: Niemand weiß dann, warum das so ist. Meistens nicht mal der betreffende "Programmierer".

Wir fassen zusammen: OOP ist notwendig wie ein Feuerlöscher. Solange nichts passiert, stört das blöde Ding. Aber wehe es brennt, und keiner ist da! OOP hat sicherlich auch Schwächen, aber es ist mindestens eine gute Art und Weise, sich die dümmsten Probleme bei der Programmierung vom Hals zu halten.

Wir brauchen aber eine grundlegende Definition, naja, eigentlich vier... Es soll David Flannagan zitiert werden mit seinen unsterblichen Worten:

"An Object is an Instance of a Class."

Nun könnten wir aufhören, das ist der Kern der Objektorientierten Programmierung. Nein, im Ernst, zumindest ist es ein guter Merksatz und er enthält viel Wahrheit.

Definition: Eine Klasse ist die Definition eines neuen Datentyps in Form eines speziellen Datensatzes, der nicht nur Datenfelder (Attribute) enthält, sondern auch Methoden, die den Zugriff auf die Datenfelder regeln oder deren Manipulation oder die Kommunikation mit anderen Objekten ermöglichen.

Definition: Eine Methode ist eine Funktion oder Prozedur, die einen Bestandteil einer Klasse darstellt und ausschließlich über das zugehörige Objekt aktiviert werden kann. Mit einer Methode werden die Eigenschaften des Objektes kontrolliert, zum Beispiel über die Attribute.

Definition: Eine Instanz ist eine konkrete Verwirklichung eines Plans oder einer abstrakten Vorlage.

Definition: Ein Objekt ist eine Entität, die mit ihrer Umwelt interagiert, ein Gegenstand von Aktivitäten anderer Objekte.

Und was ist eine Entität? Na, irgendwas eben, etwas "seiendes".

Während der Begriff "Instanz" eher mit der Existenz zu tun hat, beschreibt das "Objekt" die Eigenschaften und die Aktionen eines Individuums.

Und schon habe ich Ihnen die beiden Grundelemente einer Klasse untergejubelt:

  1. Eigenschaften - Das sind die Datenfelder. Sie werden ab jetzt Attribute genannt.
  2. Aktionen - Das sind die Methoden.

Und so ein Ding kreieren wir jetzt.

Das erste Objekt

Mein Lieblingsbeispiel ist aus einem "KnowWare"-Buch: Die Dackelverwaltung. Einerseits vergißt man dieses Beispiel nie, andrerseits kann man daran wunderbar deutlich machen, was Objekte sind. Dazu taugen die Superbeispiele aus den Hyperbüchern eher nicht. Aber lesen Sie die trotzdem dann nach den ersten Versuchen.

Es soll eine Klasse "Dackel" definiert werden. Einem Dackel kann man die Eigenschaften "Alter" und "Name" zuordnen. Ok, das gleiche gilt auch für Menschen oder Autos, aber es soll hier erst mal ausreichen:

class Dackel
{
        int Alter;     // Attribut "Alter"
        string name;   // Attribut "name"
}; // end of class Dackel

Hmm. Sieht doch aus wie ein C/C++-struct, oder? Richtig! Und das ist es auch. Wir werden gleich nochmal darauf zurückkommen.

Ein Programm, das die wichtigsten Funktionalitäten enthält, sieht so aus:

/*** Das Programm wird so nicht laufen. Es sind noch kleine Änderungen nötig. ***/

/* Headerdatien für Ein-/Ausgabe und Stringverarbeitung einbinden.*/
#include <iostream>
#include <string>
/* Namensraum std für Elemente aus iostream vorgeben; z.B. cin, cout, etc. */ using namespace std;
class Dackel
{
        int Alter;
        string name;
}; // end of class Dackel
int main ()
{
       /* Objekt (Variable) aus der Klasse "dackel" wird deklariert. */
             Dackel d;
       /* Arbeitsvariablen für die Werte der Datenfelder werden deklariert. */
             int a;     // für Datenfeld "Alter"
             string n;  // für Datenfeld "Name"
       /* Eingabe der Werte für die Datenfelder über die Tastatur. */
             cout << "Name: ";
             cin >> n;
             cout << "Alter: ";
             cin >> a;

       /* Die eingegebenen Werte werden auf die Attribute verteilt. */
             d.name  = n;
             d.Alter = a;

       return (0);
}

Das ist aber noch nicht alles. Bisher haben wir nämlich nichts gewonnen. Das hätten wir auch in C oder Pascal machen können.

Kapselung

Um nun die Struktur wirklich schützen zu können - vor dem Programmierer selbst und seinen Kollegen, die dem Code wissentlich oder unwissentlich Ungereimtheiten zufügen wollen, muß nun alles gekapselt werden. Hier gilt wieder ein altes Prinzip:

Um gut arbeiten zu können, stehen alle Werkzeuge zur Verfügung. Wir müssen sie aber auch anwenden!

Um die gewünschte Kapselung zu erreichen, muß man nun selbst etwas tun, nämlich einer Konvention folgen. Das ist immer unbequem. Aber bequem soll man es zu Hause haben, auf der Arbeit soll man gute Qualität liefern, und das heißt Schweiß zu vergießen.

Was ich bisher nämlich verschwiegen habe, ist die Tatsache, daß man in einer Klasse einstellen kann, ob die Elemente (Attribut oder Methode) von "außen" sichtbar sind oder nicht.

Um es kurz zu machen, hier ein paar goldene Regeln, die es lohnt einzuhalten:

  • Alle Attribute werden grundsätzlich "versteckt". Es gibt nur ganz selten Ausnahmen.
  • Von den Methoden werden einige versteckt und andere werden öffentlich angeboten.
  • Um Attribute zu ändern, dürfen nur (speziell konstruierte) Methoden benutzt werden.

Es gibt mehrere Arten von Methoden. Wir werden hier nicht alle besprechen, nur die wichtigsten.

Zugriffsmethoden

Die wichtigsten Methoden sind die Methoden, welche das Hauptwerkzeug der Kapselung sind. Man nennt sie Zugriffsmethoden oder access methods. Um Attribute zu verstecken, muß man sie als solche kennzeichnen. Dies wird mit den Wörtern "public" oder "private" gemacht. Das sind nicht die einzigen, aber die einzigen, die hier besprochen werden sollen. Es gibt wunderbare weiterführende Texte, aus denen man die Tiefen der Objektorientierung lernen kann.

Die Klasse wird nun erst mal ergänzt:

class Dackel
{
   private:     
        int Alter;
        string name;
}; // end of class Dackel

Damit wird dem Compiler angezeigt, daß auf die Attribute nicht von "außen" zugegriffen werden darf. In einer class sind alle Attribute nämlich als private voreingestellt. Dazu gleich mehr. Und das ist auch der Grund, warum der gerade angegebene Quelltext nicht übersetzt werden kann: Spätestens bei der Zeile

cin >> n;

würde der Compiler mit einer Fehlermeldung abbrechen (z.B. ´´Kein Zugriff auf private Element, dessen Deklaration in der Klasse "Dackel" erfolgte´´; MS-Visual C++ Compiler).

Somit haben wir diesen Teil der Struktur gekapselt. Und wie kommen wir an die Datenfelder nun heran? Die können ja nicht einfach leer bleiben!

Dazu benötigen wir Methoden, mit deren Hilfe wir die Attribute manipulieren können. Nein nein, kein Argument "das geht mit einer Zuweisung doch einfacher!"! Das ist es ja gerade, was wir verhindern wollen! Wer´s noch nicht begriffen hat, halte hier mit Lesen an und wiederhole den Abschnitt "Wozu also?" weiter oben!

Für jedes private-Attribut werden genau zwei Methoden benötigt: Eine zum Auslesen des Wertes (get) und eine zum Zuweisen eines Wertes (set). Man ergänzt den Methodennamen dann um den Attributnamen. Hier sehen wir auch gleichzeitig, wo Methoden überhaupt untergebracht werden, nämlich in der Klassendefinition:

Ergänzen wir den Quelltext um die zwei nötigen access methods:

class Dackel
{
   public:

        void setAlter (int age)
{
Alter = age;
}
int getAlter () const { return Alter; } void setName (string s) { name = s; }
        string getName ()
{
return name;
}
private: int Alter; string name; }; // end of class Dackel

Methoden werden wie normale C-Funktionen formuliert. Neu ist der Teil "public". Er ist von außen zugreifbar, also beispielsweise aus dem Hauptprogramm "main()".

Wie werden Methoden aufgerufen? Wie die Attribute. Man schreibt den Variablennamen des Objekts, dann einen Punkt und dann den Methodenaufruf. Also zum Beispiel:

das_objekt.die_methode(die_parameter);

Das Geheimnis von vorhin lüften wir jetzt! Es wurde vorhin ja festgestellt, daß Klassen - in C++ - eigentlich erweiterte "struct"s sind und erst mal als private vorgegeben werden. Nun merken wir uns (für C++):

  • ein struct ist per default immer public.
  • eine class ist per default immer private.
  • Beide reservierten Worte sind in C++ sogar austauschbar, das sollte man aber auf keinen Fall tun!

Deswegen kann man z.B. in einer Klassendefinition das "private" weglassen. Ich empfehle aber dringend, es trotzdem hinzuschreiben. Erstens ist es nicht falsch und zweitens macht es die ganze Sache übersichtlicher. Und das ist ja der Grund, warum man solche Hochsprachen verwendet. Will man es unübersichtlich, braucht man kein C oder C++ oder Java, dann kann man Fortran, Basic oder Assembler nehmen.

Es fällt auf, daß die access methods einem Muster folgen:

  • die get-Methode hat keinen Parameter. Dafür gibt sie den Typ des Attributs zurück.
  • die set-Methode hat einen Parameter vom Typ des Datenfeldes, aber keinen Rückgabewert.

Also formulieren wir ein Programm mit der Erweiterung:

class Dackel
{
   public:

        void setAlter (int age)
{
Alter = age;
} int getAlter () const { return Alter; }
string getName ()
{
return name;
}
        void setName (string s)
        {
                name = s;
        }
            
   private:
        int Alter;
        string name;

}; // end of class Dackel

int main ()
{
       /* Objekt (Variable) aus der Klasse "dackel" wird deklariert. */
             Dackel d;
       /* Arbeitsvariablen für die Werte der Datenfelder werden deklariert. */
             int a;     // für Datenfeld "Alter"
             string n;  // für Datenfeld "Name"
       /* Eingabe der Werte für die Datenfelder über die Tastatur. */
             cout << "Name: ";
             cin >> n;
             cout << "Alter: ";
             cin >> a;

       /* Die eingegebenen Werte werden auf die Attribute verteilt. */
             d.setAlter (a);
             d.setName (n);

       /* Zusätzliche Ausgabe der Werte. */
             cout << "Alter: " << d.getAlter () << endl;
             cout << "Name: " << d.getName () << endl;

       return (0);
}

Sieht umständlich aus, ist es auch, aber es ist notwendig, und einer der Gründe, warum man überhaupt diese Programmiermethode eingeführt hat (siehe "Wozu also?").

Andere Methoden

Methoden werden natürlich auch zur algorithmischen Manipulation von Objekten eingesetzt. Und schließlich gibt es noch spezielle Methoden, mit denen Objekte eingerichtet werden. In diesem Abschnitt machen wir gleich beides.

Die erste Variante soll an der Eigenschaft von Dackeln zu bellen gezeigt werden. Die zweite Variante sind sogenannte Kontruktoren. Wenn nämlich ein Objekt erzeugt wird, will man normalerweise die Attribute mit Inhalten füllen, eventuell eine Datei öffnen oder andere Dinge vorbereiten. In der klassischen prozeduralen Programmierweise nennt man das "Initialisieren". Oft vergißt man das aber erst mal, und so gibt es in der OOP den Mechanismus des constructors. Diese Methode (es können auch mehrere sein) wird zwingend automatisch bei der Erzeugung eines Objekts einmal aufgerufen. Man kann dieser Methode über Parameter bestimmte Initialwerte übergeben und so dafür sorgen, daß das Objekt in einem sauber definierten Zustand das Licht der Welt erblickt. Wenn das Objekt nicht mehr gebraucht wird, "stirbt" es, es wird wieder aus dem Arbeitsspeicher entfernt. Java macht das über eine garbage collection, einen Prozeß, der unabhängig von äußeren Einflüssen ab und zu nach Objekten sucht, auf die keine Referenz mehr existiert und diese dann "löscht". C++ überläßt das (wie in der C-Tradition üblich) dem Programmierer. Die dazu nötige Methode heißt dann sinnträchtig destructor.

Wir werden nun also drei Methoden hinzufügen:

  1. Einen (zusätzlichen) constructor
  2. Einen destructor
  3. Eine Methode bellt()

Dabei ist zu beachten, daß die Konstruktoren denselben Namen besitzen müssen wie die Klasse! Daran erkennt man diese Methoden übrigens auch.

Wichtig dabei ist, daß es immer einen Konstruktor gibt. Hat man nämlich keinen eigenen Konstruktor definiert, so wird der Standardkonstruktor ausgeführt. Er hat keine Parameter und keine Anweisungen. Klingt ziemlich schwachsinnig, aber man tut das aus zwei Gründen:

  1. Man sichert die Funktionalität des Konstruktors ausnahmslos zu.
  2. Man kann durch ihn das Objekt überhaupt erst (parameterlos) anlegen.

Ohne Konstruktor gibt´s nämlich gar keine Objekte. Definiert man nun einen eigenen Konstruktor, so wird der Standardkonstruktor nicht mehr angeboten, statt dessen werden der oder die eigenen Konstruktoren benutzt. Will man also weiterhin ein Objekt parameterlos deklarieren können, muß man den Standardkonstruktor nun selbst einbauen. Dies wird im Beispiel auch getan.

Wie unterscheidet der Compiler aber die Konstruktoren, die ja alle denselben Namen haben? Natürlich über die Parameterliste. Die ist nämlich verschieden. Dackel d wird ohne Parameter eingeführt - also wird der Standardparameter benutzt, das heißt ausgeführt. Dackel e benutzt einen int-Parameter. Das bedeutet, daß der zweite Konstruktor benutzt wird, der das Attribut Alter auf 5 initialisiert.

Der Destruktor (nur C++) darf nur einmal definiert werden. Er ist daran erkennbar, daß er vor dem Namen eine Tilde ("~") hat. Sobald das Objekt also - wie auch immer - aus dem Speicher entfernt wird, kommt der Destruktor zur Anwendung. Aufrufen kann man ihn selbst nicht.

Was im Beispiel im einzelnen passiert, wird später im Teil II dieses Textes besprochen. Für´s erste soll diese Erklärung ausreichen.

Der Quelltext gleich als komplettes Programm:

class Dackel
{
   public:
        Dackel () {};                 // Standardkonstruktor
Dackel (int startAlter) // Zweiter Konstruktor
{
Alter = startAlter;
}

~Dackel () // Destruktor
{
if (name != "")
cout << "Dackel " << name << " tot!" << endl;
else
cout << "Heapdackel weggeräumt." << endl; }
        void bellt ();
        {
            cout << "Wuff! " << endl;
        }

        void setAlter (int age)
{
Alter = age;
} int getAlter () const { return Alter; }
string getName ()
{
return name;
}
        void setName (string s)
        {
                name = s;
        }
            
  private:
        int Alter;
        string name;

}; // end of class Dackel

int main ()
{
       /* Objekt (Variable) aus der Klasse "dackel" wird deklariert. */
       /* Dackel d wird mit dem Standardkonstruktor erzeugt, parameterlos. */
             Dackel d;

       /* Dackel e wird mit dem zweiten Konstruktor erzeugt, der als */
       /* Parameter das Alter erhält. Dackel e hat also anfangs das Alter 5. */
             Dackel e (5);

       /* Arbeitsvariablen für die Werte der Datenfelder werden deklariert. */
             int a;     // für Datenfeld "Alter"
             string n;  // für Datenfeld "Name"
       /* Eingabe der Werte für die Datenfelder über die Tastatur. */
             cout << "Name: ";
             cin >> n;
             cout << "Alter: ";
             cin >> a;

       /* Die eingegebenen Werte werden auf die Attribute von -d- verteilt. */
             d.setAlter (a);
             d.setName (n);

       /* Zusätzliche Ausgabe der Werte des Dackels -d-. */
             cout << "Alter: " << getAlter () << endl;
             cout << "Name: " << getName () << endl;

       /* Dackel e bellt, d hält jedoch die Schnauze. */
             e.bellt ();

       return (0);
}

Damit wollen wir den Teil I beenden. Im nächsten Teil erfahren wir ein wenig über den dynamischen Einsatz von Objekten und ein paar komische Eigenschaften von Methoden.

Bis dahin, enjoy oop!


   (c) 2005 Schoenleber.com