Abstrakte Basisklassen werden häufig eingesetzt, um semantisch verwandte abgeleitete
Klassen mit einer gemeinsamen Basisklasse auszustatten und gemeinsam genutzte Methoden
in einem einzigen Typ zur Verfügung zu stellen. Allerdings birgt der Einsatz abstrakter
Klassen einen entscheidenden Nachteil: Gelegentlich ist es notwendig, eine Klasse von
einer Basisklasse der Framework Class Library abzuleiten.
Da eine Klasse aber nur über eine Basisklasse verfügen kann, können solche abgeleiteten
Klassen nicht mehr unter einer benutzerdefinierten abstrakten Basisklasse angeordnet
werden. In Sprachen, die Mehrfachvererbung unterstützen, können einer Klasse in einem
solchen Fall einfach mehrere Basisklassen zugeordnet werden, in C# ist dies jedoch nicht
möglich.
Die Lösung liegt in sogenannten Schnittstellen, die abstrakten Klassen sehr ähnlich sind,
da sie ebenfalls Methodendefinitionen enthalten, aber im Gegensatz zu Klassen mehrfach
vererbt werden können. Die einzige Einschränkung einer Schnittstelle ist, dass sie keine
Implementierung enthalten können, sondern auf die Methodendefinitionen beschränkt sind.
Insofern entspricht eine Schnittstelle einer vollständig abstrakten Klasse.
In der modernen, komponentenorientierten Entwicklung von Anwendungen spielen Schnittstellen
noch eine weitere, zusätzliche Rolle. Da sie mit den in ihnen enthaltenen Methodendefinitionen
nicht nur eine syntaktische Vorgabe leisten, sondern auch eine gewisse Semantik vorgeben,
werden sie als eine Art Vertrag für Komponenten eingesetzt - sofern zwei verschiedene
Komponenten die gleiche Schnittstelle implementieren, können sie als semantisch äquivalent
eingestuft werden und sind damit untereinander austauschbar.
Wenn dieser Aspekt von Schnittstellen besonders hervorgehoben werden soll, wird an Stelle
von Schnittstelle häufig auch von Kontrakt gesprochen. In der Regel werden bei der Entwicklung
von Komponenten zunächst die Kontrakte definiert, bevor Komponenten entwickelt werden, die
deren abstrakte Semantik konkret umsetzen. Daher spricht man auch von Contract First Design
oder Design by Contract.
Contract First Design bietet noch einen weiteren Vorteil. Da die Semantik vollständig über
den Kontrakt definiert ist, ist es möglich, den Zugriff auf eine Komponente ausschließlich
über deren Schnittstelle zu gestalten. Wenn die Komponente eines Tages gegen eine andere,
aber semantisch äquivalente Komponente ausgetauscht werden soll, muss an der Anwendung
an sich nichts geändert werden, da die Schnittstelle gleich geblieben ist.
Schnittstellen werden in C# mit Hilfe des Schlüsselwortes
interface definiert, wobei ihr
sonstiger Aufbau dem einer abstrakten Klasse ähnelt. Das bedeutet, dass in einer Schnittstelle
wie in einer vollständig abstrakten Klasse nur Methodendefinitionen enthalten sein können,
im Gegensatz zu diesen allerdings keine Zugriffsmodifizierer angegeben werden können. Alle
Methoden sind implizit
public, um den Charakter eines Kontraktes zu erfüllen.
Als Namensrichtlinie für Schnittstellen gibt es zwei Varianten. Für beide Varianten gilt,
dass der Name in Pascal Case genannt wird, wobei ihm zusätzlich ein großes I vorangestellt
wird. Der Name besteht entweder aus einem Adjektiv, das eine Eigenschaft beschreibt, die
mit Hilfe der Schnittstelle umgesetzt wird, oder aus einem Substantiv, sofern die
Schnittstelle an Stelle einer Klasse verwendet wird.
Im Namensraum System gibt es zahlreiche Beispiele für beide Varianten: Die Schnittstelle
ICloneable wird von allen Klassen implementiert, deren Objekte klonbar sind - die
Schnittstelle beschreibt also eine Eigenschaft, weshalb für ihren Namen ein Adjektiv
gewählt wurde. Hingegen wird die Schnittstelle IServiceProvider von solchen Klassen
implementiert, die Mechanismen zum Abrufen von Services bereitstellen. In diesem Fall
ersetzt IServiceProvider eine entsrechende Basisklasse, weshalb für den Namen ein
Substantiv gewählt wurde.
| C# |
1
2
3
4
5
6
7
8
9
10
11
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for persisting an object.
/// </summary>
public interface IPersistable
{
}
}
|
Dieser Code erzeugt eine Schnittstelle IPersistable, die dazu dient, das Entwurfsmuster
Memento zu implementieren. Memento ermöglicht es beliebigen Objekten, ihren Zustand zu
speichern und diesen zu einem späteren Zeitpunkt wieder abzurufen. Dazu werden die beiden
Methoden Store und Restore definiert, welche die Aufgabe des Speicherns und des
Wiederherstellens übernehmen.
Das Speichern der Daten übernimmt dabei ein spezielles Objekt, das sogenannte Memento.
Häufig wird dieses Entwurfsmuster eingesetzt, wenn die Absicht besteht, ein Objekt zu
ändern, vor der Änderung allerdings eine Kopie angefertigt werden soll, um im Falle des
Falles einen Rollback ausführen und damit auf den gespeicherten Stand zurückgreifen zu
können.
Da alle Methoden einer Schnittstelle implizit
public sind, kann die Angabe eines
Zugriffsmodifizierers entfallen. Da die Methoden einer Schnittstelle zudem implizit
abstrakt sind, werden ihre Definitionen jeweils mit einem Semikolon abgeschlossen, wie
es in einer vollständig abstrakten Klasse ebenfalls der Fall wäre.
Der Typ des Mementos, welches die zu speichernden Daten aufnimmt und den beiden Methoden
als Parameter übergeben wird, wird ebenfalls als Schnittstelle angegeben - auf diese Art
kann die konkrete Klasse, welche die Funktionalität des Mementos bereitstellt, problemlos
ausgetauscht werden. Die einzige Voraussetzung dafür ist, dass sie die Schnittstelle
IMemento implementiert.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for persisting an object.
/// </summary>
public interface IPersistable
{
/// <summary>
/// Stores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Store(IMemento memento);
/// <summary>
/// Restores the current instance to the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void Restore(IMemento memento);
}
}
|
Damit der Code kompiliert werden kann, muss zusätzlich noch die Schnittstelle IMemento
definiert werden, die Methoden zum Speichern und Wiederherstellen von Daten enthält. Da
das Memento zunächst nur in Verbindung mit der Klasse ComplexNumber eingesetzt werden
soll, sind Methoden zum Speichern und Wiederherstellen von Daten des Typs
float
ausreichend.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, float value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
float Restore(string key);
}
}
|
Das Prizip der Vererbung ist auch bei Schnittstellen möglich: Schnittstellen können als
Basisschnittstelle für abgeleitete Schnittstellen dienen. Dies geschieht wie bei Klassen,
indem bei der Definition der Schnittstelle die Basisschnittstelle durch den Operator :
angehängt wird. Es kann also eine spezialisierte Version von IMemento für die Klasse
ComplexNumber namens IMementoComplexNumber erzeugt werden.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Provides methods for memento classes.
/// </summary>
public interface IMemento
{
/// <summary>
/// Stores the specified value using the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
void Store(string key, float value);
/// <summary>
/// Restores the value stored with the specified
/// key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
float Restore(string key);
}
/// <summary>
/// Provides methods for a memento for the
/// ComplexNumber class.
/// </summary>
public interface IMementoComplexNumber : IMemento
{
/// <summary>
/// Stores the real value of a complex number.
/// </summary>
/// <param name="value">The real value.</param>
void StoreRealValue(float value);
/// <summary>
/// Restores the real value of a complex number.
/// </summary>
/// <returns>The real value.</returns>
float RestoreRealValue();
/// <summary>
/// Stores the imaginary value of a complex number.
/// </summary>
/// <param name="value">The imaginary value.</param>
void StoreImaginaryValue(float value);
/// <summary>
/// Restores the imaginary value of a complex number.
/// </summary>
/// <returns>The imaginary value.</returns>
float RestoreImaginaryValue();
}
}
|
Neben Methoden können in Schnittstellen auch Eigenschaften mit Definitionen für
get
und
set vorgegeben werden. Felder und Konstruktoren sind hingegen ausgeschlossen,
diese können nur in einer abstrakten oder konkreten Klasse definiert werden.
Schließlich stellt sich die Frage, wann eine Schnittstelle und wann eine abstrakte
Basisklasse eingesetzt werden sollte. Prinzipiell bieten Schnittstellen den Vorteil,
dass sie mehr Flexibilität bereitstellen, da eine Klasse zum einen von mehreren
Schnittstellen ableiten kann - aber nur von einer Basisklasse - , und zum anderen eine
Trennung zwischen Kontrakt und eigentlicher Implementierung besteht.
Des weiteren lässt der Einsatz von Schnittstellen die Möglichkeit bestehen, nach wie vor
von einer Klasse ableiten zu können, was unter Umständen nötig ist, wenn eine Klasse
beispielsweise eine bestimmte Klasse der Framework Class Library abgeleitet werden
soll.
Eine abstrakte Basisklasse verfügt jedoch über einen wesentlichen Vorteil: Im Gegensatz
zu Schnittstellen kann sie nicht nur Methodendefinitionen, sondern auch Code enthalten.
Falls also von zahlreichen Klassen gemeinsam genutzter Code besteht, kann eine abstrakte
Basisklasse helfen, die Redundanz zu vermindern und die Wartbarkeit zu verbessern.
Nachdem die Schnittstellen IPersistable, IMemento und IMementoComplexNumber definiert
wurden, können diese nun von der Klasse ComplexNumber verwendet werden. Werden
Schnittstellen von einer Klasse implementiert, werden diese genauso wie Basisklassen
mit dem Operator : angegeben, wobei mehrere Schnittstellen kommasepariert aufgezählt
werden. Wird so wohl eine Basisklasse wie auch mindestens eine Schnittstelle angegeben,
muss die Basisklasse vor den Schnittstellen genannt werden.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Represents a complex number.
/// </summary>
public sealed class ComplexNumber : IPersistable
{
#region Properties
#endregion
#region Methods
#endregion
#region Constructors
#endregion
}
}
|
Da die Klasse ComplexNumber nun die Schnittstelle IPersistable implementiert, muss sie
die beiden Methoden Store und Restore der Schnittstelle bereitstellen und mit Inhalt
füllen. Dazu werden die beiden Methoden implementiert, als handele es sich um native
Methoden der Klasse ComplexNumber.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Represents a complex number.
/// </summary>
public sealed class ComplexNumber : IPersistable
{
#region Properties
#endregion
#region Methods
// ...
/// <summary>
/// Stores the current instance in the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Store(IMemento memento)
{
// TODO gr: Store the current instance.
// 2007-06-25
}
/// <summary>
/// Restores the current instance from the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Restore(IMemento memento)
{
// TODO gr: Restore the current instance.
// 2007-06-25
}
#endregion
#region Constructors
#endregion
}
}
|
Diese Variante der Implementierung wird implizit genannt, da implizit gegeben ist, aus
welcher Schnittstelle die Definition der entsprechenden Methode stammt. Werden von einer
Klasse mehrere Schnittstellen implementiert, kann es allerdings zu Mehrdeutigkeiten
kommen, wenn zwei Schnittstellen beispielsweise eine gleichnamige Methode definieren.
Für diesen Fall gibt es die explizite Implementierung, bei der dem Methodennamen der
Name der Schnittstelle samt dem Operator . vorangestellt wird. Wird eine Methode explizit
implementiert, darf kein Zugriffsmodifizierer angegeben werden.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Represents a complex number.
/// </summary>
public sealed class ComplexNumber : IPersistable
{
#region Properties
#endregion
#region Methods
// ...
/// <summary>
/// Stores the current instance in the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void IPersistable.Store(IMemento memento)
{
// TODO gr: Store the current instance.
// 2007-06-25
}
/// <summary>
/// Restores the current instance from the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
void IPersistable.Restore(IMemento memento)
{
// TODO gr: Restore the current instance.
// 2007-06-25
}
#endregion
#region Constructors
#endregion
}
}
|