Objektorientierte Programmierung

>>Die Studenten schauen sich den Kram an, den ich ihnen präsentiere, und sagen: >Ach, man kann das doch auch alles hintereinanderschrciben, dann kann man es besser verstehen.<<<

Roland Krause, Bioinformatiker

Viele Menschen wünschen sich im Leben generell etwas mehr Ordnung. Schön, dass man so viele Stifte, Legosteine und Notebooks hat, aber noch schöner wäre es, wenn man nicht nachts drauftreten würde, weil sie überall rumliegen. Daher ist der Wunsch nach Aufbewahrungslösungen recht groß: Man stopft alles hinein, dann tritt man nicht mehr auf seine Sachen, sondern hat auch noch ungefähr im Kopf, in welcher Kiste sie sich befinden könnten.

Das Verfahren, alles in Kisten zu sammeln, hat allerdings auch seine Nachteile, denn irgendwann weiß man nicht mehr, was man in welchen Karton geräumt hat. Hier hilft es, die Dinge nach Ähnlichkeit zusammenzupacken: Stifte in eine Schublade und Kinderspielzeug in eine eigene Kiste (oder in fünf). Dann weiß man vielleicht immer noch nicht genau, wo die Kiste mit den USB-Kabeln ist, aber man muss nur noch nach Kisten suchen, nicht nach Einzelteilen, und das ist erfahrungsgemäß viel einfacher.

Prozedurale Programmierung entspricht häufig dem Vorgehen, einfach alles in einer Kiste zu lagern: Diverse Funktionen und Variablen werden in eine Datei gesteckt, aber wenn man eine bestimmte Funktion sucht, muss man ein gutes Gedächtnis haben. Eine Suchfunktion hilft zwar, aber wenn man nur noch ungefähr weiß, wie eine Funktion heißt, nutzt auch sie nicht viel. Das fundamentale Problem ist, dass es keine technischen Maßnahmen gibt, die einen zwingen, eine gewisse Ordnung zu halten.

Objektorientierte Programmierung erlaubt es hingegen, Sourcecode entsprechend dem zweiten Modell zu organisieren. Alles, was zu einem Objekt gehört, z. B. Variablen und Funktionen, wird in eine Datei geschrieben, in der ausschließlich das zu finden ist, was zu diesem Objekt gehört. Hält man sich nicht an diese Regel, mosert der Compiler.

Die Wahl, ob man seine Programme objektorientiert oder prozedural schreiben will, ist für Einsteiger schwierig zu beantworten. Sie haben vielleicht gelesen, dass Objektorientierung modern ist und dazugehört, sind sich aber nicht so genau im Klaren darüber, was diese Methode der Softwareentwicklung konkret bringen soll. Diese Unsicherheit rührt teilweise daher, dass bei der objektOrientierten Programmierung vieles ganz anders heißt als in der prozeduralen Entwicklung, selbst wenn es ähnlich funktioniert. Auch wird objektorientierte Programmierung oft als grundverschieden von der prozeduralen dargestellt. Machen Sie sich darüber nicht allzu viele Sorgen, denn man lernt zwar durch OOP eine andere Art, sich Problemlösungen vorzustellen, aber das bedeutet nicht, dass man von Anfang an anders denken muss.

Tatsächlich ist Objektorientierung historisch gesehen eine Erweiterung prozeduraler Programmierung. Beide Arten zu programmieren sind gleich mächtig, es gibt also kein Problem, das man nicht auch prozedural lösen könnte. Objektorientierte Programmierung ist nur klarer und strukturierter. Das ist schnell dahinbehauptet, aber gedulden Sie sich einen Moment, wir kommen gleich zu den Begründungen.

Objektorientierung bedeutet auch nicht, dass Sie jetzt all ihr schönes Wissen über proze-durale Programmierung, Variablen und Funktionen über Bord werfen müssten. Das, was Sie aus der prozeduralen Programmierung als »Funktionen« kennen, nennt man in der OOP »Methoden«, und sie bestehen aus handelsüblichem prozeduralen Code. Die Besonderheit der OOP besteht darin, dass die Methoden zusammen mit Variablen (die hier gerne Member genannt werden) in spezielle Module verpackt werden, die dann »Objekte« heißen.

Der Unterschied zwischen prozeduralem und objektorientiertem Denken ist für den Programmierer hauptsächlich eine Frage der Perspektive. Beispielhaft lässt sich das gut anhand von Kochrezepten erläutern.

Ein Kochrezept beschreibt, wie eine Person die Zutaten abwiegt, mischt und erhitzt. Der Ablauf ist dabei zeitlich linear - zumindest in guten Rezepten. Erst misst man Zutat X ab, dann misst man Zutat Y ab, dann rührt man Zutaten X und Y zusammen, dann brät man sie und so weiter. Die Zutaten selbst sind nur rein passive Bestandteile, die vom Koch auf eine gewisse Weise behandelt werden. Die Schritte zum Endergebnis sind sehr detailliert beschrieben.

Die geistige Sicht eines objektorientiert Programmierenden entspricht eher der Rolle, die ein Chefkoch in einer Restaurantküche einnimmt: Er kennt das Rezept und kann Teile davon delegieren. Der Chefkoch weiß, dass er einem Hilfskoch genug Wissen über Salzkartoffeln zutrauen kann, dass er nur den Auftrag zur Salzkartoffelerstellung erteilt, ohne jeden Schritt vorzugeben. Wie der Hilfskoch die Salzkartoffeln genau kocht, ist seine Sache, solange das Ergebnis stimmt.

Es gibt in diesem vereinfachten Beispiel also eine Arbeitsteilung: Der Hilfskoch bereitet seine Kartoffeln so zu, wie es für ihn am effektivsten ist, und der Chefkoch prüft nur noch das Ergebnis und kümmert sich ansonsten um die Zubereitungsschritte, die in seinen eigenen Kompetenzbereich gehören, was ihn vor Mikromanagement bewahrt und von Details entlastet.

Vorteile der objektorientierten Programmierung

Historisch ist objektorientierte Programmierung aus einer Krise entstanden. Ende der 70er stieg die Speicherausstattung der Computer durch den Wechsel auf integrierte Schaltkreise stark an. Die Softwareentwickler der damaligen Zeit nutzten die neuen Ressourcen, um benutzerfreundlichere und leistungsfähigere Programme zu schreiben, was längeren Sourcecode pro Projekt bedeutete. Der niederländische Informatiker Edsger Dijkstra beschrieb die Entwicklung 1972 in einem Beitrag über die Frühgeschichte seines Fachs so:

»Der Hauptgrund für die Softwarekrise liegt darin, dass die Maschinen um mehrere Größenordnungen zu mächtig geworden sind. Vereinfacht gesagt: Solange es gar keine Maschinen gab, war Programmierung kein Problem. Als wir ein paar schwache Computer hatten, wurde die Programmierung zu einem überschaubaren Problem, und jetzt, wo wir gigantische Computer haben, ist die Programmierung zu einem ebenso gigantischen Problem geworden.«

E. W. Dijkstra Archive: >>The Humble Programmier« Seite 340.

Und was für ihn gigantische Computer waren, ist nichts gegen das, was heute in einem Smartphone steckt.

Dadurch, dass jede Funktion überall aus dem Programm aufgerufen werden konnte, war es sehr schwer, durch Refactoring (siehe Kapitel 15) den Code zu verändern, denn jede Änderung einer Funktion kann Änderungen in den Bereichen nach sich ziehen, die diese Funktion aufrufen.

Die Projekte wurden auch schnell zu groß, als dass ein einzelner Entwickler noch die Übersicht über alles hätte behalten können. Also mussten die Aufgaben in Module aufgegliedert werden, und für jedes Modul war ein Team zuständig. Um weiterhin ein funktionierendes Programm zu erhalten, mussten zwischen den Modulen Schnittstellen vereinbart werden. Bald war der Sourcecode größerer Projekte mit den damaligen Mitteln nicht mehr zu bändigen. Die Entwickler verloren die Übersicht und begannen, Funktionalität mehrfach zu implementieren, weil ihnen nicht bewusst war, dass es sie bereits gab. In dieser Situation war objektorientierte Programmierung eine echte Hilfe, denn sie sorgt dafür, dass mehr Programmierer reibungsloser an größeren Projekten zusammenarbeiten können, und erleichtert die Definition von Schnittstellen zwischen Programmteilen.

Insbesondere das Aufkommen grafischer Benutzerinterfaces in den 1980ern verschärfte diese Krise, weil ihre Elemente, also etwa Fenster oder Menüs, besonders reichhaltige und damit komplexe Interaktionen mit sich bringen. Gleichzeitig lassen sich die Elemente von GUIs mit objektorientierten Mitteln besonders gut programmieren, was die damals neue Technologie für Entwickler interessant machte. Ein Schaltknopf in einem GUI kann beispielsweise mehrere Zustände annehmen: normal, geklickt und disabled. Um je nach Zustand ein anderes Aussehen anzunehmen, benötigt ein solches InterfaceElement relativ viel internen Code und einige Variablen, wobei ein Programm, das diesen Knopf nur verwenden will, von diesem Code nichts wissen muss. Damit eignet sich so ein Knopf gut als wiederverwendbares Modul: Als Entwickler von Programmen mit grafischer Benutzeroberfläche muss ich mir nur merken, dass ein Schaltknopf unterschiedliche Zustände haben und auf Klicks eine von mir mitgegebene Funktion aufrufen kann. Wie er das genau macht, kann mir egal sein, und damit bleibe ich von einer Menge Komplexität verschont.

Objektorientierte Programmierung kann auf unterschiedliche Weise hilfreich sein: Objekte geben Hinweise dazu, wie man sie verwenden soll.

Da die Methoden eines Objekts zu ihm gehören, kann man an ihnen ablesen, was man mit dem Objekt anstellen kann. Das Objekt definiert also eine Schnittstelle zur Außenwelt. ln der prozeduralen Programmierung liegen Funktionen und Daten ohne Bezug nebeneinander, daher ist nicht ganz so leicht ersichtlich, mit welchen Funktionen man welche Daten verarbeiten kann.

Der Code wird besser wartbar, weil Objekte in sich abgeschlossen sind.

Nicht der gesamte Code, der zu einem Objekt gehört, ist für alle Welt sichtbar. Es kann private Methoden geben, die nur aus dem Objekt heraus aufrufbar sind. Das verbessert die Wartbarkeit, weil nicht jede Änderung auch Veränderungen in entfernten Codeabschnitten nach sich zieht (lokale Änderungen haben lokale Auswirkungen). Gut geschriebener prozeduraler Code legt zwar auch großen Wert auf Wartbarkeit, aber OOP unterstützt die Entwickler dabei. Dauerhaft modularen Code zu schreiben, gelingt ohne Unterstützung durch die Sprache nur ganz wenigen - es ist schon mit OOP nicht immer einfach.

Standardisierung

OOP ist eine Methode zur Organisation und Strukturierung von Code, die man Leuten, mit denen man zusammenarbeiten will, nicht erst mühsam erklären muss. Es gibt auch andere Methoden, aber irgendein gemeinsamer Standard ist wünschenswert - und OOP ist ein derzeit weit verbreiteter.

Zusammengehöriges wird zusammengefasst.

Dadurch, dass Variablen und der Code, der mit ihnen umgeht, zu einem Objekt zusammengefasst sind, wird der Code übersichtlicher, denn man kann die Zusammengehörigkeit leicht erkennen. Jedes Objekt hat einen Namen, der wie in unserem Spielzeugkistenbeispiel einen Hinweis darauf gibt, welche Funktionalität sich in ihm verbirgt. Beispielsweise wäre es in der prozeduralen Programmierung naheliegend, beim Datenbankcode eine getUserldQ-Funktion zu haben und in einer anderen Datei bei der Benutzerverwaltung eine deactivateUser()-Funktion. ln einem objektorientierten System würde man ein User-Objekt anlegen, und in ihm die beiden Methoden User-> getld() und User->deactivate().

Komplexität wird verkapselt.

Die Komplexität von Code steckt häufig in der Fülle an Details und Bedingungen, die beachtet werden müssen. Diese Komplexität verschwindet mit objektorientierter Programmierung nicht, wird aber im Objekt durch die oben erwähnten privaten Methoden verkapselt. (Das Prinzip der »Encapsulation« erklären wir weiter unten noch genauer.) Ein Objekt mag immer noch 100 Methoden haben, aber wenn man es nur benutzen will, benötigt man nur die Handvoll Methoden, die nach außen sichtbar sind. Man muss sich nicht mit seiner vollen Komplexität auseinandersetzen.

Objektorientierte Programmierung erleichtert es, Daten zu modellieren.

ln der objektorientierten Programmierung versucht man immer, Ähnlichkeiten zwischen verwandten Daten bzw. Code zu entdecken und von diesen verwandten konkreten Beispielen zu einem allgemeinen Fall zu gelangen, der die konkreten Fälle abdeckt. In der prozeduralen Programmierung strebt man zwar ebenfalls die Abstraktion von konkreten Usecases zu allgemeinen Regeln an, objektorientierte Programmierung unterstützt die Suche nach einer generischen Lösung aber besonders wirkungsvoll durch das Mittel der Vererbung. Die Details folgen weiter unten, aber Vererbung erlaubt es, gewisse Eigenschaften eines generischen Objekts in konkreten Fällen zu verändern.

Die Prinzipien objektorientierter Programmierung

Modularität und Abschottung

Software, die über längere Zeit entwickelt wird, kommt häufig an den Punkt, dass jeder Teil eines Programms jede andere Funktion aufrufen kann. Wenn Ihnen das bekannt vorkommt, wissen Sie vermutlich auch, dass dieser Zustand sehr viel Arbeit verursacht, falls man jemals eine Funktion anpassen muss. Das System ist eng gekoppelt - verschiedene Programmbereiche haben Kreuz- und Querverbindungen untereinander. Solche eng gekoppelten Systeme enthalten Code, der sich nur schwer herauslösen und in anderen Systemen wiederverwenden lässt. Und häufig genug bedingt eine Änderung an einer Stelle weitere Änderungen in ganz anderen Codebereichen.

Wenn Sie so eine zusammengeschweißte Codemasse vermeiden wollen, dann muss es abgeschottete Bereiche geben, die für gewisse Teile des Quellcodes direkt zugänglich sind, während andere Bereiche nur durch Schnittstellen darauf zugreifen können. Sie wissen dann vorher, welche Codebereiche von Änderungen betroffen sein werden, denn das können nur die Funktionen aus dem gleichen Objekt sein, weil sie im gleichen abgeschotteten Bereich leben.

