Unterabschnitte


Bedeutung von Objektorientierung

In diesem Teil soll die Objektorientierte Programmierung (kurz OO bzw. OOP) allgemein erklärt werden. Im nächsten Teil kommt dann die Realisierung in PHP.

Die ersten Beispielprogramme, die im Verlauf dieses Manuals besprochen wurden, hatten keine große Struktur: In ihnen wird einfach am Anfang des Programmes angefangen und am Ende aufgehört. Teilweise wird mit if & Co. ein wenig hin und her gesprungen. Das Ganze wird allerdings schnell unübersichtlich und Teile lassen sich nur durch Cut'n'Paste wieder verwenden.

Bei der Programmierung mit Funktionen wird das ganze schon etwas übersichtlicher. Es gibt Funktionen, die bei einem Aufruf mit den übergebenen Werten etwas machen. Diese Funktionen lassen sich in diverse Dateien ausgliedern und können dadurch in mehreren Programmen wieder verwendet werden. Allerdings können die Funktionen untereinander (über den eigentlichen Funktionsaufruf hinaus) keine Werte austauschen, das heißt die Werte müssen alle über das aufrufende Programm gehen.

Bei der Objektorientierung ist man einen Schritt weiter gegangen und hat die Daten und die Funktionen zusammengefaßt, so daß man sie nun als eine Einheit betrachtet. Im Prinzip hat man damit versucht, die reale Welt abzubilden. Konkret heißt das, daß in einer solchen Einheit, Klasse genannt, die Funktionen (hier Methoden genannt) auf gemeinsame Daten (Attribute, Klassen- oder Instanzvariablen genannt) zugreifen können.

Ein kleines Beispiel: Funkwecker

Jeder kennt digitale Funkwecker. Diese besitzen ein Display, um die aktuelle Uhrzeit, das Datum und die Weckzeit anzuzeigen, und über Tasten können letztere auch eingestellt werden. Wie die Uhr die einzelnen Werte speichert, können wir von außen nicht feststellen[*]. Wir können auch nicht direkt die Uhrzeit ändern, sondern müssen die Tasten benutzen und können dadurch z.B. auch nicht 25:00 Uhr einstellen. Das heißt, die Uhrzeit ist gekapselt und man kommt nur über die entsprechenden Funktionen an sie heran. Die Gesamtheit aller Tasten in diesem Beispiel nennt man in der OOP übrigens Schnittstelle, und die Kapselung ist ein zentraler Bestandteil der Objektorientierung[*].

Wenn wir uns jetzt vorstellen, das Ganze mit Funktionen nachzubauen, hätten wir ein Problem: Wir könnten zwar Funktionen wie setTime() oder setAlarm() programmieren, könnten die Werte aber nicht speichern (auf globale Variablen wollen wir unter anderem des guten Programmierstils wegen verzichten). Bei einem Objekt gibt es das Problem nicht, denn dieses besitzt das Attribut Uhrzeit und kann diese mit Hilfe der Methode setTime() nach einer Plausibilätsprüfung setzen. Das Beispiel Funkwecker hinkt natürlich etwas, da Objekte im Hintergrund nicht einfach weiterlaufen und damit z.B. die Weckfunktion nicht funktionieren würde.

Jedem sein Auto

Ein weiteres anschauliches Beispiel, das oft verwendet wird, ist das einer Autofabrik. Stellen wir uns vor, wir würden ein Auto der Marke DSP[*] fahren, das in den DSP-Werken hergestellt wird. Wir bekommen - entweder direkt von der Fabrik oder über einen Händler - ein Exemplar eines nagelneuen DSPs geliefert und der gehört dann uns. Gleichzeitig gibt es aber noch Millionen anderer DSPs, die allesamt aus dem DSP-Werk stammen. Wenn wir vereinfachend annehmen, daß die DSP-Werke nur ein Modell ,,CR`` herstellen (das aber in millionenfacher Auflage), dann haben wir hier ein prima Beispiel für die Beziehungen der Objektorientierung:

