Objektorientierung:

Die objektorientierte Programmierung ist das A und O in der Entwicklung von Anwendungen. Ein klares Verständnis von Klassen, Methoden sowie Eigenschaften sind für einen Programmierer unabdingbar. Nahezu alle modernen Programmiersprachen unterstützen die wesentlichsten Merkmale dieses Paradigmas. Seit geraumer Zeit wird der sichere Umgang mit Objekten in der Programmierung als Basisqualifikation angesehen.
In diesem Artikel geht es in erster Linie um die Erklärung der dahinter liegenden Idee, zur Abrundung gibt es praktische Beispiele in C#. Wie Eingangs erwähnt ist das objektorientierte Programmieren eines der wesentlichsten Paradigmen in der Softwareentwicklung. Doch was sind eigentlich diese Paradigmen? Grundsätzlich unterscheiden wir zwischen folgenden Paradigmen:
  • Imperatives: Der geschrieben Code wird sequenziell, also eine Anweisung nach der anderen, abgearbeitet. Dieses Konzept dürfte den meisten Programmieren bekannt vorkommen, da auch aktuelle Sprachen imperativ sind.

  • Funktionales: Ein rein Funktionales Programm besteht aus einer Folge von Funktionsaufrufen (daher auch der Name). So können zum Beispiel alle Elemente sozusagen als Funktion aufgefasst werden. Die wichtigsten Anwendungsgebiete sind Compilerbau und KI-Systeme. Eine typische funktionale Sprache ist z.B. Haskell.

  • Objektbasiertes: Objektbasierte Sprachen sind, wie der Name erahnen lässt, eine Vorstufe der Objektorientierung, und kennen bereits die sogenannte Kapselung von Daten. Vererbung und andere Aspekte wird nicht unterstützt. Ein prominenter Vertreter dieses Paradigmas ist PowerShell.

  • Objektorientiertes: Sprachen dieses Paradigmas unterstützen alle wesentlichen Aspekte wie die Vererbung oder Polymorphie. Sprachen wie C++, C# und Java sind vollständig objektorientiert.
  • Logisches: Ein eher seltenes Paradigma dessen Aufbau aus Fakten und Regeln besteht. Im Fokus dieses Prinzips steht eher die Ausformulierung des Problems und nicht dessen Lösung. Als Beispiel dieses Paradigmas gilt die Sprache Prolog.

Wie auch bei der Auswahl der verwendeten Sprache gilt auch hier, es gibt kein „richtig“ oder „falsch“ sondern vielmehr ein „besser“ oder „weniger“ geeignet. Unterm Strich kann man jedoch behaupten, dass die objektorientierte Programmierung als “state of the art“ in der Softwareentwicklung gilt.

Was ist dieses Objekt eigentlich?

Die objektorientierte Programmierung hat sich die Realität als Vorbild genommen. Somit verfügt ein Objekt über typische Eigenschaften (Attribute) und Fähigkeiten (Methoden). Da dies alles ein wenig trocken wirkt, nehmen wir ein Auto als Beispielobjekt.
Ein solches Auto-Objekt kann zum Beispiel die Attribute: Hersteller, Baujahr, Leistung und Farbe haben. Als Methoden wären hier die „Fähigkeiten“ beschleunigen, bremsen oder Türen öffnen denkbar. All diese Attribute und Methoden werden somit von einem Objekt zusammengefasst. Das Objekt selber gehört wiederum zu einer Klasse. Eine Klasse ist grob gesagt der Bauplan des Objektes. In der objektorientierten Programmierung versucht man diese Objekte möglichst nahe an die realen Gegebenheiten anzupassen.
Die folgende Abbildung beschreibt die Abhängigkeit Klasse <-> Objekt und deren Attribute und Methoden.

Erste Klasse

Unser „mein Auto“ Objekt hat an dieser Stelle bereits befüllte Attribute und beschreibt somit einen schwarzen Mitsubishi mit dem Baujahr 2015 und 220 PS. Die Methoden Beschleunigen, Bremsen und TuereOeffnen aus der Klasse „Auto“ sind für dieses Objekt ebenfalls frei verfügbar, müssen aber für das Objekt nicht erneut definiert werden.
Von einer Klasse können beliebig viele Objekte (genannt Instanzen) erstellt werden. Zumindest so lange bis der Speicher voll läuft ;)

