fit 2002 > Programmmiersprachentypen > Konzepte und Techniken > objektorientierte Sprachen

Überblick


Durch die Verwendung des objektorientierten Konzepts wird versucht, Einheiten unseres Denkens (Objekte) bei der Erstellung eines Programms möglichst genau abzubilden. Der Vorgang dieses Abbilden ist in erster Linie unabhängig von der dabei verwendeten Programmiersprache. Sogenannte objektorientierte Programmiersprachen (bzw. Mischsprachen) zeichnen sich durch eine explizite Unterstützung des objektorientierten Konzepts durch sprachliche Mittel aus. Die grundlegenden dieser Sprachmittel sind bei allen objektorientierten Sprachen zu finden, wenn auch die Ausprägungen durchaus sehr unterschiedlich sein können. Bei objektorientierten Sprachen, liegt es nicht mehr in der Verantwortung des Programmierers, zum Beispiel Zugriffsrechte oder Typüberprüfungen selbst vorzunehmen, da ihm diese Aufgabe vom Compiler abgenommen wird. Der Programmierer wird also bei der objektorientierten Programmierung aktiv unterstützt.

Objekte sind Elemente unseres Denkens. In der Literatur sind zwei verschiedene Möglichkeiten beschrieben, Objekte aufzufassen, die jedoch zum selben Resultat führen: 1. Objekt besitzen einen Zustand, der für jedes Objekt unterschiedlich sein kann. Das Objekt kann auf Signale reagieren, was unter Umständen auch zu einer Zustandsänderung führt . 2. Objekte sind die Kapselung von Daten und der auf diese Daten anwendbaren Operationen.

Der Aufbau von Objekten wir in einer sogenannten Klasse beschrieben. Diese ist sozusagen der "Bauplan" der Objekte, der angibt, in welcher Form der Zustand bzw. die Daten des Objektes gespeichert werden und auf welche Signale sie reagieren können bzw. welche Operationen auf die Daten anwendbar sind. Klassen sind also benutzerdefinierte Datentypen vergleichbar mit den primitiven Datentypen wie Integer oder Float.

Durch das erzeugen einer Instanz, also durch das Anlegen einer Variable vom Typ einer Klasse, erhält man ein Objekt. Bei der Definition einer Klasse, werden ihre einzelnen Elemente mit Zugriffsrechten versehen. Es kann festgelegt werden ob die Elemente einer Klasse nur für Objekte vom selben Typ, von allen abgeleiteten Klassen oder überhaupt von überall zugänglich sein sollen.

Methoden sind die Rezeptoren für die Signale, die ein Objekt empfangen kann bzw. die Operationen, die man auf die Daten des Objektes anwenden kann. Verglichen mit der prozeduralen Programmierung stellen Methoden einfach Unterprogramme da. Allerdings erfolgt beim Aufruf der Methode ein Kontextwechsel. Neben den globalen und den entsprechenden lokalen Variablen sind auch alle zum Objekt gehörigen Elemente (Variablen und Methoden) sichtbar. Eine Sonderstellung nehmen Klassenvariablen und -methoden ein. Diese sind über die Klasse selbst ansprechbar, das heißt, dass es nicht notwendig ist ein Objekt von dieser Klasse zu instanzieren, um auf diese Elemente zugreifen zu können.

Oft gibt es starke Ähnlichkeiten zwischen einzelnen Objekten, doch sind sie zu unterschiedlich, um der selben Klasse anzugehören:



Alle Fahrzeuge können fahren, es ist jedoch nicht sinnvoll etwa ein Fahrrad zu betanken. Andererseits wäre es nicht sinnvoll, die Methode"fahren" für alle Fahrzeuge neu zu implementieren. Außerdem kann es Anwendungsbereiche innerhalb eines Programms geben, bei dem es nicht wichtig ist, ob sich das Fahrzeug betanken oder beladen lässt, wichtig ist nur, dass es fährt. Um dem Programmierer ein Werkzeug für solche Fälle in die Hand zu geben, wurde das Konzept der Vererbung eingeführt. Eine Klasse kann von einer anderen Klasse "abgeleitet" werden und erbt dabei sämtliche Eigenschaften und Methoden. Die beerbte Klasse wird Superklasse, die erbende Klasse Subklasse genannt.