Während die DSP-Werke die Pläne für die Konstruktion von Autos des Modells DSP CR haben, haben wir als Besitzer eines DSP CR nur genau ein Exemplar, können also selbst keine weiteren Exemplare herstellen. Uns interessiert auch gar nicht, wie der Fertigungsprozeß aussieht - wir wollen ja nur mit dem Auto fahren, es also benutzen. Alles, was wir dazu kennen müssen, ist die Schnittstelle des DSP CR: Wie startet man den Wagen, wie beschleunigt, bremst, schaltet und lenkt man?

An dieser Stelle kann man schon etwas sehr grundlegendes der Objektorientierung feststellen: Durch die Kapselung der Funktionalität erreicht man, daß man diese problemlos optimieren kann, solange man nur die Schnittstelle unverändert läßt und natürlich am zu erwartenden Ergebnis einer Funktion nichts verändert (wenn man bisher immer beschleunigt hat, wenn man aufs Gaspedal getreten hat, wäre man sicher mehr als überrascht, wenn die DSP-Werke in der nächsten Modellgeneration einfach die Bremse mit dem Gaspedal verbinden würden ...). Auch sollten Eigenschaften grundsätzlich nur über die Schnittstelle abruf- und veränderbar sein, damit etwaige Änderungen an der Art, wie etwas gelöst wurde, jederzeit vorgenommen werden können, ohne weitere Abhängigkeiten verfolgen zu müssen.

Doch zurück zum Autofahren: Wir erwarten also von unserem Wagen, daß er sich wie ein ganz normales Auto verhält. Den DSP-Werken überlassen wir es, die gewünschte Funktionalität zur Verfügung zu stellen (d.h. einzubauen). Wir wollen aber vielleicht nicht nur einen Wagen haben, sondern doch lieber zwei - z.B. einen für jeden Ehepartner. :-) Die beiden Wagen sollen aber natürlich nicht genau gleich sein, das wäre ja langweilig. Wir sagen den DSP-Werken also beim Bestellen des Zweitwagens gleich, daß wir diesmal keinen Standard-Silbermetallic-CR haben wollen, sondern lieber einen dunkelgrünen CR in der Family-Ausführung. Innerhalb der DSP-Werke ist für diese Bestellung der Konstruktor zuständig: Er nimmt die Daten auf und sorgt dafür, daß diese allen daran interessierten Fabrikationsstationen des Werkes in der einen oder anderen Form zur Verfügung gestellt werden.

Nun stellen die DSP-Werke im Wesentlichen natürlich DSP-Modelle her. Sicher gibt es aber auch den einen oder anderen Extra-Dienst, der nicht direkt abhängig von einem Wagen ist, jedoch sinnvollerweise von den DSP-Werken angeboten wird. Auf diesen Extra-Dienst soll jeder Außenstehende zugreifen können - egal, ob er einen DSP CR hat oder nicht. Vielleicht will der DSP-Konzern aber auch einfach nur eine Sonderaktion starten und für begrenzte Zeit ein Logo auf jeden gefertigten DSP CR sprühen lassen. Die beiden Fälle scheinen auf den ersten Blick völlig unterschiedliche Problematiken zu beschreiben, aber sie haben eine Gemeinsamkeit: Sie sollen beide auch indirekt von jedem Besitzer eines DSP CR aufruf- bzw. veränderbar sein. Logischerweise dürfen Extra-Dienste der DSP-Werke auf keine Eigenschaften von DSP-Modellen zugreifen, denn wenn ein solcher Dienst von jemandem erbeten wird, der keinen DSP CR besitzt, dann kann auch an keinem DSP CR eine Veränderung durchgeführt werden - nicht einmal die Abfrage von Daten eines DSP-Modells ist erlaubt[*]!

Von Autos zu Objekten

Nun haben wir hoffentlich alle verstanden, welche Beziehungen zwischen den Autofahrern und den DSP-Werken bestehen - es wird Zeit, zur Objektorientierung zu kommen. Doch Halt! Das war alles schon objektorientiert! :-) Was noch fehlt, ist eigentlich nur noch die Erklärung. Wenn man von obigem Beispiel zur OOP kommen will, muß man einige neue Begriffe einführen. In der folgenden Tabelle werden daher die Begriffe des Beispiels denen der tatsächlichen Objektorientierung gegenübergestellt.


