Code Design
Grundlagen der Objektorientierten Programmierung
"Programmieren ist Verstehen." - Kristen Nygaard
Fokussiere dich beim Lernen von CPP auf die Konzepte und verliere dich nicht in den technischen Details der Sprache. Das ist laut Bjarne Stroustrup, dem Erfinder von CPP, das Wichtigste für jeden, der sich auf das Abenteuer CPP einlässt. Und es ist genau das, was kaum jemand macht. Lehrbücher zu CPP umfassen nicht selten mehr als 1.000 Seiten. Sie spiegeln damit die Mächtigkeit des Sprachumfanges wider. Die Lehrbücher zu CPP sind jedoch so aufgebaut, dass es vorrangig um die technischen Details geht. Vor lauter Einzelheiten und unendlich vielen Ecken und Winkeln werden die faszinierenden Konzepte der objektorientierten Programmierung völlig vernachlässigt.
CPP ist eine großartige Computersprache. Wer sie beherrscht, ist für nahezu jedes Softwareprojekt gerüstet. Für Computerspiele mit beeindruckender dreidimensionaler Grafik ist CPP ideal geeignet und seit vielen Jahren die erste Wahl. Die ersten Seiten der Lehrbücher zu CPP sind meist auch schnell gelesen, das Grundvokabular ist leicht erschlossen, bis hin zu Klassen, Objekten, Vererbung und Polymorphie lässt sich vieles in vielen Werken nachlesen. Und doch fehlt es am Ende am Grundsätzlichen und Verbindenden. Welche und wieviele Klassen soll ich für mein Computerspiel definieren? Wer soll von wem erben? Wer verwaltet meine hundert Monster? Wohin mit den einzelnen Aufrufen der verwendeten Grafikbibliothek? Wie wird Sound mit Bewegung kombiniert? Wohin mit der Physik-Engine, die meine virtuelle Welt mit Kräften und Energie versieht? Oftmals lassen sich einzelne Bereiche problemlos gestalten. Eine Szene mit SDL,
OpenGL oder
DirectX kann noch ohne planvolles Design programmiert werden. Und es gibt viele Tutorials, in denen einzelne Techniken bis ins Detail beschrieben werden. In diesem Text soll es im Unterschied dazu nicht um Spracheinzelheiten gehen, sondern um den Prozess der objektorientierten Softwareentwicklung.
Bändigung der Komplexität
Die Entwicklung von Computerprogrammen zählt mit zu den schwierigsten Aufgaben, die Menschen in Angriff nehmen. Jede Software kann sehr schnell eine ungeheure Komplexität erreichen. Diese zu bändigen und zu gestalten ist ein große Herausforderung. Programmierer müssen intelligent sein und logisch denken können. Sie müssen kreativ sein und zugleich auf das kleinste Detail achten können. Sie müssen an eine Aufgabe abstrakt und praktisch herangehen können. Leidenschaft für ein niemals abgeschlossenes Lernen, Geduld sowie Hartnäckigkeit gehören ebenfalls unbedingt zu den Eigenschaften, die gute Programmierer auszeichnen. Jede leistungsfähige Anwendung wird geplant und gestaltet. Ohne ein professionelles Softwaredesign enden viele Projekte häufig schon bevor sie richtig begonnen haben. Die größte Schwierigkeit beim Entwickeln eines Computerspiels oder einer Simulation ist die Bändigung der Komplexität.
Objektorientierte Programmierung ist eine faszinierende Art und Weise der modernen Softwareentwicklung. Es ist eine revolutionäre Idee, eine neue Denkweise und ein umfassendes kreatives Konzept. Mit der Verbreitung objektorientierter Programmiersprachen wie Smalltalk, CPP, Java, Python und vielen anderen erfolgte ein Paradigmenwechsel. Programmierer, die diese Sprachen einsetzen, gehen ihre Arbeit völlig anders an als ihre Vorgängergeneration. So besteht beispielsweise zwischen der älteren imperativen Programmiersprache C und der neuen Sprache CPP ein fundamentaler Unterschied. Es werden andere Konzepte für das Softwaredesign verwendet, ein Programm wird völlig anders strukturiert und organisiert.
Das kreative Individuum
Objektorientierte Computersprachen wie das klassische Smalltalk stellen das kreative Potenzial des Individuums in den Mittelpunkt. Nicht der Mensch soll sich mit seinen Fähigkeiten an ein Computersystem anpassen, sondern der Computer soll eine Sprache sprechen, die den kreativen Geist des Programmierers unterstützt. Für Smalltalk wurde das Prinzip aufgestellt, dass ein System nur dann den kreativen Geist fördern kann, wenn es komplett und in allen Einzelheiten von einem Individuum verstanden werden kann. Ein fertiges großes Computerprogramm kann heute von keinem einzelnen Menschen mehr überblickt und verstanden werden. Gebaut werden können diese unglaublich abstrakten Gebilde mit der objektorientierten Methode. Dabei wird das Ganze aus lauter kleinen, überschaubaren Einheiten zusammengestellt. Klassen und Objekte sind die wichtigsten Konzepte der objektorientierten Programmierung. Aus ihnen wird alles gestaltet. Diese Elemente werden einzeln geplant und liebevoll gestaltet. Zusammen ergeben sie eine Computeranwendung, die zur Laufzeit als dynamisches Softwaregebilde zuverlässig Aufgaben erfüllt.
Das Objektorientierte Design
Das Design eines objektorientierten Computerprogramms ist vergleichbar mit der Organisation einer Gemeinschaft von Individuen. Jedes Objekt der Gemeinschaft bekommt eine bestimmte Verantwortung für seinen Bereich zugewiesen. Die Gruppe aller Objekte arbeitet auf ein gemeinsames Ziel hin. Je unabhängiger die einzelnen Objekte der Gemeinschaft aufgestellt sind, desto flexibler ist das Gebilde. Ein Objekt, das von keinem anderen abhängt, kann problemlos erweitert oder ausgetauscht werden. Objektorientiertes Design heißt auch, dass Komponenten beziehungsweise Gruppen benachbarter Objekte möglichst selbstständig existieren können. Diese Komponenten können dann auch unabhängig vom Gesamtprogramm entwickelt und gestestet werden.
Design ist ein Prozess. Sämtliche Anforderungen an die Software werden in ein Modell transformiert, das wiederum als Software implementiert werden kann. Das Produkt des Designprozesses ist also zunächst ein Modell.
Das Design sollte auf Veränderungen hin ausgerichtet sein. Die wichtigsten Design-Ziele sind laut Stroustrup: Flexibilität, Erweiterbarkeit und Portabilität. Bereiche des Systems, die sich voraussichtlich ändern werden, sollten identifiziert und eingekapselt werden. So können sie später vom Programmierer leichter gefunden und angepasst werden.
Ein Design-Dokument enthält mindestens zwei Bereiche: die Beschreibung des Klassen-Designs und die Beschreibung der Architektur. Beim Klassen-Design geht es zum einen um das statische Design, in dem die verschiedenen Klassen mit ihren Beziehungen und Charakteristiken im Detail beschrieben werden. Zum anderen zeigt das dynamische Design, wie die Klassen miteinander zusammen arbeiten (interagieren).
Ein Universum verantwortlicher Objekte
Ein objektorientiertes Programm besteht aus einer Gemeinschaft von Objekten. Diese Objekte interagieren miteinander, indem sie Nachrichten austauschen. Jedes Objekt erfüllt eine spezifische Rolle in dem Programm. Je nach der zugeteilten Aufgabe kann ein Objekt zum Beispiel eine Aktion ausführen, Kontakt zu einem anderen Objekt ermöglichen oder auch nur die eigenen Daten zur Verfügung stellen. Die Mitglieder der Community verfügen über genau festgelegte Verantwortlichkeiten. Manche Objekte können nur innerhalb ihrer direkten Nachbarn aktiv werden, andere können mit weiter weg gelegenen Objekt-Gemeinschaften zusammen arbeiten. Jedes Objekt hat ein eigenes Leben, es wird geboren, verfügt über ein Gedächtnis und kann sich entwickeln. Es erfüllt seinen Job und kann eigene Entscheidungen treffen. Und wenn es keiner mehr braucht, stirbt es. Innerhalb der Community leben nur gut ausgebildete und hilfsbereite Objekte, die sich in freundlichem Sinne ihre Wünsche gegenseitig mitteilen. Möchte das eine Objekt vom anderen etwas bekommen und fragt nach den vereinbarten Regeln artig beim anderen Objekt an, dann wird der Wunsch immer erfüllt werden. Zwischen den Objekten fließen die Nachrichten hin und her, es ist das Einzige, was außerhalb der Objekte in einem Programm stattfindet.
Jede Mitteilung im Programm läuft von einem Sender-Objekt zu einem Empfänger-Objekt. In der Regel wird der Empfänger erst zur Laufzeit des Programms bestimmt. Es findet daher eine späte Bindung zwischen der Mitteilung von dem einen Objekt (dem Funktionsaufruf) und dem Objekt, das darauf antwortet und die Funktion erfüllt (Methode), statt.
Klassen
Die Definition neuer Klassen für eine Anwendung gehört zu den grundlegenden Aktivitäten, die jeder CPP-Programmierer unternimmt. Die Klasse ist das Schlüsselkonzept der Programmiersprache CPP und steht im Mittelpunkt des Software-Designs. Die Klasse ist ein Datentyp, der vom Entwickler selbst festgelegt wird. Der Typ bestimmt, wie sich Objekte, die zu der Klasse gehören, verhalten. Der Typ bestimmt, wie die Objekte ins Leben gerufen werden, wie man mit ihnen umgehen kann und wie sie am Ende ihres Lebens wieder zerstört werden. Laut Bjarne Stroustrup ist der Schlüssel für die Entwicklung guter Programme das Design der Klassen. Jede Klasse sollte so gestaltet sein, dass sie ein wesentliches Ziel verfolgt. Die ideale Klasse ist so unabhängig wie möglich vom Rest der Welt. Ihr Interface sollte minimale Informationen über ihr Innenleben nach außen geben.
Klassen existieren nicht als isolierte Einzelwesen. Daher ist es nicht sinnvoll, einzelne Klassen zu gestalten. Es ist besser, sich konzeptionell mit einer Gruppe von Klassen zu beschäftigen, die logisch zueinander gehören. Solche Klassen-Gebilde können später eine abgeschlossene Kompente einer Software ergeben oder eine Klassen-Bibliothek. Die Komponente und nicht die einzelne Klasse sind die Einheit, mit der designt werden sollte.
Schritte beim Klassen-Design:
1. Finde die Klassen und beschreibe die wichtigsten Beziehungen
Nicht alle Klassen gehören konzeptionell in die Sphäre der Anwendung. Beispielsweise können sie auch beispielsweise Systemressourcen oder Implementations-abhängige Abstraktionen repräsentieren.
2. Verfeinere die Klassen
Wie sollen Objekte angelegt, kopiert und zerstört werden? (Konstruktoren, Kopier-Konstruktur, Destruktor)
Definiere die ersten Operationen, die zur Klasse gehören. Dies werden später die Member-Funktionen.
Überlege, welche Operationen virtuell sein sollen. Das sind diejenigen Operationen, für die die Klasse dann lediglich als Interface für andere abgeleitete Klassen agiert.
3. Verfeinere die Klassen weiter
Die Verantwortlichkeit einer Klasse - ihr Job - muss nicht heißen, dass die Klasse selbst oder ihre Memberfunktionen alle Arbeiten direkt ausführt. Es ist häufig sinnvoller, den direkten Arbeitsbereich einzugrenzen, klar zu umreissen und für spezielle untergeordnete Aufgaben andere Klassen zu beauftragen.
4. Lege die Schnittstellen fest
Im Stadium des Designs geht es noch nicht um die Einzelheiten der privaten Memberfunktionen einer Klasse. Bei der Festlegung der Interfaces sollte darauf geachtet werden, dass diese möglichst unabhängig von der Implementation der Klassen sind. Ziel ist es, dass die Schnittstellen bei einer späteren Änderung einer Klassenimplementation gleich bleiben. So können spätere Änderungen im Inneren der Klasse erfolgen, ohne dass andere Bereiche außerhalb der Klasse betroffen sind. Auch lassen sich Klassen oder Komponenten beliebig austauschen, wenn die Schnittstellen jeweils gleich bleiben. Interfaces dienen auch dazu, die Komplexität der hinter ihnen liegenden Implentationen zu verbergen, damit das Design auf einer höheren Ebene stattfinden kann. Das Denken in Schnittstellen und Komponenten abstrahiert von der Implementation einer Klasse, in der es zu einem späteren Zeitpunkt um die Details geht.
Rollen von Klassen
Klassen bilden nicht nur wichtigen Dinge ab, die aus dem Zweck einer Anwendung hervorgehen. Im Blick auf ein Computerspiel können beispielsweise Elemente wie Monster, Waffen, Schätze, Spielerpunkte und vieles mehr als eine Klasse definiert werden. In allen diesen Fällen handelt es sich um eine konkrete Klasse, da es um eine Generalisierung von handfesten Anwendungsinhalten geht. Viele Klassen übernehmen völlig andere Rollen.
Konkrete Klassen
Bei konkreten Klassen repräsentiert die Klasse ein eindimensionales Konzept. Es besteht eine direkte Beziehung zwischen den in der Klasse festgelegten Eigenschaften und den Objekten, die von der Klasse erzeugt werden können. Das Interface führt ohne Umwege zur Implementation der Klasse, es besteht eine Eins-zu-Eins-Beziehung zwischen Interface und Implementation. Typischerweise sind konkrete Klassen nicht in Klassenhierarchien eingebunden, sondern sie werden an den Enden der Ableitungsbeziehungen angesiedelt. Das Konzept jeder konkrete Klasse sollte leicht zugänglich, da aus dieser Klasse direkt die Objekte erzeugt werden.
Konkrete Klassen sollten nicht zu komplex sein, damit ihr Zweck leicht verständlich ist. Sie sollten so wenig wie möglich von anderen Klassen abhängig sein, so dass sie auch in Isolation ihre Aufgabe erfüllen können. Aufgrund der direkten Verbindung zwischen Interface und Implementation der konkreten Klassen lassen sich diese Gebilde häufig nicht ohne Auswirkung auf ihre Umwelt verändern. Neuer Code in diesen Klassen erfordert, dass das Programm neu kompiliert werden muss. Konkrete Klassen eignen sich nur in seltenen Fällen als Ausgangspunkt, von dem weitere Klassenbeziehungen abgeleitet werden. Sie sollten eher als einzelnes Konzept betrachtet werden und im Design möglichst wenig Verbindungen eingehen.
Abstrakte Klassen
Abstrakte Klassen werden häufig als Schnittstelle benutzt, von der konkrete Klassen abgeleitet werden. Dadurch lässt sich die Implementation vor dem Benutzer einer Klasse verbergen und die abgeleiteten Klassen lassen sich später leicht austauschen, ohne dass die Komponente insgesamt beeinflusst wird. Abstrakte Klassen haben normalerweise keine Konstruktoren.
Wenn absehbar ist, dass sich die Art der Objekte, die aus einer Klasse gebildet werden, ändern können, dann sollten abstrakte Klassen verwendet werden.
Knoten-Klassen
Eine Knoten-Klasse befindet sich typischerweise an einer wichtigen Stelle innerhalb einer Klassen-Hierarchie. Sie bietet mehr als ein Interface, von dem andere Klassen abgeleitet werden. Knoten-Klassen haben im Unterschied zu abstrakten Klassen in der Regel Konstruktoren. So können von einer Knoten-Klasse aus Implementationen abgeleitet werden, die über mehr Eigenschaften verfügen als die übergeordnete Klasse.
Ein Framework besteht typischerweise aus einer Hierarchie von Knoten-Klassen.
Action-Klassen
"Do-It"-Klassen, deren Hauptzweck darin besteht, eine Aktion auszulösen, enthalten oftmals nur eine virtuale Funktion. Ein Benutzer der Klasse benötigt keinerlei Informationen darüber, wie die einzelne Aktion am Ende ausgeführt wird. Es findet eine Entkoppelung zwischen dem Aufruf der Aktion und der Abarbeitung in der Implementation statt.
Interface-Klassen
Eine Interface-Klasse dient nicht dazu, selbst Aktionen auszuführen. Sie kommt vielmehr als Schnittstelle zwischen verschiedenen Code-Bestandteilen zum Einsatz. Das kann soweit gehen, dass Interfaces die Verbindung zwischen CPP-Code und Nicht-CPP-Code schaffen. Auch externe Bibliotheken wie Grafik-APIs (Application Programming Interfaces) werden über diese Art von Klassen in eigene Programme eingebunden. Eine Interface-Klasse, die beispielsweise den Zugang zu anderen Klassen-Bibliotheken schafft, in dem es die Schnittstellen miteinander kompatibel macht, wird manchmal auch Wrapper genannt.
Griff-Klassen
Die String-Klasse ist ein Beispiel für eine Handle-Klasse. Der Griff (Handle) stellt die Schnittstelle zur Verfügung, über die das repräsentierte Objekt mit seinen spezifischen Zustandsdaten angesprochen werden kann. Die Verbindung zwischen Handle und dem Objektzustand wird in der Regel über einen Pointer in der Handle-Klasse hergestellt. Die Klasse enthält neben dem Pointer oftmals nicht viel mehr.
Durch die Verwendung von Handle-Klassen lässt sich die direkte Nutzung von Pointern und Referenzen vermeiden.
Beziehungen zwischen Klassen
Die Gestaltung der Beziehungen zwischen den Klassen eines Programms ist manchmal schwieriger als der erste Entwurf der einzelnen Klassen selber. Im Mittelpunkt des Designs stehen Komponenten, das sind Gebilde von einzelnen oder mehreren Klassen. Klassen können in einer Vererbungshierarchie miteinander verbunden sein. Der Dalmatiner ist ein Hund. Dieses Beispiel zeigt eine "Ist-Ein"-Beziehung. Bei der Aggregation ist hingegen ist eine Klasse Teil einer anderen Klasse. Der Dalmatiner hat ein Dalmatinerohr. In dieser Betrachtungsweise wird eine Beziehung mit der Zuordnung "Hat-Ein" beschrieben. Aggregationen, also Zusammenstellungen von Eigenschaften zu einem größeren Etwas, sind in der Regel flexibler zu gestalten, das sie nicht auf direkter Vererbung aufbauen. Die einzelnen Elemente können zur Laufzeit des Programm dynamisch bestimmt werden. Im Unterschied dazu werden Vererbungsbeziehungen zwischen Klassen immer beim Kompilieren des Programms festgelegt, sie sind damit statisch festgeschrieben.
Objekte
Ein Objekt ist ideal, wenn es lebendig, verantwortlich und intelligent ist. Jedes Objekt ist eine unabhängige Einheit mit einer individuellen Lebensspanne. Objekte werden geboren (vom Konstruktor erzeugt), beginnen ein produktives Leben, in dem sie Aufgaben, die an sie gestellt werden, erfüllen, und werden automatisch wieder zerstört (Destruktor). Solange Objekte am Leben sind, haben sie eigene Entscheidungsbefugnisse und klar definierte Verantwortlichkeiten. Ein Objekt ist für eine fest umrissene Rolle zuständig und kann Aufgaben im Rahmen seiner Pflichterfüllung an andere Objekte delegieren. Das Wissen darüber, wie ein Objekt seine Aufgabe erfüllt, befindet sich innerhalb des Objektes und ist nicht von außen zugänglich. Das ideale Objekt kümmert sich um seinen eigenen Zustand. Es verändert diesen Zustand ausschließlich mit eigenen Methoden. Wer von dem Objekt etwas will, kann es über ein festgelegtes Protokoll darum bitten. Das Objekt versteht nur die Mitteilungen, die so formuliert sind, dass sie dem Protokoll (der Schnittstelle) entsprechen. Zu den wünschenswerten Eigenschaften eines guten Objektes gehört, dass es sein Inneres nicht nach außen öffnet. Von seiner Intelligenz und Komplexität weiß nur das Objekt selbst etwas, es kapselt sich vor der Öffentlichkeit mutwillig ab und schützt damit die sensiblen Bereiche, die sein Selbst ausmachen.
Design Pattern
Design Pattern (Entwurfsmuster) beschreiben eine bewährte Art und Weise der Interaktion zwischen den verschiedenen Mitgliedern einer Objekt-Community. Es sind in der Praxis vielfach bewährte Designlösungen für Probleme, auf die Software-Entwickler in verschiedenen Bereichen immer wieder treffen. Wer Design Pattern kennt und diese einsetzt, nutzt wertvolles Erfahrungswissen. Das Software-Design wird mit den Entwurfsmustern effizienter. Es findet auf einer höheren Ebene statt.
Notizen
Beispiel: Die Architektur für das Spiel Ankh setzt sich aus 60 CPP Klassen zusammen. Außerdem nutzt es unter anderem Komponenten von Boost und Directx-Bibliotheken.
Kristen Nygaard (1926-2002): Der norwegische Mathematiker entwickelte zusammen mit Ole-Johan Dahl von 1961 bis 1965 die Programmiersprache SIMULA und legte damit den Grundstein für die Objektorientierte Programmierung (OOP).
Stroustrup-Zitate:
"Es gibt nur einen grundlegenden Weg, um mit der Komplexität fertig zu werden: Teile und Herrsche!"
"Design und Programmierung sind menschliche Tätigkeiten. Wenn Du das vergisst, ist alles verloren."
"Softwareentwicklung ist ein Prozess, der keinen Anfang und kein Ende hat."
"Es gibt keine untere Grenze bei der Größe eine Computerprogrammes, ab der sich das Designen vor dem Start des Codens lohnt."
Softwareentwicklung ist ein iterativer und inkrementeller Prozess in drei Stufen: Analyse - Design - Implementation
CategoryCplusplus
There are no comments on this page. [Add comment]