Entwickeln Sie beispielsweise ein Content-Management-System, bei dem es Benutzer mit einfachen Rechten wie »Artikel verfassen« und »Artikel lesen« sowie Administratoren mit weitergehenden Rechten gibt, dann müssen Sie das Berechtigungslevel jedes Benutzers ständig abtragen können, um im User-Interface Funktionen freizuschalten oder zu blockieren. In unserem Beispiel dürfen alle Admins und die regulären Benutzer, die dafür explizit freigeschaltet sind, Beiträge verfassen.

Sie würden die Benutzer in einem Array verwalten: var $users = [

{ id: 1, name="Hans Meiser", mayWrite = true, level="user"},

{ id: 2, name="Bernd Lauert”, mayWrite = false, level="admin"},

];

Überall, wo ein Benutzer neue Einträge anlegen könnte, prüfen Sie Folgendes:

•    Ist der Benutzer Admin, dann darf er, unabhängig ob mayWrite true oder false ist.

•    Ist der Benutzer einfacher User, dann darf er nur, wenn mayWrite true ist.

Sie können diese Prüfung natürlich in eine Funktion auslagern, aber trotzdem müssen Sie vor dem Aufruf den entsprechenden Benutzer aus $users heraussuchen und dieser Funktion zur Prüfung übergeben. Die Variable $users könnte auch durch Fehler in anderen Codebereichen verändert werden, möglicherweise selbst in solchen, die mit dem Verfassen von Beiträgen nichts zu tun haben.

Objektorientiert würden Sie ein Objekt erstellen, das die Prüfung erledigen kann und gleichzeitig die Angaben zum Benutzer enthält:

object User = {

private var $id; private var $name; private var $mayWrite; private var $level;

function hasWritePrivileges() { if ($level == "admin") { return true;

}

return $mayWrite;

}

function getld() { return $id;

}

function getName() { return $name;

}

}

Die Funktion zur Prüfung, ob der Benutzer Artikel verfassen darf, ist jetzt in das Objekt gewandert. In unserer erfundenen OOP-Syntax ist sie nach außen sichtbar, während die Felder $mayWrite und $level nach außen nicht sichtbar sind. Die Spielregeln dafür, ob ein Benutzer aufgrund seiner expliziten Berechtigung oder seines Levels Artikel verfassen darf, werden nur noch im Objekt in der Methode hasWritePrivileges() geprüft. Der wesentliche Punkt ist hier, dass die Funktionen, die im Benutzerinterface bestimmte Funktionen erlauben oder verbieten, nichts über die Regeln des Rechtemanagements wissen müssen - das bleibt dem User-Objekt vorbehalten. Weder der Level noch die explizite Berechtigung können von außen durch einen Programmierfehler geändert werden, sie werden bei der Erzeugung des Objekts festgelegt.

Dieses Prinzip, englisch Encapsulation genannt, ist ein wichtiger Grundsatz der OOP. Es reduziert die Komplexität eines Programms, indem die innere Funktionsweise eines Objekts verborgen bleibt. Hier trifft der Begriff »objektorientiert« dann wieder halbwegs zu: Auch in der realen Welt wissen wir in der Regel weder auf der Hardware- noch auf der Softwareebene genau, wie ein Computer funktioniert. Die vollständige Komplexität ist hübsch verpackt und wir benutzen nur das Gesamtsystem.

Wie hasWritePrivileges() die Berechtigung bestimmt, ist nach außen absichtlich intransparent. Wenn Sie jemals die Rechtevervaltung in einer Datenbank implementieren wollen, dann können Sie das durch Verändern von hasWritePrivileges() tun, ohne den Rest des Programms zu stören. Und sollten Sie jemals einen neuen Berechtigungs-level wie »Editor« entführen, dann müssen Sie die Logik von hasWritePrivileges() und auch die Variablen wie $level nur an einer zentralen Stelle ändern. Nach außen ändert sich nichts, Sie prüfen weiterhin durch Aufruf von hasWritePrivileges().

Objekte kennen ein »Innen« und ein »Außen«. Für die Methoden von User-Objekten sind die Felder (Variablen) $mayWrite und $level innen. Auch die Funktion hasWritePri-vileges() gehört zum Innen und darf daher auf die Werte der Felder zugreifen. Von außen sind sie nicht zugänglich, weil sie als private deklariert sind.' Folgender Code würde also Fehler werfen:

var $user = new User(); print ($user->$level);

Die print-Anweisung versucht, von außen auf die private-Variable $level zuzugreifen, was aber nicht erlaubt ist. Sinn dieser Einschränkung ist, dass die Außenwelt brav beim Objekt anfragen muss, wenn sie etwas über als private deklarierte Variablen wissen will. Ähnlich wie lokale Variablen einer Funktion von außerhalb der Funktion nicht verändert oder gelesen werden können, sind private-Variablen lokal für ihr Objekt.

Abstraktion

Objektorientierte Programmierung ist darauf ausgelegt, den Programmierer in der Abstraktion von verwandten Problemen hin zu einem gemeinsamen Kern und unterschiedlichen Details zu unterstützen.

i Es gibt auch objektorientierte Sprachen ohne private Variablen, beispielsweise Python. Hier obliegt es dem Programmierer, keinen Unfug mit dem Innenleben von Objekten anzustellen.

Um beispielsweise in einem Grafikprogramm verschiedene Bildformate darstellen zu können, müsste man in der prozeduralen Programmierung viele Funktionen schreiben:

function renderJpeg ($x, $y, $width, $height, $data){

}

function renderPng ($x, $y, $width, $height, $data){

}

function renderTiff ($x, $y, $width, $height, $data){

}

Man muss kein Top-Programmierer sein, um einzusehen, dass es zwischen (Raster-)Bild-formaten neben einigen Unterschieden eine Menge Gemeinsamkeiten gibt: Ein Bild besteht aus Binärdaten, es hat eine Höhe und eine Breite und kann an einer beliebigen Position auf dem Display angezeigt werden. Diese Beschreibung ist eine Abstraktion, denn wir berücksichtigen nicht, welches Bildformat Transparenz beherrscht, welches verlustfrei komprimieren kann und ob es die Farben als 8 Bit (das sind 256 Farben, z. B. GIF) oder in 24 Bit (16,7 Mio. Farben und Transparenz, z. B. JPEG) codiert.

Es wäre schön, wenn wir nur eine renderlmage-Funktion hätten. Aber in der prozeduralen Programmierung müssten wir entweder für jedes Grafikformat eine eigene schreiben oder in der Funktion untersuchen, welches Grafikformat in $data steckt, um dann die formatspezifischen Dekomprimierungsfunktionen aufzurufen, was zu hässlich langen und komplexen Funktionen führen würde.

Objektorientiert kann man diese Abstraktion viel leichter vollziehen, indem man ein Image-Objekt definiert:

object Image = { var $height; var $width; var $data;

abstract function render ($x, $y);

}

Neben den Variablen $height, $width und $data gibt es eine Art Schablone für eine ren-der()-Funktion. Sie ist nicht implementiert - das ist die Aufgabe der Entwickler, die spezifischen Code für bestimmte Bildformate schreiben wollen - und daher als »abstract« definiert. Wir legen an dieser Stelle schon mal fest, dass jede konkrete render ()-lmple-mentierung zwei Variablen $x und $y übergeben bekommt, die für die Position auf dem Schirm stehen, an die das Bild gerendert werden soll.

Mithilfe der oben beschriebenen Vererbung kann man jetzt spezifische Objekte für die Repräsentation und Darstellung verschiedener Bildformate definieren:

object Pnglmage inherits Image { function render ($x, $y) {

}

}

Diesen Schritt von der Abstraktion zurück zu konkreten Implementierungen beschreiben wir im folgenden Abschnitt »Polymorphismus«.

Durch die Reduktion der verschiedenen Bildformate auf ihre Gemeinsamkeiten ist der Code teilweise weniger gut an die Besonderheiten jedes Bildformats angepasst: Ein Format, das seine Daten als 8 Bit hält, wäre besser dran, wenn es voraussetzen könnte, dass der Bildschirm auch nur in 8 Bit farbcodiert ist. Da wir aber höherwertige Grafikformate unterstützen wollen, muss die render()-Funktion des 8-Bit-Formats davon ausgehen, dass die Farbtiefe des Schirms 24 Bit ist, und die Daten entsprechend umcodieren.

Was zunächst als Nachteil erscheint, hat in der Praxis aber auch erhebliche Vorteile, denn die Reduktion von ähnlichen Typen auf ihre Gemeinsamkeiten macht die Benutzung des Codes viel einfacher. Einfacher deshalb, weil eher unwichtige Details wegabstrahiert werden: Als Entwickler muss ich nicht wissen, welche speziellen Optimierungen oder Fähigkeiten Format X besonders gerne voraussetzen würde, sondern ich kann mir eine einfache, formatübergreifende Art und Weise merken, Bilder auf den Schirm zu bekommen. Wie das genau geschieht, dafür ist derjenige verantwortlich, der die Unterstützung für sein bestimmtes Grafikformat programmiert.

Abstraktion betont die Essenz, den Kern einer Gruppe verwandter Datenstrukturen oder Codeblöcke und unterdrückt die spezifischen Details.

Abstraktion erzeugt eine klare Trennung zwischen Programmierern, die Codemodule nur verwenden wollen (in unserem Fall: Bilder zeichnen lassen), und Programmierern, die neue Codemodule schreiben wollen (in unserem Fall: neue Formate unterstützen). Die Aufgabe der einen ist damit erledigt, die Funktionen korrekt aufzurufen, während die anderen nur Funktionen schreiben müssen. Die beiden Gruppen müssen sich nicht mehr über eine Schnittstelle unterhalten - diese ist durch das abstrakte Objekt definiert.

Um erfolgreich objektorientiert zu programmieren, ist es gut, wenn man sich frühzeitig angewöhnt, nach Eigenschaften der Datenstrukturen oder Programmteile zu suchen, die sich so weit ähneln, dass sie abstrahiert werden können. So ist es in einem Warenwirtschaftssystem sinnvoll, alle Artikel auf ein Objekt zu reduzieren, das Eigenschaften wie Preis oder Lieferzeit hat. In einem Bildbearbeitungssystem würde man möglicherweise ein Basisobjekt Filter definieren. Jede konkrete Implementierung eines Filter-Objekts bringt dann eine Methode mit, die ein Eingabebild verändert. Wie sie arbeitet, ist der konkreten Implementation überlassen, das Basisobjekt hat damit nichts zu tun.

Definieren Sie zunächst Ihre abstrakten Basisobjekte und deren Eigenschaften und programmieren Sie dann die konkrete Implementierung, ergibt sich von allein eine bessere logische Gliederung Ihres Codes.

Polymorphismus

Nachdem Sie die Gemeinsamkeiten eines Objekts abstrahiert haben, müssen Sie sich um die Unterschiede kümmern, um diesen Code in unterschiedlichen konkreten Fällen anwenden zu können. In dem Beispiel von oben hatten wir mit dem Image-Objekt die grundlegenden Funktionen eines Bildes beschrieben. Um mit dem Code etwas Sinnvolles anzufangen, müssen Sie jetzt für unterschiedliche Bildformate spezifische Image-Objekte entwickeln:

object Pnglmage inherits Image { function render ($x, $y) {

}

}

object Jpeglmage inherits Image { function render ($x, $y) {

}

}

Die vorher abstrakte Funktion render() wird jetzt mit Leben gefüllt durch Code, der PNG- bzw. JPEG-codierte Daten (aus $data) auspacken und an eine Position auf dem Display schreiben kann. Die Variablen $height, $width und $data sind Teil des Elternobjekts Image und werden daher für die spezifischen Objekte nicht noch einmal definiert. Die Koordinaten $x und $y, an denen das Bild angezeigt werden soll, sind nicht Teil des Objekts, weil man das gleiche Bild eventuell auch an anderen Stellen auf dem Display anzeigen will.

Erst durch Polymorphismus kann man mit OOP wirklich modulare und wiederverwendbare Software erstellen, denn man entwickelt Schnittstellen, die für mehrere unterschiedliche, aber verwandte Bereiche unverändert bleiben. Egal, welches Grafikformat wir unterstützen wollen: Die grundlegenden Funktionen bleiben gleich. Wir können uns darauf verlassen, dass alle Grafikformate diese Funktionen unterstützen - wie das erreicht wird, interessiert uns an der Stelle nicht.

Das Zusammenspiel von Abstraktion und Polymorphismus erleichtert die Wiederverwendung von Code, weil man ihn auf immer gleiche Weise in verschiedenen Umgebungen einsetzen kann. In unserem Beispiel wäre die Einbindung eines neuen Grafikformats sehr einfach, allerdings wäre es möglicherweise aufwendig, die entsprechende render()-Funktion für dieses Format zu schreiben.

Vererbung

In der objektorientierten Programmierung spielt Vererbung eine große Rolle. Leider ist auch dieser Begriff nur teilweise mit dem in Einklang zu bringen, was wir üblicherweise darunter verstehen. Während in der realen Welt Eigenschaften von den Eltern auf Kinder einmal vererbt werden und die Kinder dann ein komplett eigenes Leben führen, bleiben Objekte in Verbindung: Ändern sich die Eigenschaften des Elternobjekts, verändern sich auch die abgeleiteten Objekte. Der gemeinsame Nenner ist, dass Eigenschaften übernommen werden, ohne dass sie neu erfunden werden müssen.

Unsere oben eingeführten Pnglmage- und Jpeglmage-Objekte sind nicht unabhängig voneinander, sondern Kinder von Image-Objekten. Während der Code, mit dem die PNG-bzw. JPEG-Dateien entpackt werden, je nach Grafikformat unterschiedlich aussieht, gibt es zwischen den Tochterobjekten doch Gemeinsamkeiten. Beispielsweise könnte man die Breite oder Höhe des Bildes in Pixeln wissen wollen, egal, welches Bildformat man vor sich hat. Den Code dafür will man natürlich nicht für jedes Bildformat neu schreiben.

Hier kommt Vererbung ins Spiel: Man schreibt den Code nur einmal als Methode im Image-Objekt, und dessen Kinder erben sie. Das sieht so aus:

object Image = { var $height; var $width; var $data;

abstract function render ($x, $y);

function getWidth () { return $width;

}

function getHeight () { return $height;

object Pnglmage inherits Image { function render ($x, $y) {

}

}

Um ein Pnglmage-Objekt zu erzeugen, schreibt man nun: var $png = new Pnglmage(url);

print ("Breite: "+$png.getWidth()+" Höhe: "+$png.getHeight());

Wie Sie sehen, hat sich an der Definition des Pnglmage-Objekts nichts geändert, aber es hat Funktionalität hinzugewonnen, weil wir die Methoden getWidth() und getHeight() im Elternobjekt implementiert haben. Im gleichen Atemzug haben auch alle anderen spezifischen Bildformate diese Funktionalität ... tja, geerbt. Statt nur ein bestimmtes Bildformat zu erweitern, haben wir mit einem Handgriff alle schlauer und nützlicher gemacht.

Das ist natürlich kein Zauberwerk In der prozeduralen Programmierung hätte man den Code für die verschiedenen Bildfunktionen eben um zwei erweitert, eine für die Höhe, eine für die Breite. Allerdings hätte man das dann für alle andern Formate nachziehen müssen.

Die Implementierung der Methoden im Elternobjekt hat drei Auswirkungen:

1.    Wenn ein Programmierer den Code sieht, dann weiß er, dass die Methoden getHeight() und getWidth() für alle Bildformate aufrufbar sind. Würde man sie im Pnglmage- und im Jpeglmage-Objekt implementieren, könnte es ein drittes Bildformat geben, das sie nicht hat.

2.    Jedes Bildformat, das wir unterstützen wollen, muss Höhe und Breite besitzen. Damit wird eine Anforderung explizit gemacht, die man leicht als gegeben betrachten würde, ohne sie zu überprüfen.

3.    Die Kindobjekte enthalten nur den spezifischen Code für ihr Grafikformat. Das Elternobjekt enthält nur den allgemeineren Code. Das hält die Objekte aufgeräumt und trennt Aufgaben klar. In den allermeisten Fällen schaut man sich nur an, welche Methoden das Elternobjekt bereithält, weil man für die Anzeige von Bildern nicht unbedingt die Spezifika der Grafikformate beachten muss. Vererbung unterstützt also Polymorphismus.

Sinnvoller Einsatz von OOP

OOP ist immer dann eine gute Sache, wenn Sie es mit Daten zu tun haben, zwischen denen viel Interaktion stattfinden kann.

Denken Sie zum Beispiel an ein Shopsystem. Die Anforderungen an ein solches System sind folgende:

•    Kunden können Artikel (Warenposten) auswählen und in den Einkaufswagen legen.

•    Kunden können sich einen Gesamtpreis anzeigen lassen.

•    Kunden können die Waren im Warenkorb bestellen oderden Warenkorb stornieren.

Wenn man diese Anforderungen in Software umsetzen will, sieht man schnell, dass sie sich gut mit verschiedenen logischen Einheiten umsetzen lassen: ein Lager, das Waren hält, ein Kunde, ein Einkaufswagen und verschiedene Artikel. Diese Hauptfiguren unserer Geschichte interagieren auf bestimmte Weise.

Wenn ein Kunde einen Artikel in den Warenkorb legt, wird eine Anfrage ans Lager abgesetzt, ob der entsprechende Artikel vorhanden ist.

Um den Gesamtpreis anzuzeigen, muss der Einkaufswagen wissen, welche Waren er enthält. Der Code, der den Einkaufswagen implementiert, berechnet den Gesamtpreis, indem er die Waren einzeln nach ihrem Preis fragt, um daraus eine Summe zu bilden.

Um die Bestellung abzuschließen, werden die Artikel aus dem Lager ausgebucht. Das Lager muss seinen Warenbestand aktualisieren und gleichzeitig muss der Warenkorb geleert werden.

Um eine Bestellung zu stornieren, wird der Warenkorb geleert.

Wenn Sie die Shopanforderungen auf diese Weise analysiert haben, dann haben Sie schon eine einfache Softwarearchitektur mit Modulen und Interaktionen entwickelt. Diese Architektur lässt sich einfach auf die Softwareobjekte Item (Artikel), Cart (Einkaufswagen), Customer (Kunde) und Magazine (Lager) abbilden:

Article { var id; var name; var price;

}

Cart {

var articles[];

function empty() {

articles = new list();

}

function getTotalPrice() { var int price = 0;

for (var i = 0; i < articles.length; i++) { price = price + articles.get(i).price;

}

return price;

}

function addArticle(Article article) {

}

}

Customer { var cart; var store;

function placeOrder() {

var articles = cart.articles; cart.empty();

...    II mit den Artikeln dann zum Bezahlen.

for (var i = 0; i < articles.length; i++) { store.removeArticle (articles.[i]);

}

}

function cancelShoppingCart() { cart.empty();

}

}

Magazine {

var articles[];

function removeArticle(Article article) {

for (var i = o; i < articles.length; i++) { if (articles[i].id == article.id) { numInStore[i] = numInStore[i]-1;

}

}

}

function hasArticle(Article article) {

... II hat das Lager einen bestimmten Artikel?

}

function addArticle(Article article) {

... II füge dem Lager einen bestimmten Artikel hinzu

}

}

Bedenken Sie, dass jedes Objekt, das Sie anlegen, eine Daseinsberechtigung haben sollte, weil es nützliche Eigenschaften mitbringt, die bisher nirgendwo implementiert waren. Objekte haben immer dann eine überzeugende Daseinsberechtigung, wenn die Interaktion von Objekt A mit einem anderen Objekt den Status von Objekt A, dem anderen Objekt oder beiden ändert - in unserem Fall können Artikel in Warenkörbe gelegt und entfernt oder dem Lager entnommen werden. Es ist für jemanden, der Ihren Code liest, einfacher zu verstehen, wenn ein Warenkorb seinen Inhalt selbst kontrolliert und verändert, als wenn man in einem globalen zweidimensionalen Array (alle Kunden, alle Waren) erst den Warenkorb eines Kunden heraussucht und dann in diesem die Artikel verändert. Beim objektOrientierten Ansatz hat jedes Kundenobjekt nur ein Warenkorbobjekt und kann die Artikelobjekte von diesem herausgeben lassen, ohne sich damit beschäftigen zu müssen, wie die Artikelobjekte genau vorgehalten werden. Man hat also nur mit einem einfachen Array statt einem zweidimensionalen zu tun.

Schreibt man eine Applikation objektorientiert, dann kann man mithilfe von kurzen umgangssprachlich formulierten Handlungsabläufen grob die Zusammenarbeit der Objekte skizzieren, ohne sich zu früh Gedanken über die Details zu machen. Eine solche grobe Kritzelei kann beim Entwurf eines Systems hilfreich sein, um die Features und Userinteraktionen zu skizzieren. Natürlich geht das bei prozeduraler Programmierung auch, aber OOP eignet sich besonders gut dazu, die Brücke von der umgangssprachlichen Beschreibung (»Also, wenn der Kunde bestellt, dann müssen alle Warenposten aus dem Einkaufskorb raus, und aus dem Lager müssen sie auch gelöscht werden«) zur Implementierung zu schlagen, weil man die Softwareobjekte als mentale Einheiten begreifen kann, die ihre eigenen Verhaltensweisen haben. Beispielsweise hat unser Cart-Objekt (der Warenkorb) die Methode empty(), die man von customer. placeOrder() aus mit cart.empty() aufruft.

Nachteile und Probleme

Weniger gut eignet sich objektorientierte Programmierung, wenn Sie einfach nur eine große Zahl ähnlicher einfacher Daten haben, denn die Objekte haben dann fast nur Methoden, die für das Lesen und Schreiben von Member-Variablen zuständig sind. Wenn man aber nur Daten ablegen will, kann man das einfacher in einem Array oder Hash tun. Stark typisierte objektorientierte Sprachen wie Java zwingen einen stattdessen, Objekte zu definieren, auch wenn der Objektansatz an dieser Stelle eher unpraktisch ist.

Objekte sind dann von Vorteil, wenn sie ...

•    ... abhängig von ihrem inneren Zustand unterschiedliches Verhalten zeigen. Beispielsweise ein User-Objekt, das bestimmte Aktionen nur erlaubt, wenn es den Zustand >>eingeloggt<< hat.

•    ... Berechnungen mit den eigenen Daten anstellen können und damit ein komplexes System nach außen hin ein einfach verwendbar machen.

•    ... verschiedene Variationen eines Themas darstellen, so dass es Grundfunktionen gibt, die alle verwandten Objekte teilen, und darüber hinaus Spezialfunktionen.

ln der Softwareentwicklung hat man es aber recht häufig mit gleichförmigen Daten zu tun, die keine komplexen Berechnungen oder Interaktionen aufweisen. Diese Art von Daten kann man zwar ebenfalls objektorientiert darstellen, man gewinnt aber wenig gegenüber prozeduraler Programmierung.

Als Beispiel könnte die Auswertung der Umsätze der letzten Jahre für alle Ihre Filialen dienen, also eine Zeitreihe einfacher Zahlen pro Filiale. Man könnte das zwar objektorientiert modellieren, indem man ein Auswerteobjekt, Filialobjekte und Umsatzobjekte definiert, aber es gibt zwischen den Umsätzen relativ wenig Interaktionen. Eine Auswertung der Durchschnittsumsätze pro Filiale liest einfach den Strom der Verkäufe ein, summiert ihn auf und bildet einen Durchschnittswert, der Zustand jedes einzelnen Verkaufs ändert sich nicht. Das gedachte Umsatzobjekt bringt auch keine eigene Intelligenz mit; das Umsatzobjekt wäre ein reiner Container für eine Zahl, im Gegensatz zu einem Warenposten, der Brutto- aus Nettopreisen bilden kann oder unterschiedliche Preise für unterschiedliche Konfigurationen hält. Diese Art von Daten können Sie besser und einfacher als Array abbilden und Funktionen, etwa zur Berechnung der durchschnittlichen Umsätze, prozedural programmieren.

var float[][] sales;

function getAverageRevenueForBranch (int branchld) { var sum = 0;

for (var i = 0; i < sales[branchld].length; i++) { sum = sum + sales[branchld][i];

}

return sum I sales[branchld].length;

}