Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
  Startseite Kapitel 1 1.2: Programmierparadigmen 
 
  1.1 Einleitung  
 

"Objektorientierte Programmierung" (OOP) ist in den letzten Jahren zu einem Schlagwort geworden, über das jedermann spricht, von dem sich viele Leute sehr viel erhoffen, und von dem wenig Leute richtige Vorstellungen haben.

Es handelt sich dabei nicht nur um eine Modeerscheinung, und wir haben es auch nicht einfach mit einer (evolutionären) Weiterentwicklung bisheriger Programmiermodelle zu tun. Objektorientiertheit ist eine grundlegend andere Herangehensweise an die Problemlösung auf dem Rechner.

Durch die wachsende Komplexität von Software in den letzten Jahren scheinen bisherige Programmiermodelle in eine Sackgasse geraten zu sein, die die Entwicklung großer Projekte sehr teuer, schwierig oder sogar unmöglich macht. Von objektorientierter Programmierung erhofft man sich vermutlich zu Recht einen Ausweg aus dieser Situation.

Üblicherweise stellt man sich unter einem Computerprogramm eine Art Kochrezept vor: Schritt für Schritt wird dem Rechner in einer Folge einfacher Anweisungen vorgeschrieben, was und wie es zu tun ist. Damit liegt der Schwerpunkt der Betrachtung auf dem "wie" der Problemlösung. Das "womit", d.h. die Daten, die zu manipulieren sind, treten dabei in den Hintergrund.

Ein objektorientierter Ansatz stellt dagegen gerade diese Daten und die mit ihnen jeweils möglichen und benötigten Operationen in den Mittelpunkt. Dabei wird möglichst versucht, Objekte aus der Realität in die Rechnerwelt hinein abzubilden.

Durch Abstraktion, d.h. Erkennen grundlegender Gemeinsamkeiten verschiedener Objekte, entstehen abstrakte Objekt-Beschreibungen, die die Objekte Instanzen sogenannter Klassen werden lassen.

Ähnlichkeiten, bzw. Spezialisierungen verschiedener Klassen wiederum werden durch den Prozeß der Vererbung von Klassen ausgenutzt. Klassenübergreifende Ähnlichkeiten von Operationen werden durch Polymorphie und Generizität unterstützt.

Ein Objekt kann von außen nicht direkt manipuliert, d.h. seine Daten verändert werden. Ein Zugriff von außen ist nur über fest definierte Schnittstellen möglich. Man sagt dann, daß man einem Objekt eine Nachricht schickt.

Das Objekt entscheidet dann, wie es auf diese Nachricht reagiert, d.h. welche sogenannte Methode es anwendet. Beispielsweise könnte sich das Objekt dazu entscheiden, seinen internen Zustand zu verändern (und dies mit einer Nachricht zu quittieren) oder einen Wert zu berechnen (und diesen Wert als Nachricht zurückzuschicken).

Statt eines bit-zermalmenden Prozessors, der Datenstrukturen zerfetzt und vergewaltigt, haben wir [in einem objektorientierten System] eine Welt von Objekten mit guten Manieren, die sich höflich gegenseitig darum bitten, diverse Wünsche auszuführen.
Daniel Ingalls -- einer der Väter der frühen objektorientierten Sprache Smalltalk
in "Design Principles behind Smalltalk"

Idealerweise sind Methoden bei strenger Objektorientierung nur über solche Nachrichten aufrufbar, also nicht als "globale" Funktionsaufrufe von allen Objekten isoliert.

Das Prinzip, Daten vor dem Zugriff von außen zu verstecken, heißt Datenkapselung oder Geheimnisprinzip und bietet enorme Vorteile, korrekte, erweiterbare und sichere Programme zu erstellen.

Die Ideen der objektorientierten Programmierung sind leider besonders schwierig zu akzeptieren und zu verinnerlichen für Programmierer, die bereits lange mit bisher üblichen prozeduralen Sprachen gearbeitet haben. Es entwickeln sich gegebenenfalls sogar regelrechte Abneigungen gegen das Konzept.

Viele Leser, die noch keine Ahnung davon haben, wie ein Computer funktioniert, finden die Ideen, die einem objektorientierten System zugrunde liegen, ganz natürlich. Dagegen denken viele Leute, die schon Erfahrung mit Computern besitzen, daß an objektorientierten Systemen wohl etwas seltsam sei.
David Robson, in "Object-Oriented Software Systems"

Leider kann die Objektorientiertheit ihre Vorteile erst bei größeren Programmsystemen voll ausspielen, so daß die Akzeptanz bei kleinen Anwendungen, im Privatgebrauch, an Schulen und sogar Universitäten zunächst zu wünschen übrig läßt. Durch den zunehmenden Einsatz in der Industrie und im Großgewerbe und den entsprechenden Bildungsdruck dringt sie aber auch immer mehr in diese Bereiche vor.

1.1.1 Objekte, Klassen, Vererbung

