CodeGym /Kurse /C# SELF /Interface-Verträge

Interface-Verträge

C# SELF
Level 23, Lektion 3
Verfügbar

1. Einführung

Also, wir haben gesehen, dass Interfaces mächtige Verträge sind. Aber was ist eigentlich Programmieren auf Interface-Ebene? Das ist eine Design-Philosophie, die sagt: "Hänge dich an Abstraktionen, nicht an konkrete Implementierungen."

Nochmal ein Vergleich: Du willst Kaffee machen. Du kaufst dir eine Kaffeemaschine. Es ist dir doch egal, ob das das konkrete Modell "Super-Duper-Maschine V3000" oder "Mega-Automat Barista 5000" ist, oder? Wichtig ist, dass das Gerät "Kaffee machen" kann – also den "Vertrag" ICoffeeMaker erfüllt. Du drückst einfach auf "Kaffee machen" und es passiert. Wie genau – ob mit gemahlenen Bohnen, Kapseln oder sonst was – ist dir als Nutzer eigentlich egal.

Im Code heißt das: Anstatt in deinen Methoden einen konkreten Typ wie Document oder Image zu verlangen, verlangst du IPrintable. Dein Code funktioniert mit jedem Objekt, das dieses Interface implementiert, ohne den konkreten Typ zu kennen.

Interface-Vertrag – das ist die Garantie, dass jede Klasse, die das Interface implementiert, auch wirklich alle Mitglieder des Interfaces (Methoden, Properties, Events) bereitstellt. Aus Sicht des Programms unterstützt jedes Objekt, das das Interface implementiert, einen bestimmten "Satz an Fähigkeiten" – darauf kann man sich verlassen, auch ohne zu wissen, wie das innen drin gemacht ist.

Stell dir vor, du hast mit einem Kollegen abgemacht, dich immer an der roten Couch in der Lobby zu treffen. Wie er da hinkommt – mit dem Aufzug, zu Fuß oder an der Wand entlang – ist egal. Hauptsache: Er ist da. Das Interface ist quasi das "rote Sofa" für den Code.

Beispiel


public interface IPrintable
{
    void Print();
}

Vertrag: Jeder, der IPrintable implementiert, muss sich irgendwie auf dem Bildschirm ausgeben können. Wie – das sind Details.

2. Vertrag als Schnittstelle zwischen Systemteilen

Wie das in der Praxis läuft

Ein System kann aus Dutzenden Klassen bestehen, die von verschiedenen Leuten, zu verschiedenen Zeiten, mit unterschiedlichen Zielen gebaut wurden. Aber wenn sie sich alle an denselben Vertrag halten (also dasselbe Interface implementieren), kann man sie einheitlich benutzen.

Beispiel aus einer echten Anwendung: Stell dir eine Kundendatenbank vor. Die Klasse Customer, die Klasse Employee, die Klasse Contractor. Jede speichert Infos auf ihre Art, aber wenn alle z.B. das Interface IContactInfo implementieren, das die Methoden GetEmail() und GetPhoneNumber() verlangt, ist es für den restlichen Code egal, zu welchem Typ das Objekt gehört – Hauptsache, man kann Email und Telefonnummer bekommen.


public interface IContactInfo
{
    string GetEmail();
    string GetPhoneNumber();
}

public class Customer : IContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }

    public string GetEmail() => Email;
    public string GetPhoneNumber() => Phone;
}

// Genauso für Employee und Contractor...

Jetzt, wenn du die Daten von allen ausgeben willst, mit denen deine Firma Kontakt hat (egal wer das ist), gehst du einfach die Liste von IContactInfo durch, rufst die Methoden auf – und alles läuft.

3. Programmieren "auf Interface-Ebene"

Auf Interface-Ebene programmieren heißt, Code zu schreiben, der nicht von konkreten Klassen, sondern nur von Interfaces (also Verträgen) abhängt. Die Klasse, die das Interface implementiert, kann alles Mögliche sein, solange sie die Anforderungen des Interfaces erfüllt.

Warum ist das wichtig?

  • Skalierbarkeit: Neue Typen lassen sich leicht hinzufügen, ohne bestehenden Code zu ändern.
  • Testbarkeit: Man kann Objekte beim Testen easy durch Mocks (Stubs) ersetzen.
  • Flexibilität: Die Implementierung kann sich ständig ändern, das Interface bleibt stabil.
  • Saubere Architektur: Deine Module sind locker gekoppelt und wiederverwendbar.

Beispiel – Bekanntes Programm: "Bankkonto"

In früheren App-Beispielen hatten wir eine abstrakte Klasse BankAccount mit einer abstrakten Methode Withdraw(), und konkrete Typen (SavingsAccount, CheckingAccount) haben die Details umgesetzt.

