Kihagyás

Schnittstellen und abstrakte (Basis-)Klassen

Dieses Kapitel enthält keine Übungsaufgabe, sondern stellt den Studierenden die zugehörige Theorie vor.

Abstrakte Klasse

Die Konzepte wurden bereits in früheren Kursen behandelt, daher fassen wir hier nur das Wichtigste zusammen und gehen speziell auf die Umsetzung in C# ein.

Eine abstrakte Klasse ist eine Klasse, die nicht instanziiert werden kann. In C# muss das Schlüsselwort abstract in die Klassendefinition geschrieben werden, z. B.:

abstract class Shape {  }

Abstrakte Klassen können abstrakte Methoden enthalten, bei denen kein Methodenkörper angegeben wird. Auch hier muss das Schlüsselwort abstract verwendet werden:


abstract void Draw();

Die Verwendung abstrakter Klassen kann zwei Ziele haben:

  • In einer Klassenhierarchie kann gemeinsamer Code der abgeleiteten Klassen in eine abstrakte gemeinsame Basisklasse ausgelagert werden, wodurch Code-Duplikationen vermieden werden.
  • Abgeleitete Klassen können einheitlich über die abstrakte Basisklasse referenziert und behandelt werden (z. B. in heterogenen Sammlungen).

In der .NET-Umgebung, ebenso wie in der Programmiersprache Java, kann eine Klasse nur eine einzige Basisklasse haben.

Schnittstelle

Ein Interface ist nichts anderes als eine Menge von Operationen. Im Grunde entspricht es einer abstrakten Klasse, bei der alle Methoden abstrakt sind.

In C# kann ein Interface mit dem Schlüsselwort interface definiert werden:

public interface ISerializable 
{
   void WriteToStream(Stream s);
   void LoadFromStream(Stream s);
}

public interface IComparable 
{
   int CompareTo(Object obj);
}

Während eine Klasse nur eine Basisklasse haben kann, kann sie beliebig viele Schnittstellen implementieren:

public class Rect : Shape, ISerializable, IComparable
{
    
}

In diesem Beispiel erbt die Klasse Rect von der Klasse Shape und implementiert außerdem die Schnittstellen ISerializable und IComparable (wobei die Basisklasse zwingend zuerst angegeben werden muss). In der Klasse, die eine Schnittstelle implementiert, müssen alle Operationen der Schnittstelle implementiert werden, das heißt, der Methodenkörper muss geschrieben werden (außer in dem seltenen Fall, dass eine abstrakte Methode implementiert wird).
Die Verwendung von Schnittstellen hat einen Hauptzweck. Als Schnittstelle referenziert, können alle Klassen, die die Schnittstelle implementieren, einheitlich behandelt werden. (z. B. heterogene Sammlungen). Eine Folge davon ist: Schnittstellen ermöglichen das Schreiben von breit verwendbaren Klassen und Funktionen. Zum Beispiel kann man eine universelle Sortierfunktion schreiben, die mit jeder Klasse verwendet werden kann, die die Schnittstelle IComparable implementiert.

Weitere Vorteile der Verwendung von Schnittstellen sind:

  • Der Client muss nur die Schnittstelle des Service-Objekts kennen, so kann er den Service einfach verwenden.
  • Wenn der Client den Service nur über die Schnittstelle nutzt, kann sich die interne Implementierung des Service ändern, ohne dass der Client angepasst (oder neu kompiliert) werden muss. Dementsprechend stellt die Schnittstelle auch einen Vertrag zwischen Service und Client dar: solange der Service die Schnittstelle garantiert, muss der Client nicht geändert werden.

Vergleich von abstrakter Basisklasse und Schnittstelle

Der Vorteil der abstrakten Basisklasse gegenüber der Schnittstelle besteht darin, dass man für Operationen eine Standardimplementierung bereitstellen und auch Membervariablen definieren kann.

Der Vorteil der Schnittstellen gegenüber der abstrakten Basisklasse ist, dass eine Klasse beliebig viele Schnittstellen implementieren kann, während sie nur eine Basisklasse haben kann.

Die Verwendung von Schnittstellen hat noch eine weitere Folge, die in bestimmten Fällen unangenehm sein kann. Wenn man einer Schnittstelle eine neue Methode hinzufügt, müssen alle implementierenden Klassen ebenfalls erweitert werden, sonst kompiliert der Code nicht. Beim Erweitern einer abstrakten Basisklasse ist das nicht so: dort kann die neue Methode als virtuelle Funktion mit einer Standardimplementierung eingeführt werden. In diesem Fall können die Unterklassen die Methode bei Bedarf überschreiben, sind dazu aber nicht gezwungen. Diese Eigenschaft von Schnittstellen kann insbesondere bei Klassenbibliotheken oder Frameworks problematisch sein. Nehmen wir an, bei der Veröffentlichung einer neuen .NET-Version wird eine neue Funktion zu einer der Schnittstellen des Frameworks hinzugefügt. Dann müssten alle implementierenden Klassen in allen Anwendungen geändert werden, sonst kompiliert der Code nicht. Das kann man auf zwei Arten vermeiden: Entweder durch die Verwendung einer Basisklasse oder indem man, falls eine Schnittstelle erweitert werden muss, besser eine neue Schnittstelle mit der neuen Methode einführt. Obwohl die erste Variante (Verwendung einer Basisklasse) auf den ersten Blick attraktiver wirkt, hat sie auch Nachteile: Wenn man von einer Framework-Basisklasse ableitet, kann die eigene Klasse keine andere Basisklasse mehr haben, was oft eine schmerzhafte Einschränkung ist.

Es lohnt sich zu wissen, dass ab C# 8 (bzw. .NET oder .NET Core Runtime, nicht unterstützt im .NET Framework) können Schnittstellen auch Standardimplementierungen für Methoden enthalten (default interface methods), wodurch das oben genannte Problem gelöst werden kann, ohne dass eine abstrakte Klasse benötigt wird. Allerdings können Schnittstellen weiterhin keine Membervariablen haben. Mehr Informationen dazu hier: default interface methods.

Da sowohl die Verwendung von Schnittstellen als auch von abstrakten Basisklassen Nachteile haben kann, nutzt man in vielen Fällen eine Kombination beider Konzepte, um das Maximum aus der Lösung herauszuholen (das heißt, der Code wird leicht erweiterbar, ohne oder mit nur minimaler Code-Duplikation).


2025-06-02 Szerzők