Man sollte versuchen, das Objekt im Sinne der Programmiersprachen nicht als zu künstliches Konstrukt anzusehen. Die Unterteilung der Datenwelt in Objekte und deren Klassifikation sind der natürlichen Identifikation von Dingen und deren Einteilung ähnlich.

Der eigentliche Beginn logischen Denkens beim Kleinkind vollzieht sich, wenn das Kind das Konzept der Gleichheit und Unveränderlichkeit, bzw. von Unterschiedlichkeit und Veränderlichkeit begreift und verinnerlicht.

Ganze Abschnitte aus den (umfangreichen) Werken des wichtigen schweizer Pädagogie-Psychologen Jean Piaget lesen sich wie Betrachtungen zu objektorientiertem Programmieren:

Long before they learn to classify objects or to arrange them in order, children perceive objects in terms of relations of similarity and dissimilarity. It is tempting to look for the origin of classification and seriation in these perceptual relationships. [...]

Knowledge is not a copy of reality. To know an object, to know an event, is not simply to look at it and make a mental copy, or image, of it. To know an object is to act on it. To know is to modify, to transform the object, and to understand the process of this transformation, and as a consequence to understand the way the object is constructed. An operation is thus the essence of knowledge; it is an interiorised action which modifies the object of knowledge. For instance, an operation would consist of joining objects in a class, to construct a classification. Or an operation would consist of ordering, or putting things in a series. Or an operation would consist of counting, or of measuring. In other words, it is a set of actions modifying the object, and enabling the knower to get at the structures of the transformation.

J. Piaget, La Genèse des Structures Logiques Elémentaires/Cognitive Development in Children
Ober- und Unterklassen werden wir immer wie im Bild links darstellen. Pfeile sollen von Ober- zur Unterklasse verlaufen, obwohl beide Richtungen ihre Berechtigung hätten:

Bei Ober- und Unterklassen verlaufen die Enthaltenseins-Relationen für die Objektmengen und die Sätze von Methoden und Eigenschaften entgegengesetzt.

Von "oben" nach "unten" werden die Klassen immer spezieller (und damit kleiner), zu ihrer Beschreibung werden aber mehr Attribute benötigt.

Bei der Darstellung von Klassen im Rechner sollte man natürlich ebenso ökonomisch vorgehen wie der menschliche Geist es ohnehin leistet: Beim Übergang von einer Ober- zu einer Unterklasse braucht man sich nicht jeweils alle Eigenschaften und Methoden zu merken, sondern nur diejenigen, die beim jeweiligen Schritt hinzukommen.

Es kann durchaus passieren, daß ein Objekte im Hinblick auf unterschiedliche Verwendungszwecke in mehrere verschiedene Klassen einsortiert wird.

Ein Buch beispielsweise wird primär als Informationsquelle angesehen, und zum Zweck Informationsbereitstellung gehört die Operation lesen und die relevante Eigenschaft Inhalt. Andererseits kann man es als Beschwerer für einen Stapel Briefe oder Blätter verwenden; die relevante Eigenschaft wäre dann Gewicht. Wenn man das Buch aus Dekorationszwecken für das Regal verwendet, wäre die Eigenschaft beispielsweise Farbe, etc.

Die Klasse Buch ist in diesem Sinn also Unterklasse der Oberklassen Informationsträger, Gewicht, Dekorationsartikel, Brennmaterial, etc., die untereinander wenig Verbindung haben dürften.

Die Vorgehensweise bei objektorientiertem Programmieren ist ganz ähnlich. Es gilt, die Objekte der Realität, die mit dem Programm in irgendeiner Form behandelt werden sollen, auf Objekte im Rechner abzubilden. Damit nicht jedes Element einzeln dem Rechner beschrieben werden muß, müssen mehr oder weniger lose zusammengehörende Objekte durch eine gemeinsame Beschreibung zusammengefaßt werden. Die Menge aller Objekte, die dieser Beschreibung genügen, nennt man dann Klasse.

Im Zusammenhang mit 2D-Computergeometrie könnte man es zum Beispiel mit Klassen Dreieck, Viereck, Kreis zu tun haben. Ein Objekt, also ein einzelnes Dreieck, Rechteck oder ein Kreis, soll verschoben, gedreht, skaliert werden können, usw.

Viele der Methoden von Dreieck und Viereck werden sehr ähnlich sein. Im einen Fall müssen beim Verschieben eben 3, im anderen 4 Eckpunkte verschoben werden, analog beim Drehen und Skalieren. Es liegt nahe, eine allgemeinere Klasse Polygon zu definieren, in der diese Methoden allgemein (für variabel viele Eckpunkte) implementiert sind.

Die Klassen Dreieck und Viereck "beerben" dann diese allgemeinere Klasse und übernehmen solche allgemeinen Methoden. Die allgemeinere Klasse wird dann Oberklasse (Basisklasse, Superklasse) genannt, die anderen Unterklassen.

Die Unterklassen behalten aber ihre Existenzberechtigung. Dinge, die allein Rechteck-spezifisch sind (z.B. genau zwei Diagonalen, vier rechte Winkel), sind in der Oberklasse nicht anwendbar. Eine Methode zum Berechnen des Flächeninhalts sollte allein aus Effizienzgründen eher in den Unterklassen implementiert sein. Die neue Methode "überschreibt" für Rechteck-Objekte dann die Polygon-Methode.