Im oben genannten Beispiel kann eine Klasse Fahrzeug definiert werden, die die Eigenschaft "maxGeschwindigkeit" und die Methode "fahren" besitzt. Fahrrad wird dann von dieser Klasse abgeleitet und eventuell um weitere Methoden und Attribute ergänzt. Weiters wird die Klasse "MotorFahrzeug" von "Fahrzeug" abgeleitet. Denn sowohl Motorräder als auch PKWs haben eine Leistung und einen Verbrauch sowie die entsprechenden Methoden. Für eine Tankstelle ist es egal, ob das zu betankende Fahrzeug ein Motorrad oder ein PKW ist, wichtig ist nur, dass es betankbar ist. Von "MotorFahrzeug" wird wiederum PKW und Motorrad abgeleitet. Somit ergibt sich eine baumförmige Struktur, die sogenannte Klassenhierarchie:




Zwar ist das Ergebnis der einzelnen"fahren" Methoden das gleiche, allerdings ist die Art und Weise, wie die Aufgabe im Detail erledigt wird völlig unterschiedlich. Um dieser Tatsache genüge zu tun, ist es möglich, geerbte Methoden zu überschreiben. So erbt sowohl "MotorFahrzeug" als auch "Fahrrad" die Methode "fahren" von "Fahrzeug". "Motorrad" und "PKW" erben die Methode "fahren" von "MotorFahrzeug". In den Klassen "Fahrrad" "Motorrad" und "PKW" kann die Methode dann nach den entsprechenden Bedürfnissen überschrieben werden. Damit ist einerseits gewährleistet, dass alle hier auf Objekte abgebildeten Fahrzeuge eine Methode Fahren besitzen müssen, da sie ja schließlich alle von"Fahrzeug" abgeleitet sind, andererseits kann die genaue Funktion von "fahren" für jede Klasse angepasst werden.

Überall, wo innerhalb des Programms ein Objekt vom Typ "Fahrzeug" angegeben werden kann, akzeptiert der Compiler auch ein "Motorrad", einen "PKW" oder ein "Fahrrad". Die Methode "fahren" kann dabei immer aufgerufen werden. Allerdings führt der Compiler normaler Weise nicht Buch, über den tatsächlichen Typ des Objekts. Das heißt, wird an der Stelle eines "Fahrzeugs" ein "Fahrrad" angegeben und die Methode "fahren" aufgerufen, wird die in "Fahrzeug" definierte Methode aufgerufen. Dies entspricht normalerweise nicht dem gewünschten Verhalten. Deshalb wurden die virtuellen Methoden eingeführt. Wenn eine Methode als virtuelldefiniert wird, erzeugt der Compiler Code, damit die der eigentlichen Klasse des Objektes entsprechende Methode aufgerufen werden kann. Wird also ein "PKW" als "Fahrzeug" angegeben und die "fahren" Methode angegeben, wird tatsächlich die in der Klasse "PKW" definierte Methode "fahren" aufgerufen, wenn diese zumindest in den Klassen "Fahrzeug" und in "MotorFahrzeug" virtuell ist. Objekte mit virtuellen Methoden werden polymorph genannt.

Normalerweise ist es nicht sinnvoll, Objekte von den Klassen "Fahrzeug" und in "MotorFahrzeug" zu instanzieren, da nur festgelegt ist, welche Eigenschaften bzw. Fähigkeiten diese Klassen besitzen. Diese können aber noch nicht näher bestimmt werden. Man bezeichnet solche Methoden als abstrakt. So lange auch nur eine Methode einer Klasse abstrakt ist, ist auch das ganze Objekt abstrakt, was bedeutet, dass der Compiler einen Fehler meldet, sobald versucht wird ein solches Objekt abzuleiten. Dies kann - vor allem bei größeren Projekten - eine wertvolle Information für den Programmierer sein.

Das Konzept der Vererbung kann verallgemeinert werden. Jede Klasse kann beliebig viele andere Klassen beerben. So könnte zum Beispiel "Fahrrad" auch von einer abstrakten Klasse "Sportgerät" abgeleitet werden. Die wird Mehrfachvererbung genannt. Bei gleich benannten Methoden in verschieden Superklassen muss die Programmiersprache Mittel zur Verfügung stellen, um die gewünschte Methode auswählen zu können. Die vorangehende Beschreibung lehnt sich an der Programmiersprache C++ an. In vielen Programmiersprachen sind abstrakte Methoden und Mehrfachvererbung ähnlich implementiert. In anderen, wie zum Beispiel in Java ist das Problem anders gelöst: Zwar kann ein Objekt höchstens von einem anderen abgeleitet sein, dafür gibt es sogenannte Interfaces. In einem Interface sind Methoden deklariert. Eine Klasse kann beliebig viele Interfaces implementieren, indem die Bezeichnungen der Interfaces in der "implements" Klausel angegeben werden und die in den Interfaces deklarierten Methoden innerhalb der Klasse entsprechend definiert werden. Jedes Objekt besitzt zwei besondere Methoden: einen Konstuktor und einen Destruktor. Erstere wird bei der Erstellung, letztere bei der Zerstörung des Objekts aufgerufen. Dadurch wird es ermöglicht sicherzustellen, dass das Objekt richtig initialisiert wird und benötigte Ressourcen belegt werden können und zum Schluss alle Ressourcen wieder freigegeben werden können.

Der Ablauf objektorientierter Programme ist nicht mehr ganz so leicht nachvollziehbar wie bei prozedural programmierten. Objektorientierte Programme beinhalten unzählige Methodenaufrufe, sodass die Fehlerbehandlung durch die Überprüfung von Rückgabewerten der aufgerufenen Methoden sehr unpraktisch ist. Die ist vor allem bei sehr stark verschachtelten Aufrufen der Fall. Als neues Konzept wurde die Exception eingeführt. Exceptions haben zwar nicht unmittelbar mit Objektorientierung zu tun, sie sind aber sehr stark mit der Objektorientierung verknüpft. Exception sind – je nach Programmiersprache mehr oder weniger spezielle Objekte, die im Falle eines Fehlers vom Programmierer mit einer speziellen Anweisung geworfen werden können. Das bedeutet, dass der reguläre Programmablaufpfad verlassen wird und der Stack solange zurückgespult wird, bis auf irgendeiner Aufrufebene, das geworfene Objekt gefangen wird. Dies geschieht dadurch, dass der Programmierer einen bestimmten Programmabschnitt als fehleranfällig kennzeichnet und direkt anschließend mitteilt, welche Objekte er im Falle eines Fehlers fängt, und was beim Fangen welches Objektes passieren soll.

In Java müssen alle Exceptions ein entsprechendes Interface implementieren, in C++ ist es hingegen möglich, sogar primitive Datentypen (int, float,...) zu werfen. Ein weiterer Unterschied ist, das Java ausschließlich mit Objektreferenzen arbeitet, und daher auch Referenzen geworfen werden, währen in C++ die Objekte auch selbst geworfen werden können, was der Normalfall ist. Manche Programmiersprachen ermöglichen es, Operatoren zu überladen. Dies ist vor allem dann interessant, wenn die entsprechende Operation, für die der Operator steht direkt oder metaphorisch auch für die Objekte möglich ist: zum Beispiel wäre es sinnvoll für eine Klasse, die einen Vektor darstellt, Addition und Subtraktion mit einem anderen Vektor zu überladen. Weiters könnte dann noch Multiplikation und Division mit einem Skalar überladen werden. So ist es möglich, Operatoren auf Objekte wie auf primitive Datentypen anzuwenden. Die objektorientierte Programmierung hat völlig neue Designpattern hervorgebracht. Stellvertretend soll hier auf eines näher eingegangen werden: das Singleton. Das Singleton ist eine Klasse, von der es zu jeder Zeit maximal eine Instanz geben darf. Dies wird mit Hilfe von Klassenvariablen und -methoden bewerkstelligt: Jede Singleton-Klasse besitzt einen Pointer bzw. eine Referenz auf ein Objekt seines eigenen Typs. Weiters ist eine Klassenmethode implementiert, die diesen Pointer bzw. Referenz zurückgibt. Falls das Objekt noch nicht vorhanden ist, wird es erstellt. Vorsichtshalber müssen der Konstruktor, der Copy-Konstruktor (diese Konstruktoren besitzt jede Klasse automatisch, auch wenn sie nicht explizit definiert werden) und der Zuweisungsoperator privat sein, damit nicht über Umwege weitere Instanzen des Singletons erzeugt werden können.

Auch auf dem Gebiet der verteilten System konnte sich die Objektorientierung durchsetzen. Remote Procedure Calls (RPC) und verwandte Verfahren werden immer öfter durch verteilte Objekte abgelöst (z.B.: CORBA: Common Object Request Broker Architecture). Über ein Nameservice können Objekte angefordert werden, die dann völlig transparent für den Programmierer manipuliert werden können. Dabei unterscheiden sich lokale und remote Objekte nicht. Die notwendigen Daten werden serialisiert und über ein spezielles Protokoll (z.B.: IIOP aber auch XML-basierte Protokolle gewinnen immer mehr an Bedeutung) über das Netzwerk verschickt. Der Client besitzt nur einen "Stub" also nur den Kopf der Methode. Bei einem Aufruf werden die Parameter serialisiert und über das Netzwerk zum "Skeleton", also die eigentliche Implementierung der Methode, am Server gesendet. Dort wird die Methode ausgeführt und der Rückgabewert wieder serialisiert und über das Netzwerk an den"Stub" im Client zurückgesendet. Auch hier setzte sich das Konzept der Exception durch. Innerhalb eines Methodenaufrufes kann also eine Exception geworfen werden, die dann ebenfalls an den Client zurückgeliefert wird.

Auch dieser Vorgang läuft genauso ab, als wäre die Exception lokal geworfen worden. Er ist daher ebenfalls völlig transparent. Client und Server können in verschiedenen Programmiersprachen geschrieben sein, solange es eine Corba-Implementierung für diese gibt. Da sich die einzelnen Sprachen sehr stark unterscheiden können aber "Stub" und "Skeleton" genau zusammenpassen müssen, werden diese in einer speziellen Sprache beschrieben IDL (Internet Definition Language). Aus der IDL-Beschreibung der Klassen werden dann automatisch programmiersprachenspezifische Definitionen erzeugt, die dann entsprechend implementiert werden können. Der Server registriert die von ihm zur Verfügung gestellten Objekte bei einem Nameservice, wo sie von den Clients über ihren Namen gefunden werden können.

Ein großer unterschied zwischen prozeduralen und objektorientierten Sprachen gibt es auch beim Linker. Während bei prozeduralen Sprachen der Linker Methodenaufrufe unmittelbar in die Adresse des auszuführenden Codes auflöst (early binding), steht bei objektorientierten Sprachen bei einem Methodenaufruf die Adresse des Codes erst zur Laufzeit fest (late binding). Dies ist notwendig um Polymorphie zu ermöglichen. Der Compiler prüft, ob die Methode existiert, ob die Typen von den Parameter und dem Rückgabewert übereinstimmen, weiß aber nicht, welcher Code schließlich genau ausgeführt wird. Diese Tatsache und die große Anzahl der Methodenaufrufe und der damit verbunden Aufwande kosten Rechenzeit.

Weiterführende Informationen


>[oop]
Object-Oriented Programming
>[software] What is Object-Oriented Software

>Entstehungskontext | Konzepte und Techniken | Entwicklung und Auswirkungen | Praxis | Bewertung