Jetzt fügen wir mal ein Interface hinzu – zum Beispiel für die Ausgabe des Kontostands:


public interface IBalanceReporter
{
    void ReportBalance();
}

public abstract class BankAccount : IBalanceReporter
{
    public double Balance { get; set; }

    public abstract void Withdraw(double amount);

    public void ReportBalance()
    {
        Console.WriteLine($"Aktueller Kontostand: {Balance} Euro");
    }
}

Jetzt können alle, die mit IBalanceReporter arbeiten, ReportBalance() aufrufen, egal welcher Kontotyp das ist.

4. Gemeinsame Verarbeitung und Polymorphie durch Vertrag

Beispiel: Allgemeiner Handler

Sobald wir mit einem Interface arbeiten, können wir generische Methoden schreiben, die nicht vom Objekttyp abhängen:


static void PrintAllBalances(IBalanceReporter[] accounts)
{
    foreach (var reporter in accounts)
    {
        reporter.ReportBalance();
    }
}

In dieser Liste können beliebige Objekte sein, die IBalanceReporter implementieren: SavingsAccount, CheckingAccount, sogar ein MockAccountForTesting. Keine Magie, alles läuft ganz normal.

Schematisch: Was bringt ein Interface-Vertrag?


+-------------------+      implementiert     +-------------------+
|   BankAccount     |  <-------------------- |  IBalanceReporter |
|  (SavingsAccount) |                        |-------------------|
|  (CheckingAccount)|                        | + ReportBalance() |
+-------------------+                        +-------------------+
         |                                       ^
         |                                       |
         +-----------+---------------------------+
                     |
        Jede andere Klasse, die den Vertrag erfüllt
Skizze: Interface-Vertrag als Schnittstelle

5. Verträge, Business-Logik und Architektur

Der Interface-Vertrag trennt klar, was eine Klasse können muss, von wie sie das macht. Deshalb lieben Software-Architekten es, Logik über Interfaces zu designen. Oft wird erst das Interface (der Vertrag) gebaut, die Implementierung kommt später.

Beispiel aus dem Leben: Bezahlsysteme

E-Wallets, Karten, PayPal, Krypto... Jede hat ihre Eigenheiten, aber wenn man abstrahiert und ein Interface IPaymentProvider baut:


public interface IPaymentProvider
{
    void Pay(decimal amount);
    bool Refund(decimal amount);
}

Der Code, der mit diesem Interface arbeitet, ist es völlig egal, ob er mit Karte oder Konto zahlt. Das ist praktisch für die Architektur und im Alltag: Neue Bezahlsysteme kann man einfach einbauen, ohne den Rest zu ändern.

6. Business-Logik in den Vertrag auslagern

Der Vertrag erlaubt es, die wichtigsten Business-Regeln auf Interface-Ebene zu bringen, und Details (wie Limit-Prüfung, Cashback usw.) den konkreten Klassen zu überlassen.

Noch ein Beispiel


public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message);
}

public class ConsoleLogger : ILogger
{
    public void LogInfo(string message)  => Console.WriteLine($"INFO: {message}");
    public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}

public class FileLogger : ILogger
{
    public void LogInfo(string message)  => /* In Datei schreiben */;
    public void LogError(string message) => /* In Datei schreiben */;
}

Der Client-Code sieht dann so aus (egal, mit welchem Logger er arbeitet):


void DoWork(ILogger logger)
{
    logger.LogInfo("Arbeit gestartet.");
    // ... irgendwas machen ...
    logger.LogError("Arbeit lief schief.");
}

Das ist Programmieren auf Interface-Ebene: Der Client-Code hängt nur vom Vertrag (Interface) ab, nicht von der konkreten Implementierung.

7. Fehler und typische Fallen

Anfänger machen oft den Fehler, dass ihr Code hart an konkrete Klassen gebunden ist, nicht an Interfaces. Das führt zu vielen Problemen:

  • Es ist schwer, etwas auszutauschen oder zu testen – man muss viel Code ändern.
  • Neue Typen lassen sich nicht einbauen, ohne an vielen Stellen zu schrauben.
  • Module sind stark "verzahnt" miteinander.

Das Gegenteil: Wenn du von Anfang an alles auf Interfaces aufbaust und Parameter darüber gibst, erreichst du lose Kopplung und Flexibilität.

Der wichtigste Tipp: Denk bei der Zusammenarbeit von Modulen immer an Verträge, nicht an konkrete Implementierungen.

2
Aufgabe
C# SELF, Level 23, Lektion 3
Gesperrt
Programmieren auf Interface-Ebene
Programmieren auf Interface-Ebene
2
Aufgabe
C# SELF, Level 23, Lektion 3
Gesperrt
Universeller Handler über Schnittstellen
Universeller Handler über Schnittstellen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION