Schnittig: Interfaces in .NET

Gilbert: Interface Design

Von Interfaces hat wohl schon jeder, der Java, C++ / COM, .NET / C# oder eine andere Sprache, welche diese nützlichen Dinger unterstützt, gehört. Aber vielen Entwicklern ist oftmals nicht klar, warum man denn ein Interface implementieren soll – oder genauer: Wann und warum man denn lieber mit einem Interface als mit einer Klassenimplementierung arbeiten sollte.

Unter objektorientierter Betrachtungsweise haben Interfaces eine ganz klare Aufgabe: Die Trennung von Schnittstelle und Implementierung. Interfaces stellen eine vollkommen losgelöste Schnittstellenbeschreibung dar, die im Grunde nur Methoden und Properties beschreibt, deren tatsächliche Implementierung aber vollständig offen lässt. Diese erfolgt erst durch die Klasse, welche ein Interface implementiert.

Wann ist nun aber der Einsatz von Interfaces in .NET sinnvoll? Hier existieren zwei Hauptszenarien: Zum einen unterstützt .NET – genau wie Java und im Gegensatz zu C++ – keine Mehrfachvererbung. Deswegen sind Konstruktionen wie die Folgende nicht möglich:


class Auto
{
  public bool FährtAufLand
  {
    get
    {
      return true;
    }
  }
}
 
class Boot
{
  public bool FährtAufWasser
  {
    get
    {
      return true;
    }
  }
}
 
class Amphibienfahrzeug: Auto, Boot
{
}

An dieser Stelle helfen einem nun Interfaces weiter. Denn im Gegensatz zur unmöglichen Mehrfachvererbung darf eine Klasse beliebig viele Interfaces implementieren:


interface IAuto
{
  bool FährtAufLand { get; }
}
 
interface IBoot
{
  bool FährtAufWasser { get; }
}
 
class Auto: IAuto
{
  public bool FährtAufLand
  {
    get
    {
      return true;
    }
  }
}
 
class Boot: IBoot
{
  public bool FährtAufWasser
  {
    get
    {
      return true;
    }
  }
}
 
class Amphibienfahrzeug: IAuto, IBoot
{
  public bool FährtAufLand
  {
    get
    {
      return true;
    }
  }
 
  public bool FährtAufWasser
  {
    get
    {
      return true;
    }
  }
}

Besitzt man mehrere Amphibienfahrzeuge kann man natürlich dafür auch gleich ein eigenes Interface basteln:


interface IAmphibienfahrzeug: IAuto, IBoot
{
}

Soweit – so gut. Das Problem, welches man nun hat, ist, dass man die eigentliche Implementierung ja in seiner Amphibienfahrzeug-Klasse vornehmen muss. Existieren aber bereits Auto- und Boot-Klassen, so führt das zwangsläufig zu Redundanz, da die Properties FährtAufLand und FährtAufWasser ja in Auto, Boot und Amphibienfahrzeug implementiert werden müssen. Wesentlich schlauer wäre es, wenn die Amphibienfahrzeug-Klasse direkt auf die von Auto und Boot implementierten Properties zurückgreifen könnte. Für die Lösung dieses Problems scheint das Stellvertreter-Entwurfsmuster (engl. Proxy [Design-]Pattern) geradezu prädestiniert: Wir implementieren das Amphibienfahrzeug so, dass es die entsprechenden Aufrufe an Auto und Boot weiterleitet:


class Amphibienfahrzeug: IAmphibienfahrzeug
{
  private IAuto m_auto = new Auto();
  private IBoot m_boot = new Boot();
 
  public bool FährtAufLand
  {
    get
    {
      return m_auto.FährtAufLand;
    }
  }
 
  public bool FährtAufWasser
  {
    get
    {
      return m_boot.FährtAufWasser;
    }
  }
}

Nun müssen die beiden Properties zwar immer noch im Amphibienfahrzeug implementiert werden – aber wenn man einmal annimmt, dass beide Properties eine 100 Zeilen lange Implementierung benötigen, hat man den Aufwand doch enorm reduziert. Außerdem kann man jetzt in der Amphibienfahrzeug-Klasse kontrollieren, ob der Aufruf an Boot und Auto weitergereicht werden oder ein eigener Wert zurück geliefert werden soll (Amphibienfahrzeug fährt zwar wie Boot auf Wasser, aber nicht wenn Tür offen).

Die oben angesprochene, zweite gute Einsatzmöglichkeit für Interfaces ist die komponentenbasierende Softwareentwicklung. Nehmen wir an, ich entwickle ein Programm, welches durch Plugins erweiterbar sein soll. Hier bietet es sich an, ein Interface für solche Plugins zu definieren:


public interface IPlugin
{
  string Name { get; };
  void TuEtwas();
}

Weiterhin stelle ich im Programm einen Lademechanismus für Plugins zur Verfügung. Welche Plugins konkret geladen werden sollen lässt sich ja beliebig festlegen (alle aus einem bestimmten Verzeichnis, in einer Konfigurationsdatei angegebene, manuell ausgewählte…). Der Ladevorgang könnte z.B. so aussehen:


void LoadPlugins(IEnumerable<string> pluginFilesToLoad)
{
  foreach (string pluginFile in pluginFilesToLoad)
  {
    Assembly asm = Assembly.LoadFrom(pluginFile);
    foreach (Type t in asm.GetTypes())
    {
      if (typeof(IPlugin).IsAssignableFrom(t))
      {
        IPlugin plugin = Activator.CreateInstance(t) as IPlugin;
        if (null != plugin)
          m_loadedPlugins.Add(plugin);
      }
    }
  }
}

Nun kann jeder, der Lust hat, mein Programm durch Plugins erweitern. Um die Abhängigkeiten zwischen Plugins und dem eigentlichen Programm zu minimieren ist es sinnvoll, die Interface-Definition in ein separates Assembly zu packen, welches dann sowohl vom Programm als auch von den Plugins referenziert wird. Das hat den großen Vorteil, dass die Plugins nicht neu übersetzt werden müssen, wenn sich das eigentliche Programm (oder z.B. auch nur seine Assembly-Version) ändert.

Assembly-Struktur mit separatem Schnittstellen-Assembly

Natürlich gibt es durchaus noch weitere Szenarien, in denen der Einsatz von Interfaces sinnvoll ist. Die beiden angesprochenen sind aber diejenigen, welche mir bisher am häufigsten über den Weg gelaufen sind.

1 Kommentar