Die vier Pfeiler der Objektorientierung

  • Kapselung: Die Kapselung besagt, dass Attribute und Methoden nur über kontrollierten Zugriff erreicht werden können. Das Ergebnis dieser Kapselung bildet die Klasse. Eine Klasse kann eine andere Klasse nicht unbeabsichtigt verändern oder auslesen.

  • Sichtbarkeit: Klassen können unterschiedliche Sichtbarkeiten aufweisen um fest zu legen welche Elemente eines Objektes, also einer Instanz einer Klasse, für andere Objekte sichtbar sind. Diese Sichtbarkeit wird über sogenannte Zugriffsmodifizierer definiert. Es gibt eine ganze Reihe dieser Modifizierer. Der restriktivste ist private womit Elemente des Objektes nur innerhalb der Klasse sichtbar sind. Mit public geben wir die Elemente des Objektes auch für alle anderen Objekte frei. Umgelegt auf unser Autobeispielt wäre es ratsam die Attribute mit public zu markieren, damit auch andere Objekte diese Informationen erfragen können.

  • Vererbung: Eine weitere Auszeichnung der objektorientierten Programmierung ist die Vererbung. Dies bezeichnet die Möglichkeit, dass eine Klasse von einer anderen abgeleitet werden kann. Die erbende Klasse erhält somit alle Attribute und Methoden der übergeordneten Klasse. Die Möglichkeit der Mehrfachvererbung, also das direkte Erben von mehreren Klassen gleichzeitig bieten nur wenige Sprachen wie zum Beispiel C++ und Python.

  • Polymorphie: Die Polymorphie beschreibt grob gesagt, dass Objekte des gleichen Types auf das gleiche Kommando unterschiedlich reagieren. Einfaches Beispiel zur Veranschaulichen. Man nehme die beiden Klassen PKW und Autobus. Beide Klassen erben einer Mutterklasse KFZ. In der KFZ-Klasse wird die Methode TuereOeffnen() sozusagen als Platzhalter definiert da je nach KFZ-Typ die Methode etwas anderen tun muss. Da das öffnen der Türen bei einem PKW anders als bei einem Autobus aussieht, wird diese Methode in den beiden Unterklassen jeweils spezifiziert. Nun wird bei dem Methodenaufruf des KFZ immer die richtige Implementation aufgerufen, abhängig davon ob es sich um einen PKW oder Autobus handelt.

Objekt, wo kommst du her, wo gehst du hin

Wir bisher beschrieben, sind Objekte konkrete Instanzen einer Klasse. Während wir eine Klasse ganz gewöhnlich in unserem Quellcode definieren, wird ein Objekt erst zur Laufzeit generiert. Ein Objekt wird in der Regel von einem sogenannten Konstruktor erstellt. Dieser Konstruktor ist direkt in der Klasse definiert und liefert uns das gewünschte Objekt mit all seinen Attributen und Methoden mit denen dann gearbeitet werden kann. Diese Objekte haben in der Regel eine begrenzte Lebensdauer.
Werden Objekte innerhalb des laufenden Programmes nicht mehr benötigt, werden sie gelöscht und der von ihnen reservierte Speicher wird freigegeben.
Je nach Programmiersprache ist man entweder selber für die Speicherfreigabe verantwortlich oder es wird einem vom System abgenommen. So besitzt C# oder Java den sogenannten Garbage Collector, der sich um die Speicherverwaltung kümmert, während wir in C++ eigene Destruktoren (quasi die Gegenspieler der Konstruktoren) implementieren und aufrufen müssen. Beides hat seine Vor- und Nachteile wobei der Garbage Collector einem einiges an Arbeit aber auch an Kontrolle abnimmt.

Im Detail funktionier der Garbage Collector - unser Speichermanager - so:
Wenn du mit dem Schlüsselwort new ein neues Objekt erstellt, dann wird zur Laufzeit Arbeitsspeicher für dieses Objekt reserviert. Um den Arbeitsspeicher nicht endlos mit Objekten voll zu packen, greift unser Garbage Collector ein. Dieser bestimmt automatisch den besten Zeitpunkt (CPU-Auslastung zB) für die Freigabe des Speichers. Dabei wird gezielt nach Objekten im verwalteten Heap gesucht, die von dem Programm nicht weiter benötigt werden. Diese Objekte werden aus dem Speicher entfernt, so dass diese Ressourcen wieder verwendet werden können.
Noch mehr zum Thema GC findest du hier.

Beispiel Quellcode in C#