Es macht Sinn, eine noch allgemeinere Klasse GraphObjekt o.ä. einzuführen, die sowohl von Polygon wie auch von Kreis beerbt wird, obwohl Drehen und Flächenberechnung beim Kreis wohl völlig anders aussehen werden. Die Implementation ist auf dieser allgemeinen Ebene sinnlos.

Dennoch handelt es sich beim Kreis um ein 2D-grafisches Objekt. Deshalb werden die meisten Schnittstellen genauso aussehen. Gleichgültig, ob man ein Dreieck oder einen Kreis entlang eines Vektors verschieben möchte, die Nachricht sollte in beiden Fällen verschieben heißen, und als einziger Parameter wird der Vektor benötigt.

Klassen, in denen gar nichts implementiert wird, und die nur dazu dienen, Schnittstellen für ihre Unterklassen festzulegen, werden abstrakt genannt.

1.1.2 Objekte mit C

Erstaunlicherweise liefert gerade Windows® ein simples Beispiel für objektorientierte Mechanismen, das wir ohne die Kenntnis von objektorientierten Sprachen bereits verstehen können. Die Objekte, die dabei im Mittelpunkt stehen, sind selbstverständlich Fenster. Die Realisierung ist dabei nicht besonders befriedigend -- wir sollten bedenken, daß die Objektorientiertheit hier in C "simuliert" wird. Es wird aber bereits Vererbung realisiert, und es wird demonstriert, wie explizit Nachrichten ausgetauscht werden.

Das ungefähre Verhalten aller möglichen Fenster ist fest in Windows® einprogrammiert. Man kann diese allgemeine Fenster-Klasse aber beerben durch Registrierung einer eigenen Unterklasse, durch die einige Eigenschaften neu festgelegt werden. Das geschieht mit einer C-Struktur wndclass. Beispielsweise definiert man den Namen der Klasse und das Aussehen des zu verwendenden Mauscursors so:

wndclass.lpszClassName="MyClass";
wndclass.hCursor=LoadCursor(NULL,IDC_ARROW);
Wenn die Struktur fertig initialisiert ist, teilt man dem System den Wunsch nach einer entsprechenden neuen Klasse wie folgt mit:
RegisterClass(&wndclass);
Von dieser Klasse kann man nun beliebig viele Objekte erzeugen, die dann tatsächlich als Fenster auf dem Bildschirm erscheinen, nämlich wie folgt (einige Parameter sind weggelassen):
MyWindow=CreateWindow("MyClass",...);
Die Methoden, die mit Fenstern üblich sind, sind Initialisierung, Größenänderung, Neuzeichnen eines Fensterbereichs, Reaktion auf Mausklicks, etc. Die Standard-Reaktion von Windows® ist dabei meistens, nichts zu tun.

Das Beerben auf der Methoden-Ebene geschieht dadurch, daß alle (gegenüber den Standard-Fenstern) geänderten Methoden in einer Prozedur vom Benutzer definiert werden, der sogenannten Window-Prozedur (sie wird mitregistriert.)

Diese Routine wird dann bei Eintreten entsprechender Ereignisse (Fenster wird wieder sichtbar, Benutzer tippt eine Taste) automatisch aufgerufen und über eine Nachricht mit den notwendigen Informationen versorgt. Das sieht vom Prinzip wie folgt aus:

... WndProc(HWND window, UINT message, ...) { switch (message) { case WM_CREATE: /* Fenster gerade erstellt, Initialisierungen */ ... /* Reaktion: Initialisierungen */ case WM_SIZE: /* Die Gre hat sich gerade geandert */ ... /* Reaktion: Anpassung von Grafiken, etc. */ case WM_PAINT: /* Ein Teil des Fensters wird (wieder) sichtbar */ ... /* Reaktion: neu zeichnen */ case WM_DESTROY: /* Das Fenster wird gleich vernichtet */ ... /* Reaktion: Speicher freigeben o.. */ } return DefWindowProc(hwnd,message,wParam,lParam); }
Die cases werden mit return beendet, so daß die letzte Anweisung bewirkt, daß Nachrichten, die von dieser Routine nicht behandelt werden, von der übergeordneten Klasse (in Windows®) übernommen werden.

Das Benutzer-Fenster kann natürlich auch Nachrichten (an andere Fenster) verschicken, z.B.:

SendMessage(OtherWindow,WM_CLOSE,...);
Daraufhin reagiert das andere Fenster (über seine Window-Prozedur) z.B. mit der Abfrage "Wollen Sie mich wirklich schließen?".

Indirekt (über Pointer) kann man der eigenen Window-Klasse auch eigene Daten mitgeben. Außerdem können Methoden für selbstdefinierte Ereignisse geschrieben werden.
 
  Startseite Kapitel 1 1.2: Programmierparadigmen 
 

© 1998 Axel Rogat