Arbeiten mit .NET: OOP/ Vererbung/ Abstrakte Klassen
In der Vererbung von Klassen gibt es einige Besonderheiten, mit denen wir spezielle Gegebenheiten erheblich vereinfachen können.
Problem
[Bearbeiten]Stellen wir uns folgendes Problem vor: Wir möchten gern einen Fuhrpark für ein Autohaus verwalten. Dazu entwickeln wir eine mehr oder weniger komplexe Vererbungshierarchie:
class Auto { // Hier erfolgt die Implementierung // der grundlegenden Funktionalität für jedes Auto. } class Audi : Auto { // Hier erfolgt die Implementierung // der speziellen Funktionalität für jeden Audi. } class A4 : Audi { // Hier erfolgt die Implementierung // der speziellen Funktionalität für den "Audi A4". }
Soweit nichts Neues. Alles bekannte Materie. Aber macht es wirklich Sinn, Objekte von der Klasse Auto erzeugen zu können? Wohl kaum, oder?
Genauso wenig Sinn macht es aber, die sorgfältig abstrahierten Informationen aus der Klasse Auto auf die Erben zu verteilen.
Es gibt also keine Lösung? Doch, die gibt es. Sie heißt:
Abstrakte Klassen
[Bearbeiten]Eine abstrakte Klasse funktioniert beinahe genauso, wie jede andere Klasse auch. Der wesentliche Unterschied besteht darin, dass jeder Versuch, von ihr ein Objekt zu bekommen, fehlschlägt.
Genau das, was wir für unser kleines Problem oben suchen, oder? Dann passen wir es doch gleich an:
abstract class Auto { // Hier erfolgt die Implementierung // der grundlegenden Funktionalität für jedes Auto. } abstract class Audi : Auto { // Hier erfolgt die Implementierung // der speziellen Funktionalität für jeden Audi. } class A4 : Audi { // Hier erfolgt die Implementierung // der speziellen Funktionalität für den "Audi A4". }
Die Vererbung funktioniert nach wie vor. Aber jetzt können wir kein Objekt mehr von den Klassen Auto oder Audi bekommen.
Diese abstrakten Klassen bieten uns die Möglichkeit, sogenannte Basisklassen in der Vererbungshierarchie zu schaffen, die Eigenschaften und Fähigkeiten zum Zwecke der vereinfachten Wartung zusammenfassen, ohne dass es Sinn ergeben würde, von ihnen explizit Objekte zu erzeugen oder erzeugen zu können.
Quellcode: Abstrakte Klasse Auto
[Bearbeiten]Unsere Klasse Auto sieht jetzt also vollständig so aus:
abstract class Auto { protected int m_Nummer; protected string m_Farbe; protected string m_Typ; public Auto(int nummer, string farbe, string typ) { m_Nummer = nummer; m_Farbe = farbe; m_Typ = typ; } // Eigenschaften public int Nummer { // Was soll passieren, // wenn jemand die Nummer lesen möchte? get { return m_Nummer; } // Was soll passieren, // wenn jemand die Nummer schreiben möchte? set { // Wir prüfen, ob der Wert größer als 0 // und kleiner oder gleich 2000 ist. // Wenn ja, dann übernehmen wir den Wert. // Wenn nein, dann ignorieren wir diesen Versuch, // fehlerhafte Daten einzugeben. if ((value > 0) && (value <= 2000)) m_Nummer = value; } } public string Farbe { // Was soll passieren, // wenn jemand die Farbe lesen möchte? get { return m_Farbe; } // Was soll passieren, // wenn jemand die Farbe schreiben möchte? set { m_Farbe = value; } } public string Typ { get { return m_Typ; } } public void StarteMotor() {} public void Beschleunige() {} public void Hupe() {} public void Blinke(bool links, bool rechts) { // Sollen wir links blinken? if (links == true && rechts == false) { Console.WriteLine("Links blinken"); } // Sollen wir rechts blinken? if (links == false && rechts == true) { Console.WriteLine("Rechts blinken"); } // Oder ist der Warnblinker an? if (links == true && rechts == true) { Console.WriteLine("Der Warnblinker ist an."); } } }
Aber mit der Abstraktion können wir noch viel mehr machen. Mit ihr lassen sich nicht nur Klassen abstrahieren, sondern auch Methoden.
Abstrakte Methoden
[Bearbeiten]Werfen wir dazu noch einmal einen genauen Blick auf die Klasse Auto, wie wir sie eben zusammengefasst haben.
Wir haben ja nun erfahren, dass Abstraktion uns hilft, sinnlose Dinge (Objekte von Auto) zu vermeiden. Nun ist das Starten des Motors zwar nicht wirklich sinnlos, aber es gibt unzählige Varianten, wie dieser Vorgang in den Autos umgesetzt wird. Während die Mittelklassewagen beispielsweise zahlreiche Informationen prüfen, lassen die Kleinwagen solche Prüfungen häufig eher mager ausfallen. Und ältere Typen prüfen gleich gar nichts.
Würden wir jetzt also schon in der Klasse Auto festlegen, wie der Startvorgang zu erfolgen hat, könnten wir es später nur schwer ändern. Es gibt zwar durchaus auch dafür Möglichkeiten, aber die sind umständlich. Warum also nicht gleich richtig machen, insbesondere, wenn es doch ganz einfach geht? Wir müssen nichts weiter machen, als die Methode StarteMotor() zu abstrahieren:
public abstract void StarteMotor();
Da abstrakte Methoden keine Erläuterung enthalten, welche Aktionen unternommen werden sollen, wenn die Methode aufgerufen wird, lassen wir auch die geschweiften Klammern einfach weg. Stattdessen setzen wir ein Semikolon, um dem Compiler anzuzeigen, dass wir es ernst meinen, wenn wir die Methode StarteMotor() ohne diese geschweiften Klammern schreiben.
Welchen Sinn hat es aber, eine Methode zu schreiben, die nichts macht und auch gar nichts machen darf?
Nun, das ist, wieder einmal, ganz einfach: Eine der nächsten Subklassen muss sich darum kümmern, dass die Beschreibung der Aktionen erfolgt, die beim Aufruf der Methode ausgeführt werden sollen. Wir verschieben also die Beschreibung der Aktionen in der Methode StarteMotor() von der Klasse Auto auf einen der Erben. Dabei gilt die Regel, dass spätestens der erste nicht-abstrakte Erbe, in unserem Fall also die Klasse A4, sich darum kümmern muss, dass die Aktionen beschrieben werden. Allerdings dürfen wir die ausführliche Beschreibung auch schon bei der Klasse Audi machen.
Das Gleiche gilt natürlich auch für die Klassen Bmw und Porsche. Alle Erben der Klasse Auto müssen sich zwingend um eine eigene Liste von Aktionen bemühen --- so lautet der Deal, wenn man von einer Klasse erben möchte, die abstrakte Methoden enthält.
Wir haben also wieder einmal mit minimalen Mitteln erreicht, dass wir die Wirklichkeit ein bisschen genauer abbilden können: Audi, Bmw, Porsche und all die anderen Erben müssen sich jetzt also herstellerspezifisch selbst darum kümmern, wie genau der Startvorgang des Motors abläuft.
Wir erwarten schon, dass auch diese Beschreibung ähnlich simpel funktioniert, nicht wahr? Und wir werden gewiss nicht enttäuscht. Auch hier hilft uns ein einzelnes Schlüsselwort.
Gehen wir dazu in unsere, mittlerweile abstrakte, Klasse Audi, wie wir sie im Abschnitt Vererbung ganz einfach bereits geschrieben haben. Falls noch nicht vorhanden, tragen wir jetzt das Schlüsselwort abstract nach, wie wir es oben in diesem Abschnitt gelernt haben:
abstract class Audi : Auto { public Audi(int nummer, string farbe) : base(nummer, farbe, "Audi") { } }
Alles, was wir jetzt machen müssen, ist eine Beschreibung der Aktionen für die Methode StarteMotor():
abstract class Audi : Auto { public Audi(int nummer, string farbe) : base(nummer, farbe, "Audi") { } public override void StarteMotor() { // Wir bauen hier die Grundlogik ein, // die Audi-spezifisch ist, // wenn der Motor gestartet wird. // Wir prüfen elektronische Bauteile, etc... // Damit wir etwas sehen, geben wir eine Information aus. Console.WriteLine("Motorcheck: Ok - Motor ist gestartet..."); } }
Das war's schon. Mehr müssen wir nicht machen, denn von jetzt an kann jedes Objekt von unserer Klasse A4 jederzeit den Motor starten. Auch das testen wir natürlich. (Wenn wir die Klasse A4, die wir im Abschnitt Vererbung ganz einfach gebaut haben, nicht zur Verfügung haben, holen wir das hier nach.)
// Schnellbau des Audi A4 class A4 : Audi { public A4(int nummer, string farbe) : base(nummer, farbe) { m_Typ += " A4"; } }
Jetzt können wir testen:
// Wir holen uns, wie gewohnt, das Objekt A4 a4 = new A4(1, "Rot"); // Können wir den Motor starten? a4.StarteMotor(); // Ausgabe = "Motorcheck: Ok - Motor ist gestartet..."
Das kleine Schlüsselwort
override
wirkt also wahre Wunder. Es teilt dem Compiler mit, dass wir die abstrakte - also "nicht greifbare" - Methode StarteMotor() aus der abstrakten Klasse Auto, jetzt mit Leben gefüllt haben, indem wir sie "überschreiben".
Quellcode: Abstrakte Klasse Auto mit abstrakten Methoden
[Bearbeiten]Unsere Klasse Auto sieht jetzt also vollständig so aus:
abstract class Auto { protected int m_Nummer; protected string m_Farbe; protected string m_Typ; public Auto(int nummer, string farbe, string typ) { m_Nummer = nummer; m_Farbe = farbe; m_Typ = typ; } // Eigenschaften public int Nummer { // Was soll passieren, // wenn jemand die Nummer lesen möchte? get { return m_Nummer; } // Was soll passieren, // wenn jemand die Nummer schreiben möchte? set { // Wir prüfen, ob der Wert größer als 0 // und kleiner oder gleich 2000 ist. // Wenn ja, dann übernehmen wir den Wert. // Wenn nein, dann ignorieren wir diesen Versuch, // fehlerhafte Daten einzugeben. if ((value > 0) && (value <= 2000)) m_Nummer = value; } } public string Farbe { // Was soll passieren, // wenn jemand die Farbe lesen möchte? get { return m_Farbe; } // Was soll passieren, // wenn jemand die Farbe schreiben möchte? set { m_Farbe = value; } } public string Typ { get { return m_Typ; } } public abstract void StarteMotor(); public void Beschleunige() {} public void Hupe() {} public void Blinke(bool links, bool rechts) { // Sollen wir links blinken? if (links == true && rechts == false) { Console.WriteLine("Links blinken"); } // Sollen wir rechts blinken? if (links == false && rechts == true) { Console.WriteLine("Rechts blinken"); } // Oder ist der Warnblinker an? if (links == true && rechts == true) { Console.WriteLine("Der Warnblinker ist an."); } } }