Teil 3 – Ojektorientierte Programmentwicklung
Teil 3 – Ojektorientierte Programmentwicklung
Die objektorientierte Programmierung ist nach wie vor das bestimmende Paradigma bei der Anwendungserstellung. Klassen und Objekte, Methoden und Eigenschaften, Generalisierung und Spezialisierung sind die Schlagwörter dieses Ansatzes. Einmal vollständig verstanden, ist das Wissen sprach- und problemneutral anwendbar.
Im dritten Teil unserer Artikelserie steht die objektorientierte Programmierung im Mittelpunkt (Kasten: „Artikelserie“). Objektorientierung gilt bereits seit längerer Zeit als das übliche Vorgehen bei der Programmentwicklung. Das gilt für alle Anwendungstypen, insbesondere aber für Geschäftsanwendungen. Nahezu alle modernen Programmiersprachen unterstützen die wesentlichen Charakteristika dieses Ansatzes. Die sichere Beherrschung dieses Programmierparadigmas kann daher als Basisqualifikation für angehende Entwickler angesehen werden. Der Artikel gibt zunächst einen Überblick über die grundlegende Idee und die Entwicklung der objektorientierten Programmierung. Mit diesem Verständnis fällt es dann leichter, sich mit den Details zu beschäftigen. Da man am besten anhand einfacher Beispiele lernt, betrachten wir die Umsetzung sogleich in der Programmiersprache C#. Eine Übungsaufgabe wird diesen Teil der Artikelserie abschließen.
Teil 1: Einführung: Programmentwicklung, Sprachen, Entwicklungsumgebung
Teil 2: Basics: Variablen, Datentypen, Ablaufstrukturen, Algorithmen
Teil 3: Objektorientierung: Klassen, Eigenschaften, Methoden, Ereignisse, Vererbung
Teil 4: User Interface: Design aus technischer Perspektive
Teil 5: Architektur: Anwendungsschichten und Kopplung, Model-View-Controller-Muster
Teil 6: Daten: Datenbanktheorie und Datenmodellierung
Die Anfänge der Programmentwicklung waren durch ein intuitives „Drauflosprogrammieren“ gekennzeichnet. So lassen sich natürlich heute in keiner Weise mehr die Anforderungen an die Erstellung von hochkomplexer und möglichst fehlerfreier Software erfüllen. Die Art und Weise der Softwareentwicklung hat sich im Laufe der Zeit zu einem ingenieurmäßigen Vorgehen entwickelt. Vorgehensmodelle beschreiben, in welcher zeitlichen Reihenfolge die einzelnen Schritte des Entwicklungsprozesses durchlaufen werden; Methoden des Projektmanagements sorgen auf höherer Ebene dafür, dass der gesamte Prozess weitgehend planbar ist. Moderne Werkzeuge unterstützen die Entwickler in jeder Phase dieses Vorhabens. Eine sehr wichtige Frage bei der Programmierung ist die Art und Weise wie eine reale Aufgabenstellung in Software abgebildet und damit bearbeitet wird. Dabei kann ein Programm als ein Modell der realen Welt aufgefasst werden. Je nach Problem existieren unterschiedliche Ansätze zum Aufbau dieses Modells. Sie werden als Programmierparadigmen bezeichnet. Es wird zwischen den folgenden Paradigmen unterschieden:
Computerprogramme können grundsätzlich nach unterschiedlichen Paradigmen aufgebaut werden. Ein „falsch“ oder „richtig“ gibt es dabei nicht. Vielmehr kann man lediglich eine mehr oder weniger gute Eignung des einen oder anderen Ansatzes feststellen. Moderne Programmiersprachen unterstützen meist auch nicht nur ein Paradigma, sondern es können mehrere Ansätze miteinander kombiniert werden. Dazu ein Beispiel: Die Sprache C# ist vom Wesen her eine objektorientierte Programmiersprache. Dennoch ist bekannt, dass sich bestimmte (Teil-)Aufgaben einer Programmieraufgabe – beispielweise stark mathematisch orientierte Probleme – eleganter unter Anwendung funktionaler Aspekte lösen lassen. Dem Programmierer stehen daher innerhalb der Sprache C# auch Elemente der funktionalen Programmentwicklung zur Verfügung. Ob er sie auch tatsächlich nutzt, bleibt seine individuelle Entscheidung, denn grundsätzlich würde man auch zur Lösung gelangen, wenn man ausschließlich die objektorientierte Programmierung anwendet. Entscheidet man sich dagegen für eine Implementierung in F# (eine funktionale Programmiersprache), so kann man bei Bedarf auch auf objektorientierte Konzepte zurückgreifen, denn sie sind auch in F# vorhanden, auch wenn das nicht der Schwerpunkt dieser Sprache ist.
Insgesamt kann man aus heutiger Perspektive jedoch sagen, dass sich die objektorientierte Programmierung seit Langem etabliert hat und heute als State of the Art in vielen Bereichen der Softwareentwicklung gilt.
Die objektorientierte Programmierung basiert darauf, die Software in Anlehnung an die Gegebenheiten der realen Welt als Objekte aufzufassen. Solche Objekte verfügen über Eigenschaften, die sie näher charakterisieren. Ebenso können Objekte bestimmte Tätigkeiten ausführen. Sie können miteinander kommunizieren und in einer bestimmten Beziehung zueinander stehen. Beispielsweise können ähnliche Objekte einen gleichen Bauplan aufweisen. Zum vereinfachten Einstieg betrachten wir Abbildung 1.
Ein Automobil ist je nach Sichtweise ein mehr oder weniger komplexes Objekt, das sich beispielsweise stets durch folgende Ausprägungen (Werte) von Eigenschaften (Attribute) beschreiben lässt:
Diese Eigenschaften repräsentieren also die Daten des Objekts. Ebenso verfügt ein Auto stets über bestimmte Fähigkeiten, d. h., es kann bestimmte Funktionen/Aufgaben ausführen. Sie werden mit Blick auf die objektorientierte Programmierung als Methoden bezeichnet. Mit Bezug auf das Beispiel sind folgende Fähigkeiten (Methoden) von Interesse:
Unter Objekten versteht man nunmehr die Zusammenfassung von Daten und Funktionen. Dabei gehört ein konkretes Objekt zu einer Klasse. Eine Klasse wiederum ist der Bauplan für die Objekte. Mit Blick auf die Abbildung haben wir eine allgemeine (abstrakte) Klasse Auto vorliegen. Konkretisiert man ein bestimmtes Auto anhand seiner spezifischen Werte, so gelangt man zum Objektbegriff. Konkret: Das Fahrzeug mit dem Kennzeichen EF-LN-88 ist ein konkretes Objekt der Klasse Auto. Dabei weist es die folgenden konkreten Werte der o. g. Eigenschaften auf:
Gleichwohl verfügt auch dieses konkrete Automobil über die Fähigkeiten (Methoden) zu beschleunigen und zu bremsen. Mittels der objektorientierten Programmierung wird also versucht, die reale Welt – soweit wie das für die Lösung der anstehenden Probleme notwendig ist – möglichst exakt (1:1) abzubilden. Methoden dienen auch gleichzeitig dazu, die Werte bestimmter Eigenschaften bei Bedarf zu modifizieren. So könnte eine Methode lackieren aus dem grauen PKW das farbenfrohe blaue Modell machen (Abb. 2).
Dabei können von einer Klasse beliebig viele Objekte (Instanzen) erstellt werden, die sich jeweils in der Ausprägung der einzelnen Eigenschaftswerte unterscheiden. Ein besonderes Leistungsmerkmal der objektorientierten Programmierung ist die Möglichkeit der Vererbung. Eine Klasse kann von einer anderen Klasse abgeleitet werden und erbt dann automatisch alle Eigenschaften und Fähigkeiten dieser Mutterklasse. Neben den Eigenschaften und Methoden einer Klasse sind noch als wichtiges drittes Merkmal die Ereignisse zu nennen. Sie werden von bestimmten Objekten ausgelöst. Beispielsweise könnte ein Objekt der Klasse Auto ein Ereignis (akustisches Signal) auslösen, wenn der Benzintank fast leer ist und dieses Ereignis an ein Objekt einer anderen Klasse senden. In unserem Beispiel registriert das der Fahrer (ein Objekt der Klasse Person). Damit weiß er, dass er den PKW betanken muss. Ereignisse stellen die Grundlagen für die Interaktionsfähigkeit der Anwendung dar.
Moderne Programmiersprachen verfügen meist über die Fähigkeit, die objektorientierte Programmierung gut bis sehr gut zu unterstützen. Die Entwicklung ist dabei weder sprunghaft erfolgt, noch ist sie bereits abgeschlossen. Als Basis und Vorläufer der objektorientierten Programmierung gilt die strukturierte Programmentwicklung, deren Wesen hauptsächlich darauf beruht, dass eine komplexe Gesamtaufgabe in Teilaufgaben zerlegt wird. Wiederkehrende Teilaufgaben brauchen dann nur einmal implementiert zu werden. Eine Prozedur umfasst genau eine solche Teilaufgabe und kann von mehreren Stellen des Programms aufgerufen werden. Die Sprache Pascal gilt als Paradebeispiel der prozeduralen bzw. strukturierten Programmentwicklung. Durch diese Eigenschaften war sie auch lange Zeit als Lehr- und Ausbildungssprache etabliert. Zwischenzeitlich ist sie um objektorientierte Konzepte angereichert worden. Abbildung 3 zeigt zeitliche Meilensteine auf dem Weg der Objektorientierung anhand einiger Sprachen. Einige Programmiersprachen haben auf diesem Weg eine Erweiterung um objektorientierte Elemente erfahren (Pascal => Object Pascal); andere Sprachen wurden gleich mit ihrem ersten Entwurf auf objektorientierter Basis konzipiert (C#, Java).
Die objektorientierte Programmierung fördert insbesondere den Ansatz der Wiederverwendung. Quellcode zur Lösung allgemeiner Probleme kann für wiederkehrende ähnliche Fragestellungen zur Verfügung gestellt werden. Das geschieht in Form von Bibliotheken, die als Referenzen in Projekte eingebunden werden. Ein sehr gutes Beispiel ist das .NET Framework, das eine sehr umfassende Klassenbibliothek mit mehreren 1 000 Klassen für ein sehr breites Anwendungsspektrum bereitstellt. Eine effektive Programmentwicklung basiert auf einem bestmöglichen Zusammenwirken von Programmiersprache, Klassenbibliothek (eigene und fremde Komponenten) und einer bestmöglichen Werkzeugunterstützung (Abb. 4).
Die Konzepte der objektorientierten Programmierung sind nicht starr, d. h., sie unterliegen einer fortlaufenden Entwicklung. Auch unterstützen nicht alle Programmiersprachen sämtliche Konzepte vollständig. So wird beispielsweise die Mehrfachvererbung nur von wenigen Sprachen umgesetzt. C# erlaubt z. B., keine direkte Vererbung von mehreren Basisklassen. Die Grundpfeiler der objektorientierten Programmierung sind:
Klassen sind also die Vorlagen für konkrete Objekte. Im Regelfall werden die Klassen zur Entwurfszeit im Quellcode definiert, und zur Laufzeit müssen die konkreten Objekte erstellt werden. In den meisten Programmiersprachen werden Objekte über Konstruktoren erzeugt. Ist ein Objekt erstellt, ist i. d. R. dessen Struktur (Anzahl und Art der Eigenschaften und Methoden) fix und kann zur Laufzeit nicht mehr angepasst werden. Zur Laufzeit kann dann mit dem Objekt gearbeitet werden; zum Beispiel können dessen Methoden aufgerufen werden. Auch kann es in Beziehung zu anderen Objekten treten. Grundsätzlich haben alle Objekte eine begrenze Lebensdauer. Sie endet dann, wenn sie innerhalb des laufenden Systems nicht mehr benötigt werden.
Da einzelne Objekte – je nach Größe – einen beachtlichen Bedarf an Systemspeicher verbrauchen können, sollten nicht mehr benötigte Objekte wieder gelöscht und der von ihnen reservierte Speicher freigegeben werden. Man unterscheidet zwischen statischem und dynamischem Speicher. Der statische Speicher beherbergt Daten, die für die gesamte Laufzeit der Anwendung (des Moduls) benötigt werden. Die hier gespeicherten Daten werden zum Start des Programms erstellt und während der Laufzeit nicht gelöscht. Nur im dynamischen Speicher werden die Objekte zur Laufzeit erstellt und wieder gelöscht. Bezüglich des dynamischen Speichers wird zwischen Stack und Heap unterschieden. Der Stack wird verwendet, um lokale Variablen zu speichern. Deren Lebensdauer ist auf die Laufzeit der jeweiligen Routine beschränkt. Der Name Stack kommt daher, dass die Daten innerhalb dieses Speicherbereichs aufeinander gestapelt werden. Wichtig ist jedoch, dass die Speicherung an die Laufzeit der Routine gekoppelt ist. Eine Anwendung verfügt daher über mehrere Stackspeicher, da auch mehrere Routinen aktiv sind. Im Gegensatz dazu, können Daten auf dem so genannten Heap zu jedem Zeitpunkt der Anwendung erstellt und wieder gelöscht werden. Objekte sollten erst dann wieder vom Heap gelöscht werden, wenn sie nicht mehr benötigt werden, d. h., wenn kein aktiver Verweis mehr auf diesen Speicherbereich zeigt.
Je nach Programmiersprache ist man als Entwickler selbst für das Löschen nicht mehr benötigter Bereiche zuständig, oder das System erledigt es automatisiert. Ersteres ist aufwändig und fehleranfällig. In Object Pascal ist man beispielweise für das Löschen nicht mehr benötigter Objekte selbst verantwortlich. Die zweite Variante ist komfortabel. Ist im Zweifelsfall eine sofortige Löschung notwendig, kann sie stets manuell mit dem entsprechenden Befehl veranlasst werden. C# verwendet unter Rückgriff auf das .NET Framework eine automatisierte Bereinigung des Speicherbereichs. Das erledigt der so genannte Garbage Collector des Systems (Kasten: „Automatisierte Speicherfreigabe – der Garbage Collector“). Abbildung 5 zeigt Stack und Heap im Vergleich und demonstriert die Arbeit des Garbage Collectors.
Abb. 5: Dynamischer Anwendungsspeicher: Stack (Links), Heap (Mitte) und Funktionsweise des Garbage Collectors (Unten)
Automatisierte Speicherfreigabe – der Garbage Collector
Der Garbage Collector (Speichermanager) vom .NET Framework verwaltet die Reservierung und Freigabe von Arbeitsspeicher für die Anwendung. Wenn Sie den Operator new zum Erstellen eines Objekts verwenden, reserviert er zur Laufzeit Arbeitsspeicher für das Objekt aus dem verwalteten Heap. Arbeitsspeicher ist jedoch nicht unendlich verfügbar. Möglicherweise muss mithilfe der Garbage Collection Arbeitsspeicher freigegeben werden. Das Optimierungsmodul der Garbage Collection bestimmt den besten Zeitpunkt für die Freigabe anhand der erfolgten Reservierungen. Dabei wird nach Objekten im verwalteten Heap gesucht, die nicht mehr von der Anwendung verwendet werden. Anschließend werden die für das Freigeben des Arbeitsspeichers erforderlichen Operationen ausgeführt.
Software kann nicht ohne Vorbereitungen erstellt werden. Für die Entwicklung bedarf es eines Konzepts. Auf der Ebene des Entwurfs bis zur Implementierung geht es darum, ein Modell der Anwendung zu entwerfen. Als Unterstützung wird dabei häufig auf die Modellierungssprache UML (Unified Modeling Language) zurückgegriffen. Sie bietet ein ganzes Spektrum an Diagrammtypen für die Visualisierung der unterschiedlichsten Sachverhalte und ist auf den objektorientierten Ansatz ausgerichtet. Im Stadium des statischen Entwurfs sind das Klassen- und das Objektdiagramm von Interesse:
Abb. 6: Ein Klassendiagramm
Um die Beziehungen zwischen den Objekten (zur Laufzeit) zur visualisieren, stehen ebenfalls einige Diagrammtypen aus der UML zur Verfügung. Sie beschreiben das Ablaufverhalten des Systems. Auf Klassen- bzw. Objektebene bietet sich insbesondere das Sequenzdiagramm an. Es dient dazu, den Nachrichtenfluss zwischen den Objekten darzustellen. Insbesondere wird der zeitliche Ablauf der Nachrichten verdeutlicht. Die Notationselemente sind die Lebenslinie (gestrichelte Linie), die Nachricht und der Interaktionsrahmen. Eine Lebenslinie ist genau einem Objekt einer Klasse (Rechteck) zugeordnet. Die Lebenslinie symbolisiert die passive Lebenszeit des Objekts. Wird das Objekt verwendet, kommt es zur Aktivierung. Mittels eines Balkens wird dieser Zustand verdeutlicht. Objekte von Klassen werden mithilfe eines Konstruktors erzeugt (Start der Lebenslinie). Die Darstellung der Objektzerstörung ist dank Garbage Collector für C# entbehrlich. Die Kommunikation zwischen den Objekten wird mithilfe von Nachrichten dargestellt. Sie können entweder den Aufruf einer Operation darstellen oder das Senden eines Signals (Pfeil) beinhalten. Es ist auch möglich, dass Objekte Nachrichten an sich selbst senden. Abbildung 7 zeigt ein Beispiel für ein Sequenzdiagramm.
Abb. 7: Ein Sequenzdiagramm
Setzen wir das in Abbildung 6 gezeigte Klassendiagramm in C# um. Es sind die Klassen und deren Vererbungsbeziehungen zu definieren. Die Sichtbarkeiten der gesamten Klasse und ihrer Member (Eigenschaften, Methoden und Ereignisse) sind festzulegen (Tabelle 1). Das Ergebnis ist in Listing 1 dargestellt.
Listing 1: Quellcode (C#) für das Klassendiagramm aus Abbildung 6 namespace ProgrammierkursTeil3 { public abstract class Fahrzeug { private int fahrzeugNummer; public int FahrzeugNummer { get { return fahrzeugNummer; } set { fahrzeugNummer = value; } } private int leerGewicht; public int LeerGewicht { get { return leerGewicht; } set { leerGewicht = value; } } private int zulaessigesGesamtGewicht; public int ZulaessigesGesamtGewicht { get { return zulaessigesGesamtGewicht; } set { zulaessigesGesamtGewicht = value; } } public bool PruefeVerfuegbarkeit() { // Code, um die Verfügbarkeit für alle Fahrzeuge zu prüfen return true; } } public abstract class Kraftfahrzeug : Fahrzeug { private int hoechstgeschwindigkeit; public int Hoechstgeschwindigkeit { get { return hoechstgeschwindigkeit; } set { hoechstgeschwindigkeit = value; } } public abstract void PruefeFahrerlaubnis(); } public class Fahrrad : Fahrzeug { private double rahmenHoehe; public double RahmenHoehe { get { return rahmenHoehe; } set { rahmenHoehe = value; } } } public class Motorrad : Kraftfahrzeug { public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } public class PKW : Kraftfahrzeug { private int anzahlSitzplaetze; public int AnzahlSitzplaetze { get { return anzahlSitzplaetze; } set { anzahlSitzplaetze = value; } } public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } public class LKW:Kraftfahrzeug { private int nutzLast; public int NutzLast { get { return nutzLast; } set { nutzLast = value; } } public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } } Listing 1: Quellcode (C#) für das Klassendiagramm aus Abbildung 6 namespace ProgrammierkursTeil3 { public abstract class Fahrzeug { private int fahrzeugNummer; public int FahrzeugNummer { get { return fahrzeugNummer; } set { fahrzeugNummer = value; } } private int leerGewicht; public int LeerGewicht { get { return leerGewicht; } set { leerGewicht = value; } } private int zulaessigesGesamtGewicht; public int ZulaessigesGesamtGewicht { get { return zulaessigesGesamtGewicht; } set { zulaessigesGesamtGewicht = value; } } public bool PruefeVerfuegbarkeit() { // Code, um die Verfügbarkeit für alle Fahrzeuge zu prüfen return true; } } public abstract class Kraftfahrzeug : Fahrzeug { private int hoechstgeschwindigkeit; public int Hoechstgeschwindigkeit { get { return hoechstgeschwindigkeit; } set { hoechstgeschwindigkeit = value; } } public abstract void PruefeFahrerlaubnis(); } public class Fahrrad : Fahrzeug { private double rahmenHoehe; public double RahmenHoehe { get { return rahmenHoehe; } set { rahmenHoehe = value; } } } public class Motorrad : Kraftfahrzeug { public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } public class PKW : Kraftfahrzeug { private int anzahlSitzplaetze; public int AnzahlSitzplaetze { get { return anzahlSitzplaetze; } set { anzahlSitzplaetze = value; } } public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } public class LKW:Kraftfahrzeug { private int nutzLast; public int NutzLast { get { return nutzLast; } set { nutzLast = value; } } public override void PruefeFahrerlaubnis() { // Hinweis, dass die Implementierung noch aussteht throw new NotImplementedException(); } } }
class Auto { // Felder // Konstruktoren // Eigenschaften // Methoden // Ereignisse }
public int Alter { get { // hier den Lesezugriff implementieren return alter; // Zugriff auf das lokale Feld alter } set { // hier den Schreibzugriff implementieren Alter = value; } }
public void Bremsen() { // Implementierung des Bremsvorgangs }
Diese wenigen Ausführungen können natürlich nicht das systematische Studium der Dokumentation ersetzen. Als Einstieg in die Arbeit mit Klassen ist das Onlineprogrammierhandbuch der MSDN-Dokumentation gut geeignet.
Die objektorientierte Programmierung bleibt das Maß der Dinge beim Entwurf und der Implementierung moderner Software. Die Konzepte sind dabei unabhängig von der Wahl der Programmiersprache. C# bietet als moderne Sprache eine bestmögliche Unterstützung aller notwendigen Ansätze. Die grafische Visualisierung erfolgt mithilfe der Diagramme aus der UML. Auch den objektorientierten Entwurf erlernt man nicht über Nacht: Üben, Üben und nochmals Üben ist also an der Tagesordnung. Dabei gibt es (fast) keine falschen Lösungen, sondern solche, die als Softwaremodell besser oder eben weniger gut geeignet sind.
Wie in den letzten beiden Teilen gibt auch dieses Mal wieder eine „Hausaufgabe“ (Kasten: „Hausaufgabe: Objektorientierter Entwurf“). Einen Lösungsvorschlag finden Sie mit Erscheinen dieses Artikels it-fachartikel.de. Im kommenden Artikel unseres Programmierkurses wird es sehr „oberflächlich“ zugehen. Wir beschäftigen uns mit den Grundlagen des modernen User-Interface-Designs – und das hat es wirklich ins sich. Seien Sie gespannt!
Ihr Auftraggeber ist die Immobilienfirma „Nobel und Teuer“ [2]. Deren Geschäft besteht darin, Häuser zu verkaufen. Grundsätzlich ist zwischen Einfamilienhäusern und Geschäftshäusern zu unterscheiden. Zu beiden Haustypen müssen die folgenden Eigenschaften gespeichert werden:
Zu beiden Immobilientypen kann man den Verkaufspreis anfragen. Nur für das Geschäftshaus kann man die Anzahl der Büroräume erfragen.
Aufgaben:
Links & Literatur
[1] Gewinnus, T.; Doberenz, W.: „Der Visual C#-Programmierer“, Carl Hanser Verlag, München, 2009
[2] Balzert, H.: „Lehrbuch – Grundlagen der Informatik“, Spektrum Akademischer Verlag, 2005
Aufmacherbild: Butterflies freshly emerged from cocoon via Shutterstock / Urheberrecht: Ksenia Ragozina