Teil 3 – Ojektorientierte Programmentwicklung

Einführung in die Programmierung: Objekte, nichts als Objekte
Kommentare

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.

Artikelserie
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: Datenmodellierung, Datenbanken (Dateisystem, Server, Cloud)

Paradigmen der Programmentwicklung

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:

  • Imperatives: Das Programm besteht aus einer Folge von Anweisungen, die streng sequenziell abgearbeitet werden. Das Konzept des imperativen Programmierparadigmas beruht auf Funktionen und Prozeduren zur Abbildung der Funktionalität und Wiederverwendung von Quellcode. Der bekannteste Vertreter ist die Programmiersprache C.
  • Objektbasiertes: Diese Programmiersprachen kennen Objekte zur Kapselung von Daten und Funktionalität. Weitergehende Konzepte wie beispielsweise Vererbung oder die Abbildung von Beziehungen zwischen Objekten werden jedoch nicht angeboten. Objektbasierte Sprachen können damit als Vorstufe der objektorientierten Programmierung aufgefasst werden. Ein Beispiel ist die Script-Sprache Microsoft Powershell.
  • Objektorientiertes: Programmiersprachen dieser Gattung erweitern das objektbasierte Programmierparadigma um die typischen Konzepte der Objektorientierung wie Vererbung und Polymorphie. Moderne und häufig eingesetzte Vertreter sind Java und C#.
  • Funktionales: Ein funktionales Programm besteht nur aus einer Reihe von Funktionsaufrufen. Eigenständige Wertzuweisungen existieren nicht; alle Elemente können als Funktionen aufgefasst werden. Einsatzgebiete sind Anwendungen der künstlichen Intelligenz, Compilerbau und Computeralgebrasysteme. Funktionale Sprachen beruhen auf dem so genannten Lambda-Kalkül. Beispiele funktionaler Programmiersprachen sind Lisp, Haskell und – mit zunehmender Bedeutung – die Sprache F#.
  • Logisches: Im Mittelpunkt steht der Aufbau einer Datenbasis, die aus Fakten und Regeln besteht. Fakten sind dabei wahre Aussagen im Sinne der Mathematik. Im Fokus steht die Problemformulierung, nicht der Lösungsalgorithmus. Die Sprache Prolog basiert auf dem logischen Paradigma.
  • Deklaratives: Überbegriff für das funktionale und das logische Paradigma.

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.

Objektorientierung im Überblick

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.

Abb. 1: Zusammenhang zwischen Klasse (allgemein) und Objekten (konkret)

Abb. 1: Zusammenhang zwischen Klasse (allgemein) und Objekten (konkret)

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:

  • Hersteller
  • Typ
  • Farbe
  • Zulassungsjahr

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:

  • Beschleunigen
  • Bremsen

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:

  • Hersteller = Toyota;
    Typ = Avensis,
    Farbe = grau,…

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).

Abb. 2: Von den Gegebenheiten der realen Welt zum objektorientierten Modell

Abb. 2: Von den Gegebenheiten der realen Welt zum objektorientierten Modell

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.

Objektorientierte Programmiersprachen

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).

Abb. 3: Objektorientierte Konzepte in Programmiersprachen

Abb. 3: Objektorientierte Konzepte in Programmiersprachen

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).

Abb. 4: Komponenten der objektorientierten Programmierung

Abb. 4: Komponenten der objektorientierten Programmierung

Objektorientierte Konzepte im Detail

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:

  • Kapselung: Die Kapselung besagt, dass Informationen und das zugehörige Verhalten zur Verarbeitung dieser Informationen zusammengefasst werden. Das Ergebnis dieser Integration ist das Konzept der Klasse. Konkrete Ausprägungen einer Klasse sind die Objekte.
  • Geheimnisprinzip: Es besagt, dass ein Objekt nach außen nur die Informationen und Methoden bereitstellt, die zur Anwendung notwendig sind. Ein Beispiel: Objekte der Klasse Auto verfügen über die Methode Bremsen(). Für einen Benutzer ist es nicht von Interesse, wie das innerhalb der Klasse umgesetzt wird (unterschiedliche Bremssysteme). Es interessiert nur, dass der Aufruf der Methode zu dem gewünschten Ergebnis führt. Wird die Klasse später aktualisiert (zum Beispiel ein verbessertes Bremssystem) ist für eine problemlose Weiterverwendung lediglich sicherzustellen, dass die Aufrufkonventionen (Schnittstellenparameter) konstant gegenüber der Vorversion bleiben. Die eigentliche Implementierung kann dabei angepasst werden.
  • Sichtbarkeit: Klassen und deren Eigenschaften und Methoden können unterschiedliche Sichtbarkeiten aufweisen. Damit wird angegeben, welche Elemente eines Objektes für andere Objekte sichtbar sind. Die beiden Extreme lauten: (1) private: Das Element ist nur innerhalb der Klasse sichtbar und (2) public: Das Element ist für alle anderen Objekte sichtbar. Beispielsweise werden Variablen, die für eine Berechnung innerhalb einer Methode benutzt werden, als private definiert. Soll auf eine Eigenschaft eines Objekts auch von außen zugegriffen werden, so ist sie als public zu kennzeichnen. Für die Klasse PKW könnte es beispielsweise sinnvoll sein, die Eigenschaft Leistung als public zu definieren, denn so können andere Objekte diesen Wert erfragen.
  • Vererbung: Mithilfe der Vererbung können Eigenschaften und Methoden einer übergeordneten Klasse auf nachgelagerte Klassen vererbt werden. Ein Beispiel: Die Klasse Fahrzeug enthält die Eigenschaften Gewicht und Alter. Von dieser Klasse werden die Unterklassen PKW und LKW abgeleitet. Die Objekte dieser Klassen erben die beiden Eigenschaften von der Klasse Fahrzeug und brauchen lediglich um spezifische Elemente ergänzt zu werden. Die Zuordnung von Eigenschaften und Methoden zu Ober- und Unterklassen folgen dabei den Prinzipien von Generalisierung und Spezialisierung.
  • Polymorphie: Objekte verschiedener Klassen müssen gelegentlich gleiche Funktionen ausführen. Beispielsweise müssen alle motorgetriebenen Fahrzeuge betankt werden. Dazu wird in der abstrakten Klasse Fahrzeug die Methode Tanken() definiert. Von abstrakten Klassen können keine Objekte erzeugt werden. Sie sind zu unspezifisch und dienen nur als übergeordneter Platzhalter bzw. zur Definition gemeinsamer Eigenschaften. In den Unterklassen PKW und LKW muss jeweils die Methode Tanken() konkretisierend definiert und implementiert werden. So kann man festlegen, dass Objekte der Klasse PKW mittels Benzin und Objekte der Klasse LKW mittels Diesel betankt werden. Damit wird erreicht, dass beim Aufrufen der Methode Tanken() stets die richtige Implementierung verwendet wird.

