Claus SchönleberWhat the heck are Objects? - Part IObjektorientierte Programmierung, Basics - oder: OOP in 120 Minuten |
||||||
|
[ Work | Home ] | |||||
Inhalt
Die Beispiele sind in C++. In einem späteren Text mag es dann auch Java-Beispiele geben. Vorkenntnisse: Es sollten gute Kenntnisse in einer prozeduralen Programmiersprache (z.B. C oder Pascal) vorhanden sein. |
Objekte?Ein Begriff aus den 80er Jahren ist inzwischen in aller Munde und in vielen Quelltexten, manchmal korrekt, manchmal vermurkst: Objektorientierte Programmierung. Natürlich hört man, wenn man die Prinzipien jemandem erklärt, oft die Bermerkung: "Wozu das? Braucht man doch gar nicht! Ging und geht doch auch so!" Ja, stimmt. Aber mit derselben Begründung könnte man noch in Maschinensprache programmieren, oder, um mit geläufigeren Bildern zu sprechen, noch mit der Pferdekutsche reisen. Ging doch auch früher. Und das ist kein dummes Argument. Es ging und geht ja tatsächlich. Trotzdem gibt es natürlich Entwicklungen, Modernisierungen, neue Erkenntnisse. Und die mündeten eben in einem neuen Paradigma, einem Modell, das statt des maschinenhaften Vorgehens eines prozeduralen Ansatzes die Vorstellung realisierte, dass in der wirklichen Welt auch keine Maschinen Abläufe abarbeiten, sondern Objekte durch Kommunikation miteinander interagieren. Damit sollte eine größere Komlpexität erreicht werden. Allerdings muß man hier streng unterscheiden zwischen Sprachen, die objektorientierte Methoden anbieten und echt objektorientierten Sprachen. Es ist möglich, in jeder Programmiersprache objektorientiert zu programmieren. Es ist halt nur mehr oder weniger aufwendig. Für Neugierige ganz grob erklärt: Man definiere einen record (in C: struct) und wenn man während der Laufzeit einen solchen braucht, erzeuge man einen Zeiger (pointer) auf einen solchen record-Typ. Man muss dann noch in einer gewissen Weise mit den zugehörigen Funktionen und Prozeduren umgehen, aber im Prinzip war´s das (auch das läßt sich mit Zeigern auf die Funktionen und Prozeduren lösen).
C++ ist dabei die eher hemdsärmeligere Sprache, Java die hohe Theorie. Naja, das ist etwas lax ausgedrückt, aber es trifft den Kern. Um es kurz zu umreißen: Der objektorientierte Ansatz funktioniert recht gut solange ein Programm läuft. Aber wie startet man sowas? Jeder, der gerade Java lernt und schon mal C oder Pascal gesehen hat, kennt das ziemlich dumme Gefühl: "Wie starte ich das blöde Ding?". Und das ist nicht der einzige Haken. Jedesmal, wenn in einem der einschlägigen Java-Bücher der Vergleich zu C++ betrieben wird, liest man Sätze wie "Während in C++ ... zulässig ist, geht das in Java natürlich nicht, weil das kein sauberer, objektorientierter Ansatz ist." Und dann liest man eine Fußnote im Sinne von: "Aber wenn Sie´s doch mal brauchen, geht das schon, es heißt nur anders." Und meistens ist es dann etwas umständlicher. Das beweist, daß des objektorientierte Modell ein sehr gutes Modell ist, aber die Welt, die dadurch abgebildet werden soll, eben nicht so rein und ordentlich ist, wie es das Modell voraussetzt. Aus diesem Grunde fühlen sich Praktiker auch oftmals in C++ wohler, da kann man halt auch mal schweinern, wenn man´s unbedingt braucht. Wozu also?Wo aber sitzt denn nun das schneidende Argument, das wirklich überzeugt? Die Begründung findet sich nicht in den Hobby-PCs oder in den kleinen Softwarehäusern, die kleine und mittlere Programme schreiben. Erst ab einer gewissen Komplexität zeigt die OOP (objektorientierte Programmierung) ihre Stärken. Stellen wir uns ein Projekt vor, das insgesamt einen Umfang von 50.000 Zeilen oder noch mehr: 200.000 Zeilen hat. Das schafft auch nicht mehr ein Programmierer, dazu braucht man eine Truppe. Oftmals sitzen die noch nicht mal an einem Ort, sondern in der Welt verstreut. Worauf muß man in so einem Falle achten?
Wir fassen zusammen: OOP ist notwendig wie ein Feuerlöscher. Solange nichts passiert, stört das blöde Ding. Aber wehe es brennt, und keiner ist da! OOP hat sicherlich auch Schwächen, aber es ist mindestens eine gute Art und Weise, sich die dümmsten Probleme bei der Programmierung vom Hals zu halten. Wir brauchen aber eine grundlegende Definition, naja, eigentlich vier... Es soll David Flannagan zitiert werden mit seinen unsterblichen Worten: "An Object is an Instance of a Class." Nun könnten wir aufhören, das ist der Kern der Objektorientierten Programmierung. Nein, im Ernst, zumindest ist es ein guter Merksatz und er enthält viel Wahrheit.
Und was ist eine Entität? Na, irgendwas eben, etwas "seiendes". Während der Begriff "Instanz" eher mit der Existenz zu tun hat, beschreibt das "Objekt" die Eigenschaften und die Aktionen eines Individuums. Und schon habe ich Ihnen die beiden Grundelemente einer Klasse untergejubelt:
Und so ein Ding kreieren wir jetzt. Das erste ObjektMein Lieblingsbeispiel ist aus einem "KnowWare"-Buch: Die Dackelverwaltung. Einerseits vergißt man dieses Beispiel nie, andrerseits kann man daran wunderbar deutlich machen, was Objekte sind. Dazu taugen die Superbeispiele aus den Hyperbüchern eher nicht. Aber lesen Sie die trotzdem dann nach den ersten Versuchen. Es soll eine Klasse "Dackel" definiert werden. Einem Dackel kann man die Eigenschaften "Alter" und "Name" zuordnen. Ok, das gleiche gilt auch für Menschen oder Autos, aber es soll hier erst mal ausreichen: class Dackel { int Alter; // Attribut "Alter" string name; // Attribut "name" }; // end of class Dackel Hmm. Sieht doch aus wie ein C/C++-struct, oder? Richtig! Und das ist es auch. Wir werden gleich nochmal darauf zurückkommen. Ein Programm, das die wichtigsten Funktionalitäten enthält, sieht so aus: /*** Das Programm wird so nicht laufen. Es sind noch kleine Änderungen nötig. ***/ /* Headerdatien für Ein-/Ausgabe und Stringverarbeitung einbinden.*/ #include <iostream> Das ist aber noch nicht alles. Bisher haben wir nämlich nichts gewonnen. Das hätten wir auch in C oder Pascal machen können. KapselungUm nun die Struktur wirklich schützen zu können - vor dem Programmierer selbst und seinen Kollegen, die dem Code wissentlich oder unwissentlich Ungereimtheiten zufügen wollen, muß nun alles gekapselt werden. Hier gilt wieder ein altes Prinzip:
Um die gewünschte Kapselung zu erreichen, muß man nun selbst etwas tun, nämlich einer Konvention folgen. Das ist immer unbequem. Aber bequem soll man es zu Hause haben, auf der Arbeit soll man gute Qualität liefern, und das heißt Schweiß zu vergießen. Was ich bisher nämlich verschwiegen habe, ist die Tatsache, daß man in einer Klasse einstellen kann, ob die Elemente (Attribut oder Methode) von "außen" sichtbar sind oder nicht. Um es kurz zu machen, hier ein paar goldene Regeln, die es lohnt einzuhalten:
Es gibt mehrere Arten von Methoden. Wir werden hier nicht alle besprechen, nur die wichtigsten. ZugriffsmethodenDie wichtigsten Methoden sind die Methoden, welche das Hauptwerkzeug der Kapselung sind. Man nennt sie Zugriffsmethoden oder access methods. Um Attribute zu verstecken, muß man sie als solche kennzeichnen. Dies wird mit den Wörtern "public" oder "private" gemacht. Das sind nicht die einzigen, aber die einzigen, die hier besprochen werden sollen. Es gibt wunderbare weiterführende Texte, aus denen man die Tiefen der Objektorientierung lernen kann. Die Klasse wird nun erst mal ergänzt: class Dackel { private: int Alter; string name; }; // end of class Dackel Damit wird dem Compiler angezeigt, daß auf die Attribute nicht von "außen" zugegriffen werden darf. In einer class sind alle Attribute nämlich als private voreingestellt. Dazu gleich mehr. Und das ist auch der Grund, warum der gerade angegebene Quelltext nicht übersetzt werden kann: Spätestens bei der Zeile
würde der Compiler mit einer Fehlermeldung abbrechen (z.B. ´´Kein Zugriff auf private Element, dessen Deklaration in der Klasse "Dackel" erfolgte´´; MS-Visual C++ Compiler). Somit haben wir diesen Teil der Struktur gekapselt. Und wie kommen wir an die Datenfelder nun heran? Die können ja nicht einfach leer bleiben! Dazu benötigen wir Methoden, mit deren Hilfe wir die Attribute manipulieren können. Nein nein, kein Argument "das geht mit einer Zuweisung doch einfacher!"! Das ist es ja gerade, was wir verhindern wollen! Wer´s noch nicht begriffen hat, halte hier mit Lesen an und wiederhole den Abschnitt "Wozu also?" weiter oben! Für jedes private-Attribut werden genau zwei Methoden benötigt: Eine zum Auslesen des Wertes (get) und eine zum Zuweisen eines Wertes (set). Man ergänzt den Methodennamen dann um den Attributnamen. Hier sehen wir auch gleichzeitig, wo Methoden überhaupt untergebracht werden, nämlich in der Klassendefinition: Ergänzen wir den Quelltext um die zwei nötigen access methods: class Dackel { public: void setAlter (int age) Methoden werden wie normale C-Funktionen formuliert. Neu ist der Teil "public". Er ist von außen zugreifbar, also beispielsweise aus dem Hauptprogramm "main()". Wie werden Methoden aufgerufen? Wie die Attribute. Man schreibt den Variablennamen des Objekts, dann einen Punkt und dann den Methodenaufruf. Also zum Beispiel:
Das Geheimnis von vorhin lüften wir jetzt! Es wurde vorhin ja festgestellt, daß Klassen - in C++ - eigentlich erweiterte "struct"s sind und erst mal als private vorgegeben werden. Nun merken wir uns (für C++):
Deswegen kann man z.B. in einer Klassendefinition das "private" weglassen. Ich empfehle aber dringend, es trotzdem hinzuschreiben. Erstens ist es nicht falsch und zweitens macht es die ganze Sache übersichtlicher. Und das ist ja der Grund, warum man solche Hochsprachen verwendet. Will man es unübersichtlich, braucht man kein C oder C++ oder Java, dann kann man Fortran, Basic oder Assembler nehmen. Es fällt auf, daß die access methods einem Muster folgen:
Also formulieren wir ein Programm mit der Erweiterung: class Dackel { public: void setAlter (int age) Sieht umständlich aus, ist es auch, aber es ist notwendig, und einer der Gründe, warum man überhaupt diese Programmiermethode eingeführt hat (siehe "Wozu also?"). Andere MethodenMethoden werden natürlich auch zur algorithmischen Manipulation von Objekten eingesetzt. Und schließlich gibt es noch spezielle Methoden, mit denen Objekte eingerichtet werden. In diesem Abschnitt machen wir gleich beides. Die erste Variante soll an der Eigenschaft von Dackeln zu bellen gezeigt werden. Die zweite Variante sind sogenannte Kontruktoren. Wenn nämlich ein Objekt erzeugt wird, will man normalerweise die Attribute mit Inhalten füllen, eventuell eine Datei öffnen oder andere Dinge vorbereiten. In der klassischen prozeduralen Programmierweise nennt man das "Initialisieren". Oft vergißt man das aber erst mal, und so gibt es in der OOP den Mechanismus des constructors. Diese Methode (es können auch mehrere sein) wird zwingend automatisch bei der Erzeugung eines Objekts einmal aufgerufen. Man kann dieser Methode über Parameter bestimmte Initialwerte übergeben und so dafür sorgen, daß das Objekt in einem sauber definierten Zustand das Licht der Welt erblickt. Wenn das Objekt nicht mehr gebraucht wird, "stirbt" es, es wird wieder aus dem Arbeitsspeicher entfernt. Java macht das über eine garbage collection, einen Prozeß, der unabhängig von äußeren Einflüssen ab und zu nach Objekten sucht, auf die keine Referenz mehr existiert und diese dann "löscht". C++ überläßt das (wie in der C-Tradition üblich) dem Programmierer. Die dazu nötige Methode heißt dann sinnträchtig destructor. Wir werden nun also drei Methoden hinzufügen:
Dabei ist zu beachten, daß die Konstruktoren denselben Namen besitzen müssen wie die Klasse! Daran erkennt man diese Methoden übrigens auch. Wichtig dabei ist, daß es immer einen Konstruktor gibt. Hat man nämlich keinen eigenen Konstruktor definiert, so wird der Standardkonstruktor ausgeführt. Er hat keine Parameter und keine Anweisungen. Klingt ziemlich schwachsinnig, aber man tut das aus zwei Gründen:
Ohne Konstruktor gibt´s nämlich gar keine Objekte. Definiert man nun einen eigenen Konstruktor, so wird der Standardkonstruktor nicht mehr angeboten, statt dessen werden der oder die eigenen Konstruktoren benutzt. Will man also weiterhin ein Objekt parameterlos deklarieren können, muß man den Standardkonstruktor nun selbst einbauen. Dies wird im Beispiel auch getan. Wie unterscheidet der Compiler aber die Konstruktoren, die ja alle denselben Namen haben? Natürlich über die Parameterliste. Die ist nämlich verschieden. Dackel d wird ohne Parameter eingeführt - also wird der Standardparameter benutzt, das heißt ausgeführt. Dackel e benutzt einen int-Parameter. Das bedeutet, daß der zweite Konstruktor benutzt wird, der das Attribut Alter auf 5 initialisiert. Der Destruktor (nur C++) darf nur einmal definiert werden. Er ist daran erkennbar, daß er vor dem Namen eine Tilde ("~") hat. Sobald das Objekt also - wie auch immer - aus dem Speicher entfernt wird, kommt der Destruktor zur Anwendung. Aufrufen kann man ihn selbst nicht. Was im Beispiel im einzelnen passiert, wird später im Teil II dieses Textes besprochen. Für´s erste soll diese Erklärung ausreichen. Der Quelltext gleich als komplettes Programm: class Dackel { public: Dackel () {}; // Standardkonstruktor Damit wollen wir den Teil I beenden. Im nächsten Teil erfahren wir ein wenig über den dynamischen Einsatz von Objekten und ein paar komische Eigenschaften von Methoden. Bis dahin, enjoy oop! |
|||||
|
(c) 2005 Schoenleber.com |