Tutorial: Aktualisieren von Schnittstellen mit Standardschnittstellenmethoden in C# 8.0

Ab C# 8.0 können Sie in .NET Core 3.0 eine Implementierung definieren, wenn Sie einen Member einer Schnittstelle deklarieren. Das häufigste Szenario ist das sichere Hinzufügen von Membern zu einer Schnittstelle, die bereits veröffentlicht ist und von unzähligen Clients verwendet wird.

In diesem Tutorial lernen Sie, wie die folgenden Aufgaben ausgeführt werden:

  • Erweitern Sie Schnittstellen problemlos durch Hinzufügen von Methoden mit Implementierungen.
  • Erstellen Sie parametrisierte Implementierungen, um größere Flexibilität zu bieten.
  • Ermöglichen Sie Implementierern, eine spezifischere Implementierung in Form einer Überschreibung zu bieten.

Voraussetzungen

Sie müssen Ihren Computer zur Ausführung von .NET Core einrichten, einschließlich des C# 8.0-Compilers. Der C# 8.0-Compiler steht ab Visual Studio 2019 Version 16.3 oder mit dem .NET Core 3.0 SDK zur Verfügung.

Übersicht über das Szenario

Dieses Tutorial beginnt mit Version 1 einer Kundenbeziehungsbibliothek. Sie erhalten die Startanwendung von unserem Beispielerepository auf GitHub. Das Unternehmen, das diese Bibliothek erstellt hat, beabsichtigte, dass Kunden mit vorhandenen Anwendungen seine Bibliothek verwenden. Minimale Schnittstellendefinitionen wurden bereitgestellt, die Benutzer ihrer Bibliothek implementieren sollten. So sieht die Schnittstellendefinition für einen Kunden aus:

public interface ICustomer
{
    IEnumerable<IOrder> PreviousOrders { get; }

    DateTime DateJoined { get; }
    DateTime? LastOrder { get; }
    string Name { get; }
    IDictionary<DateTime, string> Reminders { get; }
}

Eine zweite Schnittstelle wurde definiert, die eine Bestellung darstellt:

public interface IOrder
{
    DateTime Purchased { get; }
    decimal Cost { get; }
}

Von diesen Schnittstellen aus konnte das Team eine Bibliothek für die Benutzer erstellen, um den Kunden eine bessere Benutzererfahrung zu bieten. Das Ziel bestand darin, eine intensivere Beziehung zu Bestandskunden aufzubauen und ihre Beziehungen zu neuen Kunden zu verbessern.

Jetzt ist es Zeit, die Bibliothek für das nächste Release zu aktualisieren. Eines der angeforderten Features gewährt Kunden, die viele Bestellungen aufgeben, einen Treuerabatt. Dieser neue Treuerabatt wird angewendet, wenn ein Kunde eine Bestellung aufgibt. Der spezifische Rabatt ist eine Eigenschaft jedes einzelnen Kunden. Jede Implementierung von ICustomer kann andere Regeln für den Treuerabatt festlegen.

Die naheliegendste Methode zum Hinzufügen dieser Funktionalität ist die Verbesserung der ICustomer-Schnittstelle mit einer Methode zur Anwendung eines Treuerabatts. Diese Entwurfsempfehlung löste bei erfahrenen Entwicklern Bedenken aus: „Schnittstellen sind unveränderlich, sobald sie veröffentlicht sind! Dies ist ein breaking change!" C# 8.0 fügt Standardschnittstellenimplementierungen zum Aktualisieren von Schnittstellen hinzu. Die Autoren der Bibliothek können der Schnittstelle neue Member hinzufügen und eine Standardimplementierung für diese Member bereitstellen.

Mit Implementierungen von Standardschnittstellen können Entwickler eine Schnittstelle aktualisieren, während gleichzeitig alle Implementierer diese Implementierung überschreiben können. Benutzer der Bibliothek können die standardmäßige Implementierung als eine nicht unterbrechende Änderung akzeptieren. Wenn ihre Geschäftsregeln anders sind, können sie überschreiben.

Upgraden mit Standardschnittstellenmethoden

Das Team stimmte der wahrscheinlichsten Standardimplementierung zu: einem Treuerabatt für Kunden.

Das Upgrade sollte die Funktionalität zum Festlegen von zwei Eigenschaften bieten: die für den Rabatt erforderliche Anzahl an Bestellungen sowie den Prozentsatz des Rabatts. Damit wird es zum idealen Szenario für Standardschnittstellenmethoden. Sie können der ICustomer-Schnittstelle eine Methode hinzufügen und die wahrscheinlichste Implementierung bereitstellen. Alle vorhandenen und alle neuen Implementierungen können die Standardimplementierung verwenden oder ihre eigene angeben.

Fügen Sie zunächst die neue Methode einschließlich deren Text der Schnittstelle hinzu:

// Version 1:
public decimal ComputeLoyaltyDiscount()
{
    DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
    if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
    {
        return 0.10m;
    }
    return 0;
}

Der Bibliotheksautor schrieb einen ersten Test zum Überprüfen der Implementierung:

SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
    Reminders =
    {
        { new DateTime(2010, 08, 12), "childs's birthday" },
        { new DateTime(1012, 11, 15), "anniversary" }
    }
};

SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);

o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o);

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

Beachten Sie den folgenden Teil des Tests:

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

Diese Umwandlung von SampleCustomer zu ICustomer ist erforderlich. Die SampleCustomer-Klasse muss keine Implementierung für ComputeLoyaltyDiscount bereitstellen; dies erfolgt über die ICustomer-Schnittstelle. Allerdings erbt die SampleCustomer-Klasse keine Member von ihren Schnittstellen. Diese Regel hat sich nicht geändert. Um jede in der Schnittstelle deklarierte und implementierte Methode aufrufen zu können, muss die Variable vom Typ der Schnittstelle sein, in diesem Beispiel ICustomer.

Bereitstellen der Parametrisierung

Ein guter Anfang. Aber die Standardimplementierung ist zu restriktiv. Viele Nutzer dieses Systems könnten unterschiedliche Schwellenwerte für die Anzahl der Käufe, eine andere Dauer der Mitgliedschaft oder einen anderen Rabattprozentsatz auswählen. Sie können mehr Kunden eine bessere Upgradeerfahrung bieten, indem Sie eine Möglichkeit zum Festlegen dieser Parameter bereitstellen. Wird fügen nun eine statische Methode hinzu, die diese drei, die Standardimplementierung steuernden Parameter festlegt:

// Version 2:
public static void SetLoyaltyThresholds(
    TimeSpan ago,
    int minimumOrders = 10,
    decimal percentageDiscount = 0.10m)
{
    length = ago;
    orderCount = minimumOrders;
    discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;

public decimal ComputeLoyaltyDiscount()
{
    DateTime start = DateTime.Now - length;

    if ((DateJoined < start) && (PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

Dieses kleine Codefragment zeigt viele neue Sprachfunktionen. Schnittstellen können nun statische Member einschließlich Feldern und Methoden enthalten. Verschiedene Zugriffsmodifizierer sind ebenfalls aktiviert. Die zusätzlichen Felder sind privat, die neue Methode ist öffentlich. Beliebige der Modifizierer sind auf Schnittstellenmembern erlaubt.

Anwendungen, die die allgemeine Formel zum Berechnen des Treuerabatts verwenden, aber andere Parameter, müssen keine benutzerdefinierte Implementierung bereitstellen; sie können die Argumente über eine statische Methode festlegen. Der folgende Code legt z.B. eine „Kundenwertschätzung“ fest, die jeden Kunden mit mehr als einem Monat Mitgliedschaft belohnt:

ICustomer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

Erweitern der Standardimplementierung

Der Code, den Sie bisher hinzugefügt haben, hat eine einfache Implementierung für diese Szenarien ermöglicht, in denen Benutzer etwas wie die Standardimplementierung wünschen, oder um eine unzusammenhängende Gruppe von Regeln bereitzustellen. Für ein finales Feature werden wir den Code ein wenig umgestalten, um Szenarien zu ermöglichen, in denen Benutzer die Standardimplementierung erstellen möchten.

Stellen Sie sich ein Startupunternehmen vor, das neue Kunden gewinnen möchte. Es bietet einen Preisnachlass von 50% für die erste Bestellung eines neuen Kunden. Andernfalls erhalten Bestandskunden den Standardrabatt. Der Bibliotheksautor muss die Standardimplementierung in eine protected static-Methode verschieben, sodass jede Klasse, die diese Schnittstelle implementiert, den Code in ihrer Implementierung wiederverwenden kann. Die Standardimplementierung des Schnittstellenmembers ruft diese freigegebene Methode ebenfalls auf:

public decimal ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);
protected static decimal DefaultLoyaltyDiscount(ICustomer c)
{
    DateTime start = DateTime.Now - length;

    if ((c.DateJoined < start) && (c.PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

In einer Implementierung einer Klasse, die diese Schnittstelle implementiert, kann die Überschreibung die statische Hilfsmethode aufrufen und diese Logik zum Bereitstellen des „Neuer Kunde“-Rabatts erweitern:

public decimal ComputeLoyaltyDiscount()
{
   if (PreviousOrders.Any() == false)
        return 0.50m;
    else
        return ICustomer.DefaultLoyaltyDiscount(this);
}

Den vollständigen Code finden Sie in unserem Beispielrepository auf GitHub. Sie erhalten die Startanwendung von unserem Beispielerepository auf GitHub.

Diese neuen Features bedeuten, dass Schnittstellen problemlos aktualisiert werden können, wenn eine vernünftige Standardimplementierung für diese neuen Member vorhanden ist. Entwerfen Sie Schnittstellen sorgfältig, um einzelne funktionale Konzepte auszudrücken, die von mehreren Klassen implementiert werden können. Dies erleichtert das Aktualisieren dieser Schnittstellendefinitionen, wenn neue Anforderungen für diese gleichen funktionalen Konzept entdeckt werden.