Tabelle 18.1: Von Autos zu Objekten
Welt der Autos Welt der Objekte
DSP-Werk Klasse DSP
mein DSP CR Objekt der Klasse DSP
Eigenschaften des DSP CR Attribute der Klasse DSP
Funktionalität des DSP CR Methoden der Klasse DSP
Konstruktor der DSP-Werke Konstruktor der Klasse DSP
Zweitwagen anderes Objekt der Klasse DSP
Extra-Dienste der DSP-Werke static-Methoden der Klasse DSP
Sonderaktion der DSP-Werke static-Attribut der Klasse DSP



Vererbung

Wer hat nicht schon einmal von Vererbung gehört? Beim Programmieren und insbesondere bei Objekten geht es natürlich nicht um den biologischen Begriff. Vererbung ist der zweite fundamentale Begriff der OOP und bedeutet, daß jedes Objekt einer Klasse B dieselben Methoden beherrscht wie ein Objekt der Klasse A, von der Klasse B abgeleitet wurde, d.h. geerbt hat. Zusätzlich kann es noch eigene Methoden definieren oder vorhandene Methoden neu definieren (überschreiben).

Bezogen auf das obige Auto-Beispiel könnten z.B. die DSP-Werke ein neues Modell DSP CR-JH herausbringen wollen. Der Einfachheit halber nehmen sie dazu die Pläne des DSP CR, ergänzen hier und ändern da etwas - das neue Modell basiert ja auf dem erfolgreichen DSP CR. Ebenso ist der DSP CR im Grunde auch nur ein Auto, also warum für jedes Modell das Rad neu erfinden? ;-) Denkbar ist doch, daß irgendwo in den Tiefen des DSP-Archivs schon ein Plan für ein Ur-Auto liegt, der wieder herausgekramt werden kann, wenn alle Ideen des Modells CR verworfen werden müßten - niemals aber die Grundidee des Autos!


Image-Beispiel

Ein Beispiel aus der Programmier-Praxis zeigt es noch deutlicher:
Wir haben eine Klasse ,,Image`` (Bild), die mit Hilfe der Image-Funktionen der Programmiersprache ein leeres Bild mit definierbarer Höhe und Breite erzeugt. Diese Klasse kennt die Methode show(), die das Bild anzeigt. Ein leeres Bild ist aber langweilig, also benötigen wir eine neue Klasse ,, Point``, durch die es möglich sein soll, einen Punkt in diesem Bild zu malen. Für diesen Zweck gibt es die Methode set(x, y). Da die Methode show() mehr oder weniger unverändert übernommen werden kann, nehmen wir ,,Point`` (Punkt) als Erweiterung von ,,Image``, das heißt ,,Point`` erbt alle Methoden und Attribute von ,,Image``. Auf dieselbe Weise kann man von ,,Point`` wiederum ,,Circle`` (Kreis) und ,,Rectangle`` (Rechteck) ableiten, wobei letzteres durch die Spezialform ,,Square`` (Quadrat) erweitert werden kann. Das ganze wird später als Übung noch etwas vertieft werden.


Konstruktor

Wie schon in der Erklärung des Auto-Beispiels gesehen, braucht man zum Erzeugen eines Objektes einen sogenannten Konstruktor. Bei unserem Image-Beispiel haben wir die Methode init(), die bei jedem Objekt als erstes einmal aufgerufen werden muß, damit verschiedene Attribute initialisiert werden. Wird dies nicht gemacht, kann es passieren, daß die Klasse nicht richtig funktioniert[*]. Damit aber eine solche Initialisierungsfunktion immer einmal aufgerufen wird, wurde der Konstruktor eingeführt. In vielen Programmiersprachen - so auch in PHP - heißt der Konstruktor so wie die Klasse. Der Konstruktor ist im Grunde nichts anderes als eine spezielle Methode, die keine Return-Anweisung enthalten darf, weil der Konstruktor grundsätzlich und implizit ein Objekt der Klasse zurückliefert.