Der Lebenszyklus von Objekten

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)

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.

Objektorientierung und UML

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:

  • Klassendiagramm: Klassendiagramme stellen das zentrale Konzept der UML dar. Eingesetzt werden sie in allen Phasen des Softwareentwicklungsprozesses. Sie können unterschiedliche Detaillierungsgrade aufweisen und beschreiben grafisch die Beziehungen (zum Beispiel Vererbung) zwischen den Klassen einer Anwendung. Klassendiagramme stellen eine statische Sichtweise auf die Anwendung dar. Neben dem Namen der Klasse und dem Typ werden die Eigenschaften und Methoden benannt. In Visual Studio können Klassendiagramme automatisiert direkt aus dem Quellcode erstellt werden. Abbildung 6 zeigt ein Beispiel für ein Klassendiagramm.
  • Objektdiagramm: Das Objektdiagramm ist eine Spezifizierung des Klassendiagramms. Es stellt die Beziehungen der tatsächlich erzeugten Objekte zu einem bestimmten Zeitpunkt zur Laufzeit dar. Objektdiagramme sind als Ergänzung zu den Klassendiagrammen aufzufassen. Im Objektdiagramm werden der Name des Objekts und durch Doppelpunkt die zugehörige Klasse angegeben. Da die Abbildung zu einem bestimmten Zeitpunkt erfolgt, weisen die Attribute bestimmte Werte auf, die angegeben werden können. Ein Beispiel: Das Objekt MeinAuto der Klasse PKW verfügt über das Attribut Typ. Dieses Attribut weist zum betrachteten Zeitpunkt den Wert Typ = Avensis auf. Im Diagramm erfolgt eine Beschränkung auf die wesentlichen Attribute. Im Objektdiagramm werden keine Operationen dargestellt. Beide Diagramme müssen „harmonisieren“ – d. h., es dürfen keine Objekte existieren, zu denen keine zugehörige Klasse modelliert wurde und Klassen, von denen keine Objekte erzeugt werden (außer statische Klassen), sind auf Notwendigkeit zu untersuchen.

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.

Umsetzung in C#

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();
    }
  }
}
Tabelle 1: Mögliche Sichtbarkeiten von Klassen und deren Member (Attribute, Methoden) [1]

Tabelle 1: Mögliche Sichtbarkeiten von Klassen und deren Member (Attribute, Methoden) [1]

  1. In C# wird eine Klasse mit dem Schlüsselwort class deklariert. Also zum Beispiel:
    class Auto
    {
      // Felder
      // Konstruktoren
      // Eigenschaften
      // Methoden
      // Ereignisse
    }	
    
  2. Felder sollten nur als privat deklariert werden, denn der Zugriff auf die internen Daten eines Objekts soll nur über Eigenschaften bzw. Methoden stattfinden. Eine Methode ist wie folgt zu definieren:
    public int Alter
    {
      get
      {
        // hier den Lesezugriff implementieren
        return alter; // Zugriff auf das lokale Feld alter
      }
      set
      {
        // hier den Schreibzugriff implementieren
        Alter = value;
      }
    }
    
  3. Methoden haben in ihrer Grundform die folgende einfache Syntax:
    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.

Fazit, Hausaufgabe und Ausblick

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:

  • Einfamilienhaus: Haustyp, Besitzer, Adresse, Wohnfläche, Anzahl der Bäder, Schwimmbad (ja/nein), Garten, (ja/nein), Baujahr, Verkaufspreis (Netto)
  • Geschäftshaus: Besitzer, Adresse, Anzahl der Büroräume, Geschosszahl, Fahrstuhl (ja/nein), Tiefgarage (ja/nein), Baujahr, Verkaufspreis (Netto)

Zu beiden Immobilientypen kann man den Verkaufspreis anfragen. Nur für das Geschäftshaus kann man die Anzahl der Büroräume erfragen.

Aufgaben:

  1. Entwerfen Sie ein UML-Klassendiagramm, das die beiden Klassen in Abhängigkeit einer übergeordneten Klasse darstellt.
  2. Erstellen Sie ein Objektdiagramm, das je Klasse (Einfamilienhaus, Geschäftshaus) zwei konkrete Objekte darstellt. Konkretisieren Sie dazu die Werte für die Eigenschaften nach Ihrer Fantasie.
  3. Setzen Sie das Klassendiagramm aus 1) in C#-Klassen um.

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

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Aufmacherbild: Butterflies freshly emerged from cocoon via Shutterstock / Urheberrecht: Ksenia Ragozina

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -