Arbeiten mit .NET: OOP/ Verknüpfung/ Interface/ Vergleich mit abstrakten Klassen

ProblemBearbeiten

Nehmen wir uns einmal folgende Klasse her:

abstract class Klasse
{
  public abstract string Eigenschaft1 {get;}

  public abstract void Methode1();
  public abstract void Methode2();
}

Und schauen wir uns eine Schnittstelle an:

interface ISchnittstelle
{
  string Eigenschaft1 {get;}

  void Methode1();
  void Methode2();
}

Was unterscheidet diese beiden voneinander? Die Zugriffsmodifizierer? Nicht wirklich, denn Schnittstellen dürfen weder Zugriffsmodifizierer (public, protected, private oder internal) noch andere Schlüsselwörter, wie etwa abstract oder virtual, deklarieren.

Und tatsächlich: Beide sind völlig identisch. Sie erzwingen beide gleichermaßen, dass die Erben der abstrakten Klasse bzw. die Abonnenten der Schnittstelle sich um die spezielle Implementierung der Eigenschaften und Methoden kümmern müssen. Eigenschaften und Methoden der Schnittstellen sind also implizit abstract.

Warum macht man dann aber diesen Hokuspokus mit zwei verschiedenen Dingen, die beide das Gleiche bewirken?

Abstrakte Klassen vs. SchnittstellenBearbeiten

Diese Diskussion ist so alt, wie es abstrakte Klassen und Schnittstellen gibt. Und sie wurde auch an vielen Orten im Internet schon ausführlich geführt. Daher beschränken wir uns auf zwei nahe liegende Dinge:

Abstrakte Klassen können - müssen aber nicht - Eigenschaften 
und Methoden vollständig implementieren. 
Schnittstellen können dies niemals.
Schnittstellen stellen ultimative Verträge dar.
Jede hier erwähnte Eigenschaft oder Methode muss zwingend
in der Klasse realisiert werden, die diese Schnittstelle implementiert.

Das hat weit reichende Auswirkungen auf unsere Software.

Auch wenn es grundsätzlich egal zu sein scheint, ob man eine abstrakte Klasse mit abstrakten Eigenschaften und Methoden oder einfach eine Schnittstelle nehmen sollte, ist es ratsam, sich genau zu überlegen, welche Option dem eigenen Zweck dienlicher ist.

Meistens entwickeln wir Software nicht mehr als Wegwerf-Produkt. Vielmehr soll die Software über viele Jahre oder Zyklen ordentlich wachsen und gedeihen. Und genau hier setzt unsere Überlegung an:

Darf es eine Schnittstelle sein?Bearbeiten

Besteht die Möglichkeit, dass die Schnittstelle später geändert werden kann, soll oder muss? Wenn ja, haben wir ein Problem. Wahlweise müssen wir dann alle Klassen, die diese Schnittstelle implementieren, ebenfalls bearbeiten, oder wir versionieren die Schnittstellen:

interface ISchnittstelle2 : ISchnittstelle
{
  void Methode3 ();
}

Häufige Änderungen können also schnell zu einem unübersichtlichen Berg an Schnittstellen-Versionen führen. Andererseits garantiert uns genau dieses scheinbar negative Verhalten auch positive Auswirkungen; denn wir können uns auf die absolute Vertragstreue verlassen. Ändert sich der Vertrag auch nur minimal, bemerken das alle "Unterzeichner" des Vertrages sofort und treten in den Streik.

Oder doch lieber eine abstrakte Klasse?Bearbeiten

Alternativ können wir natürlich abstrakte Klassen mit abstrakten Methoden und Eigenschaften nehmen. Auch hier gilt, dass alle neuen abstrakten Methoden und Eigenschaften ebenfalls in der Vererbungs-Hierarchie implementiert werden müssen. Darüber hinaus lassen sich aber auch nicht-abstrakte Eigenschaften und Methoden in der abstrakten Klasse deklarieren, die dann implizit mitvererbt werden, ohne dass die Erben dies bemerken müssen. Ein feiner Aspekt, wenn man später feststellt, dass die ursprüngliche Beschreibung der abstrakten Klasse nicht vollständig war oder einfach heute nicht mehr ganz aktuell ist. Aber wehe, wenn sie es bemerken oder diese neuen Features sich mit den Eigenschaften oder Methoden der Kinder beißen. Dann kann es ganz schnell zu bösen Seiteneffekten kommen.

Wie es passieren kann, dass die Kinder nicht merken, wenn die Eigenschaften oder Methoden der abstrakten Eltern geändert werden?

// Unsere Beispielklasse besitzt 
// die abstrakte Nur-Lese-Eigenschaft "Name".
abstract class Parent
{
  protected string m_Name;

  public abstract string Name { get; }
}

// Unser Kind erbt von dieser Klasse.
class Child : Parent
{

  public override string Name
  {
    get { return m_Name; }
  }
}

Bis hierher ist das Kind Child im festen Glauben, dass m_Name von außen nicht geändert werden kann. Dementsprechend "nachlässig" wird auch die Überwachung der Variablen m_Name implementiert. Bösartige Angriffe sind eben nicht zu erwarten. Nun ändern wir die Basisklasse Parent ein wenig ab:

// Modifizierte Beispielklasse 
abstract class Parent
{
  protected string m_Name;

  public abstract string Name { get; }

  protected void ChangeValue()
  {
    m_Name = "Hier ist geändert worden";
  }
}

Unsere Subklasse Child interessiert diese Änderung nicht. Sie bemerkt es nicht einmal. Und doch hat die Variable m_Name auf einmal einen unerwarteten Wert... Dabei ist die Variante, dass die Subklasse Child ihrerseits bereits eine eigene Methode ChangeValue() hat, noch die beste aller Möglichkeiten. Schließlich bricht unser Programm dann wenigstens gleich ganz zusammen. Viel schlimmer wäre es, wenn wir den Wert der Variablen m_Name beispielsweise für die Lohnabrechnung benutzen würden und am Monatsende feststellen müssten, dass die Gehälter nicht nur nicht überwiesen wurden, sondern vielleicht sogar irgendwo verschwunden sind, nur weil die abstrakte Klasse klammheimlich den Namen des Empfängers geändert hat.

SchlussfolgerungBearbeiten

Es gibt keine ultimativ richtige Lösung. Die Entscheidung ist spezifisch, wird von vielen Faktoren beeinflusst und beeinflusst ihrerseits nachhaltig unsere Software. Wir sollten uns also jedesmal, wenn wir auf diese Frage stoßen, wirklich ausreichend Zeit nehmen, genau zu analysieren, was passieren könnte, wenn...