Es gibt die Unterscheidung zwischen dem Standardkonstruktor und dem Allgemeinen Konstruktor. Während ersterer parameterlos und dadurch eindeutig bestimmt ist, erwartet letzterer mindestens einen Parameter und ist dadurch allgemein brauchbar (daher die Bezeichnung). ,,Allgemeiner Konstruktor`` ist natürlich nur ein Oberbegriff für alle Konstruktoren außer dem Standardkonstruktor.


Destruktor

Manchmal braucht man neben einem Konstruktor auch eine Funktion, die am Ende die Aufräumarbeiten erledigt. Auch dafür wurde eine Möglichkeit geschaffen, Destruktor genannt. Destruktoren können keine Parameter haben.

PHP kennt allerdings bisher leider keine Destruktoren. Man kann sich über die Funktion register_shutdown_function() einen Behelf zusammenbauen oder sich PHP 5 anschauen. ;-)


Klasse oder Objekt

Wer das erste Mal von Klassen und Objekten hört, wird sich unweigerlich fragen, was diese Begriffe bedeuten und was der Unterschied zwischen beiden ist. Die Erklärung zum Auto-Beispiel sollte diese Frage eigentlich schon geklärt haben, doch allgemein könnte man sagen, ein Objekt ist einfach ein konkretes Exemplar einer Klasse. Die Klasse wiederum ist die abstrakte Definition der Eigenschaften eines Objektes. Nehmen wir als Beispiel die Klasse ,,Lebewesen``. Welche Eigenschaften ein Exemplar dieser Klasse haben muß, kann dir ein Biologe sagen. Ich kann dir nur sagen, daß du ein Objekt der Klasse ,,Lebewesen`` bist: du bist ein konkretes Exemplar der abstrakten Gattung Lebewesen.


Zugriffsrechte: public, private oder doch protected?

Bei Klassen gibt es Methoden, die jedes Objekt einer solchen Klasse benutzen können soll. In unserem Beispiel sind das init(), show() und set(). Auf der anderen Seite sollen Daten ja gekapselt sein, d.h. man kann nicht direkt darauf zugreifen. Für genau diese Unterscheidung gibt es die drei Schlüsselwörter ,,public`` (auf die Methode / das Attribut darf von überall aus zugegriffen werden), ,,private`` (nur aus genau der definierenden Klasse darf auf die Methode / das Attribut zugegriffen werden) und ,,protected`` (aus der Klasse und allen abgeleiteten Klassen darf auf die Methode / das Attribut zugegriffen werden). Zusätzlich dürfen alle Objekte einer Klasse auf alle Eigenschaften eines Objekts derselben Klasse zugreifen (also auch private-definierte).

Bei unserem obigen Image-Beispiel müssen die genannten Methoden public sein (sonst würden sie keinen Sinn machen). Die Attribute wie z.B. width oder height, die über init() gesetzt werden, sollten nicht auf public gesetzt werden, damit nicht jeder an sie heran kommt. Wenn sie nun in der Klasse ,,Image`` auf private gesetzt würden, hätten wir keine Möglichkeit, aus der Klasse ,,Point`` darauf zuzugreifen. Wenn dies gewünscht ist, müssen sie auf protected gesetzt werden.

Wem dies jetzt zu viel war, der kann sich beruhigt zurücklehnen und aufatmen: PHP 4 kennt noch keine Unterscheidung zwischen public, private und protected. Es ist einfach alles public. Da sich das jedoch mit PHP 5 definitiv ändern wird (siehe 20.5), sollte man sich zumindest grundlegende, grobe Gedanken machen und z.B. alle Methoden, die ausschließlich von der Klasse selbst (oder erbenden Klassen) und nur intern benutzt werden, an das Ende der Klasse verfrachten und für private oder protected vormerken, z.B. durch Kennzeichnung mittels einem (keinesfalls zwei!) vorangestelltem Unterstrich - gilt auch für Attribute.

Christoph Reeg