PHP, ursprünglich eine prozedurale Skriptsprache, hat erst mit Version 4 brauchbare objektorientierte Züge angenommen. In diesem Kapitel will ich versuchen zu zeigen, wie man objektorientiert in PHP programmiert und auch ein paar Beispiele geben, um zu sehen, wie man von der Problemstellung zur OO-Umsetzung kommt und auch, daß sich das bißchen Mehraufwand wirklich lohnt (Stichwort Übersichtlichkeit/Wiederverwendbarkeit). Das folgende Kapitel gilt der Einfachheit halber nur für PHP 4.06 aufwärts, exklusive PHP 5.
Zu beachten ist, daß der Name ,,stdClass`` in PHP4 reserviert ist, ebenso wie alle Methodennamen, die mit zwei Underscores (z.B. __sleep, __wakeup) beginnen.
Beispiel:
class DSP_CR { function Auto() { ... } ... }
Eine Besonderheit beim Umgang mit Konstruktoren ist schließlich noch der implizite Aufruf des Konstruktors der Basisklasse in dem Fall, daß eine Klasse keinen Konstruktor besitzt. Die Unfähigkeit von PHP3, dies zu meistern, ist auch der Grund, warum nach Möglichkeit PHP4 eingesetzt werden sollte.
Beispiel:
class Auto { ... } class DSP_CR extends Auto { ... }
Beispiel:
class Auto { var $baujahr; var $farbe; ... } class DSP_CR extends Auto { // hier in Ermangelung von Methoden noch // nicht über den Konstruktor gesetzt var $farbe = "metallic"; ... }
Zur Referenzierung von Attributen mehr weiter unten (20.2.1).
Beispiel:
class Auto { var $baujahr; var $farbe; function Auto() { ... } ... } class DSP_CR extends Auto { // Da DSP_CR von Auto erbt, müssen hier keine Variablen // definiert werden. Die geerbten Attribute (und auch // Methoden) der Basisklasse kann man natürlich nutzen. function DSP_CR($farbe) { if (empty($farbe)) $this->farbe = "metallic"; else $this->farbe = $farbe; } function setzeFarbe($farbe) { ... } ... }
Zur Referenzierung von Methoden mehr weiter unten (20.2.1).
Folgende Syntax, in der das Schlüsselwort new neu eingeführt wird, erzeugt in PHP ein Objekt:
$meinWagen = new DSP_CR();
Das Objekt meinWagen wird hierbei von der Klasse DSP_CR erzeugt, die dazu ihren parameterlosen Konstruktor benutzt. Der Quelltext der Klasse muß natürlich bereits vom PHP-Parser gelesen worden sein, bevor eine solche Zuweisung erfolgen kann. Üblicherweise lagert man Klassendefinitionen in Include-Dateien aus, z.B. nach dem Schema <KlassenName>.inc, und importiert sie mit include() oder require().
Wird anstelle des parameterlosen Konstruktor einer verwendet, der Parameter erwartet, erfolgt die Objektinitialisierung ganz analog, indem man wie bei Funktionsaufrufen die Parameter in entsprechender Reihenfolge innerhalb der runden Klammern angibt.
Auf das so erzeugte Objekt kann nun so lange zugegriffen werden, bis es explizit auf NULL gesetzt wird oder das Skript beendet wird. Allgemein kann man mit Objektvariablen übrigens ganz analog zu normalen Primitivtyp-Variablen Objekt-Zuweisungen durchführen. Folgender Code ist somit gültig:
$meinWagen = new DSP_CR(); $meinZweitwagen = new DSP_CR(); $meinWagen = $meinZweitwagen;
Achtung: Bis vor der Zuweisung waren meinWagen und meinZweitwagen noch völlig unterschiedliche Objekte! Danach enthalten die beiden Objektvariablen zwar auch noch unterschiedliche Referenzen (zeigen also nicht auf das gleiche Objekt), aber der Inhalt der beiden Objekte ist nun so lange gleich, bis eines der Objekte geändert wird.
Innerhalb einer Klasse gehören die Attribute und Methoden der Klasse immer dem gerade zu betrachtenden Objekt, das wie gesagt nur außerhalb der Klasse existiert. Da die Klasse quasi den ,,Plan`` für ein Objekt darstellt, liegt der Gedanke nahe, daß dieser Plan in dem Moment, wo man ein spezielles Objekt betrachtet, die Eigenschaften des Objekts annimmt. Wenn also die Klasse eigentlich nur der Bauplan für Objekte ist, so verwandelt sie sich doch in dem Moment, wo man ein einzelnes Objekt betrachtet, in das Objekt selbst! Dadurch hat man die Möglichkeit, ein Objekt abhängig von seinen ganz ,,persönlichen`` Eigenschaften zu verändern, ohne es beim Erstellen der Klasse schon zu kennen! :-)
Will man also Attribute oder Methoden referenzieren, beginnt man mit dem für PHP typischen Dollarzeichen, gefolgt vom Namen des zu referenzierenden Objektes, einem Strichpfeil (->) und dem Namen des Attributs bzw. der Methode. Will man innerhalb einer Klasse auf die Attribute oder Methoden derselben Klasse zugreifen (die wie gesagt in dem Moment, wo man ein Objekt betrachtet, das Objekt selbst darstellt), muß man das Schlüsselwort this anstelle des Objektnamens verwenden.
Eine kleine Anmerkung noch: Wie vielleicht bekannt sein dürfte, werden in in doppelten Anführungsstrichen geschriebenen Strings Variablen ersetzt. Das gilt aber nur für solche Variablen, deren Namen nur aus Buchstaben bestehen. Insbesondere werden Referenzen nicht erkannt, da sie notwendigerweise den Strichpfeil enthalten. Hier ist also ein Unterbrechen des Strings unter Zuhilfenahme des Punkt-Stringoperators geboten.
Grundsätzlich ist es natürlich immer möglich, mittels einer return-Anweisung die lokale Referenz zurückzugeben und der ursprünglichen Objektvariable zuzuweisen.
$a = &$b; $c =& $a; $c = 42;
Die beiden obigen Zuweisung bewirken gleichermaßen, daß jeweils beide Variablen nicht nur denselben Wert haben, sondern auch eine Wertänderung einer der Variablen automatisch die exakt selbe Wertänderung der anderen Variablen bewirkt. Die letzte Anweisung bewirkt folglich, daß alle drei Variablen den Wert 42 annehmen.
Auch bei Funktions- und Methodenaufrufen wird normalerweise Kopiersemantik angewandt. Um explizit Referenzsemantik zu fordern, stellt man hierbei bei einer Funktions-/Methodendefinition einer zu referenzierenden Parameter-Variablen ein Kaufmanns-Und voran:
function Plus($var) { $var++; } function Minus(&$var) { $var--; } $var = 42; Plus($var); Minus($var); echo $var;
Die Ausgabe obigen Codes ist natürlich 41, da nur die Funktion Minus den Wert der Variablen $var ändert. In Java wäre das nicht so offensichtlich, da hier u.U. eine Klassenvariable namens var existieren könnte, die implizit gemeint sein könnte. In PHP müßte man dann aber $this->var schreiben.
Unter 20.7.2 findet sich noch eine Übung zu komplexeren Anwendungen der Referenzsemantik.
$car = new Auto(); $dsp = new DSP_CR(); $my_cars = array($car, $dsp); // mit Kopiersemantik foreach ($my_cars as $object_copy) echo $object_copy->farbe."\n"; // Referenzsemantik I foreach ($my_cars as $key=>$object) echo $my_cars[$key]->farbe."\n"; // Referenzsemantik II foreach (array_keys($my_cars) as $key) echo $my_cars[$key]->farbe."\n";
In diesem Beispiel habe ich direkt auf die Klassenvariable $farbe zugegriffen, weil die Klasse Auto außer dem Konstruktor keine bekannten Methoden hat. Das ist in PHP4 auch gar kein Problem; in PHP5 wird das je nach Sichtbarkeitsdefinition aber nicht mehr möglich sein. Außerdem sollte man sich grundsätzlich angewöhnen, auf Attribute nur per Getter- und Settermethoden zuzugreifen, da in diesen dann (ggf. auch erst nachträglich) geeignete Plausibilitätstests gemacht und diese bei Bedarf in erbenden Klassen überschrieben werden können.
Ein primitives Beispiel:
class Auto { ... function Erfinder() { return "Etienne Lenoir, Carl Benz"; } ... } echo Auto::Erfinder();
Beispiel: Angenommen, wir hätten in der Klasse DSP_CR die Methode lenke(drehung) der Klasse Auto überschrieben, von der DSP_CR erbt. Will man nun innerhalb von DSP_CR auf die gleichnamige Methode der Basisklasse Auto zugreifen, kommt folgende Syntax zum Einsatz:
... function lenke($drehung) { // Servo aktivieren $drehung = $this->servo($drehung); // Methode der Basisklasse aufrufen parent::lenke($drehung); } ...
/** * Auto Klasse * * @author DSP * @version 1.0 */ class Auto { /** * Baujahr * @type int */ var $baujahr; /** * Farbe * @type string */ var $farbe; /** * Konstruktor * @param $farbe gewünschte Farbe */ function Auto($farbe) { $this->farbe = $farbe; $this->baujahr = date(); } /** * Ändert die Wagenfarbe * @param $farbe gewünschte Farbe */ function setzeFarbe($farbe) { $this->farbe = $farbe; } /** * Welches Baujahr? * * @return Das Baujahr * @returns string */ function Baujahr() { return $this->baujahr; } } /** * DSP CR Klasse * Konstruktor der Basisklasse wird implizit aufgerufen * * @author DSP * @version 1.0 */ class DSP_CR extends Auto { /** * Konstruktor * eigentlich wäre gar keiner notwendig, * aber wir wollen ja die Farbe initialisieren... */ function DSP_CR($farbe) { if (empty($farbe)) $farbe = "metallic"; parent::Auto($farbe); } }
Alternativ zum letzten Codeteil, in dem der Konstruktor der Basisklasse aufgerufen wird, kann man auch folgende Syntax verwenden, die unabhängig vom Namen der Basisklasse ist - in Java würde man das übrigens mit einem einfachen super(parameter) machen.
class A extends B { function A($param) { $parent = get_parent_class($this); $this->$parent($param); } }
Neu ist auch die Unterstützung eines der zentralen Grundsätze der OOP: Sichtbarkeit. Mit PHP5 kann man Attribute und Methoden als public, private oder protected deklarieren und damit einschränken, wer darauf von wo zugreifen darf. Wem das Java vorkommt, dem werden auch static und final sowie const etwas sagen: Ersteres ist vom Prinzip her weiter oben beschrieben (20.3.1, im Unterschied dazu wird einfach ein neues Schlüsselwort eingeführt, das bewirkt, daß $this nicht verwendet werden darf); final beschreibt nicht in erbenden Klassen überschreibbare Methoden bzw. Attribute und const konnte man bisher mittels define() simulieren: konstante, also nach dem Setzen nicht mehr veränderbare Instanzvariablen.
Konstruktoren heißen jetzt standardmäßig __construct, womit der Konstruktor der Basisklasse mittels parent::__construct() eindeutig bestimmt ist. Analog dazu heißen die neu eingeführten Destruktoren, die immer dann automatisch aufgerufen werden, wenn die letzte Referenz auf ein Objekt gelöscht wird, __destruct. Mit ihnen kann man z.B. Debuginformationen sammeln oder Datenbankverbindungen sauber beenden.
Auch von Java bekannt sind abstrakte Klassen und Interfaces, also einerseits nichtinstanzierbare Klassen mit Methoden, die von erbenden Klassen überschrieben werden müssen und andererseits Schnittstellenbeschreibungen, die definieren, welche Funktionalität (Methoden) eine Klasse, die ein bestimmtes Interface implementiert, bieten muß.
Schließlich gibt es nun auch ein ausgeklügeltes System zum Abfangen von Exceptions (Ausnahmen, das sind behandelbare Laufzeitfehler im Unterschied zu nicht abfangbaren, z.B. dem Absturz der PHP-Engine). Wen wundert's noch, daß sich dieses try-catch nennt?
Wem PHPDoc nicht genug ist, der kann nun auch ganz explizit die erwarteten Klassenzugehörigkeiten vor die Variablen im Parameterteil von Methodendeklarationen schreiben, also z.B.:
... function setMetallic(Auto $car) { $car->setzeFarbe("metallic"); } ...
Diese Methode würde also alle Objekte akzeptieren, die von Auto oder einer davon erbenden Klasse (z.B. DSP_CR) instanziert worden sind. Falls ein Objekt von anderem Typ oder gar ein Primitivtyp übergeben wird, wird eine Fehlermeldung ausgegeben.
Nett ist auch, daß nun von Methoden zurückgelieferte Objekte direkt angesprochen, sprich dereferenziert, werden können. In PHP4 muß der Rückgabewert eines Funktionsaufrufs immer erst in einer (Objekt-) Variablen abgespeichert werden, auf die man dann weitere Aufrufe anwenden kann. Beispiel für die neue Freiheit:
... class MyTest { var $test; function __construct() { $this->test = "42 is the answer."; } function print() { echo $this->test; } } MyTest()->print(); ...
Wer schon einmal herausfinden wollte, von welcher Klasse ein Objekt ist (zur Laufzeit, sonst ist das ja trivial), wird sicherlich is_a und Konsorten bemüht haben. Mit PHP5 gibt es nun ganz analog zu Java die Infix-Notation instanceof, die dasselbe ermöglicht (im Gegensatz zu is_a wird der Klassenname aber nicht als PHP-String aufgefaßt, sprich gequotet).
Die neue __autoload() Funktion erleichtert das Einbinden von Klassen, indem sie immer dann automatisch aufgerufen wird, wenn eine Klasse nicht gefunden wird. Mit ihrer Hilfe kann man z.B. vom Namen der Klasse abhängig verschiedene includes definieren.
Ganz besonders schick ist die extra geschaffene Möglichkeit, Methodenaufrufe sowie das Setzen und Abfragen von Variablen zu überschreiben (mittels Methoden __call, __get und __set). Die Setter-Funktionen werden dabei konsequenterweise nicht nur bei Zuweisungen, sondern auch auch beim Inkrementieren und Dekrementieren aufgerufen. Mit diesen Eigenschaften eignen sich diese Funktionen natürlich besonders gut zum sauberen Debuggen.
Bleibt noch anzumerken, daß all dies rückwärtskompatibel implementiert wurde, d.h. PHP4-Skripts sollten mit evtl. leichten Anpassungen auch unter PHP5 laufen. Das ist wohl der Punkt, wo sich die Geister scheiden und Lager bilden - OOP-pur-Verfechter auf der einen und PHP3-Hacker auf der anderen Seite ...
Zu den weiteren Neuerungen in PHP5 zählen die rundum erneuerte XML-Unterstützung (libxml2-basiert), SQLite-Integration (dafür ist MySQL-Support nicht mehr direkt eingebaut) sowie verbesserte Handhabung von Streams.
Wer sich selbst ein Bild machen will, sollte sich mal http://www.php.net/zend-engine-2.php ansehen.
Zuerst jedoch die Methode, die Christoph bevorzugt:
Man abstrahiert von der Problemstellung soweit, daß man Subjekte, also Teile
des Ganzen mit individuellen Eigenschaften, identifizieren kann. In der
Programmierpraxis sind das z.B. eigenständige Prozesse wie Datei-Ein-/Ausgabe
oder die jeweils zentrale Informationseinheit, deren Daten z.B. in einer
Datenbank festgehalten werden.
Hat man diese Abstraktion erst einmal geschafft, fällt es leicht, diese Subjekte
mit Objekten zu assoziieren, die zu einer zu erstellenden Klasse gehören. Die
Eigenschaften des Subjekts setzt man der OO-Logik folgend in Form von Attributen
(Klassenvariablen) um. Schließlich muß man sich noch über die Schnittstelle
Gedanken machen - i.A. sind Konstruktor, Lese- und Schreibzugriff für die
Eigenschaften und einige zusätzliche Funktionen nötig, die allesamt als Methoden
der Klasse implementiert werden.
Und nun mein Ansatz:
Als erstes sollte man das Problem auf die zentrale Funktionalität reduzieren,
denn diese bildet i.A. die Schnittstelle der späteren Klasse, also die
benötigten Funktionen. Darüber hinaus werden natürlich meist noch weitere
Funktionen benötigt, diese sind aber intern und könnten daher ,private` bzw. ,protected` deklariert werden.
Zu beachten ist auch, daß man in erster Linie die Funktionalität in eine Klasse steckt, die gemeinsame Daten (die späteren Attribute) verwendet. Es folgen einige Beispiele - wer noch Anschauliches hat, kann diese gerne an Christoph schicken, der sich mit Objektorientierung in PHP mindestens so gut auskennt wie ich. ;-)
|
|
|
|
Schreibe eine Klasse ,,Image``, die mit Hilfe der PHP-Image-Funktionenein leeres Bild erzeugt und die Methode show() implementiert. Leite von dieser Klasse eine neue namens ,,Point``ab mit der Methode set(x, y), die einen Punkt malt. Programmiere schließlich die Klassen ,,Circle`` (Kreis), ,,Rectangle`` (Rechteck) als Erben von ,,Point`` sowie die Klasse ,,Square`` (Quadrat), die ,,Rectangle`` erweitert. Erstelle von jeder malfähigen Klasse ein Objekt und benutze es jeweils, um das jeweilige Symbol auszugeben.
Eine mögliche Lösung findet sich in Abschnitt B.9.
Vorgegeben ist folgende ,,naive`` Implementierung, die die besonderen Erfordernisse der Verwendung von Referenzen schlicht ignoriert und damit sicher nicht das Gewünschte leistet. Außerdem steht unten noch das erwartete Ergebnis, also das, welches eine korrekte Implementierung erzeugen würde - und deine Lösung ist das doch hoffentlich, oder? :-)
Hinweis: Eine korrekte Implementierung kann schon erreicht werden, wenn pro Zeile maximal ein Zeichen (welches wohl?) hinzugefügt wird.
class MyArray { var $array; function MyArray() {} function add($val) { $this->array[] = $val; } } class Test { var $var; function Test($var, $array) { $this->var = $var; $array->add($this); } function setVar($var) { $this->var = $var; } function getVar() { return $this->var; } }
$array = new MyArray(); $test = new Test(42, $array); $test2 = $test; $test->setVar(0); echo $test->getVar()." ".$test2->getVar()."\n"; var_dump($array);
Das Beispiel soll zuerst ,,0 0`` ausgeben. Bei falscher Implementierung (wie hier) wird statt dessen ,,0 42`` ausgegeben. Der Variablen-Dump schließlich soll bestätigen, daß tatsächlich eine Referenz auf das Objekt $test im Array $array abgelegt wurde. Ist dies nicht der Fall, ist der Wert der Variablen var 42 statt 0 und es steht dann kein Kaufmanns-Und vor object.
object(myarray)(1) { ["array"]=> array(1) { [0]=> &object(test)(1) { ["var"]=> int(0) } } }
Eine mögliche Lösung findet sich in Kapitel B.10.