ein Projekt von goloroden.de

Schnittstellen

Was sind Schnittstellen?

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.

Benutzerdefinierte Schnittstellen

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.

Schnittstellen implementieren

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
    }
}