Für unser Abschlussbeispiel implementieren wir eine kleine Klassenhierarchie. Ganz oben steht die Klasse Kfz die wir mit dem Schlüsselwort class deklarieren. Attribute (oder Felder) markieren wir als private da wir auf diese nur über die Methoden zugreifen wollen (Stichwort Kapselung).
Aus dieser leiten sich die Klassen Pkw und Lkw ab. Da ein Pkw- bzw. Lkw-Objekt nicht spezifisch genug ist, deklarieren wir diese Klassen als abstrakt und verhindern somit, dass diese Klassen instanziert werden können.
Aus der Pkw-Klasse leiten wir weiter drei verschiedene Klassen ab. Den Kombi, die Limousine und das Cabrio. Diese Klassen sollen im späteren Verlauf auch unsere Objekte darstellen und sind daher nicht mehr abstrakt.
Zum leichteren Verständnis lässt sich zum Beispiel in Visual Studio aus dem Code ganz einfach ein UML (Unified Modeling Language) Diagramm erstellen das die Beziehungen der Klassen klar darstellt. In unserem Beispiel sieht dieses Diagramm so aus:

Klassendiagramm

Dank der Vererbung verfügt nun unser Kombi sowohl über die Kombispezifische PruefeMotor-Methode als auch über die Attribute anzahlSitze aus der Pkw-Klasse und leergweicht aus der Kfz-Klasse. Die Starten-Methode aus den abstrakten Überklasse PKW kann nun ebenfalls in den einzelnen PKW-Typen implementiert werden. Da alle PKWs über diese Methode verfügen (schließlich wird die ja geerbt) kann man im weitern Verlauf zum Beispiel gesagt werden "Starte mir alle PKW" ohne jedes einzelne Objekt sperat ansprechen zu müssen, da ja alle Kombis und Cabrios zu PKW-Klasse gehören.
Der Code zu diesem Diagramm hat zwar keine ausprogrammierten Methoden, zum allgemeinen Verständnis ist das allerdings nicht wichtig:
    
namespace CsharpExample
{
    public abstract class Kfz
    {
        private int leergewicht;
        public int Leergewicht
        {
            get { return leergewicht; }
            set { leergewicht = value; }
        }

        private int leistung;
        public int Leistung
        {
            get { return leistung; }
            set { leistung = value; }
        }

        private string kennzeichen;
        public string Kennzeichen
        {
            get { return kennzeichen; }
            set { kennzeichen = value; }
        }


        public void Auftanken()
        {
            // Code um das KFZ aufzutanken
        }
    }

    public abstract class Pkw : Kfz
    {
        private string farbe;
        public string Farbe
        {
            get { return farbe; }
            set { farbe = value; }
        }

        private int anzahlTueren;
        public int AnzahlTueren
        {
            get { return anzahlTueren; }
            set { anzahlTueren = value; }
        }

        private int anzahlSitze;
        public int AnzahlSitze
        {
            get { return anzahlSitze; }
            set { anzahlSitze = value; }
        }

        public void Starten()
        {
            // PKW wird gestartet
        }
    }

    public abstract class Lkw : Kfz
    {
        private int anzahlAchsen;
        public int AnzahlAchsen
        {
            get { return anzahlAchsen; }
            set { anzahlAchsen = value; }
        }

        private int maximalgewicht;
        public int Maximalgewicht
        {
            get { return maximalgewicht; }
            set { maximalgewicht = value; }
        }

        public void Starten()
        {
            //LKW wird gestartet
        }
    }

    public class Kombi : Pkw
    {
        private void PruefeMotor()
        {
            // Motor des Kombis wird geprüft
        }

        private void KofferraumOeffnen()
        {
            //Kofferraum wird geöffnet
        }
    }

    public class Limousine : Pkw
    {
        private void PruefeMotor()
        {
            // Motor der Limousine wird geprüft
        }

        private void FahrerRufen()
        {
            //Fahrer wird gerufen
        }
    }

    public class Cabrio : Pkw
    { 
        private void PruefeMotor()
        {
            // Motor des Cabrios wird geprüft
        }

        private void DachOeffnen()
        {
            //Dach wird geöffnet
        }
    }
}
    

Aufgabe zum Üben

Als Aufgabe gilt es nun folgendes zu Implementieren:
  • Erstelle eine Klasse Baum. Grundsätzlich unterscheiden wir zwischen Laubbaum und Nadelbaum die von unserer Baum-Klasse erben. Unter der Nadelbaum-Klasse sollen sich wieder drei Subklassen befinden die auch instanzierbar sind. Hierbei könnte es sich zum Beispiel um Tanne, Fichte und Kiefer.
  • Implementiere ein paar zusätzliche Attribute und Methoden. Überlege dir, welche Attribute für alle Baumarten gültig sind und welche vielleicht nur für eine Tanne. (zugegeben, ein wenig Fanatsie wird hier notwendig sein ;) )
  • Stelle ein UML Diagramm deines Codes dar.