Självstudie: Uppdatera gränssnitt med standardgränssnittsmetoder

Du kan definiera en implementering när du deklarerar en medlem i ett gränssnitt. Det vanligaste scenariot är att på ett säkert sätt lägga till medlemmar i ett gränssnitt som redan har släppts och används av otaliga klienter.

I den här självstudien får du lära dig att:

  • Utöka gränssnitten på ett säkert sätt genom att lägga till metoder med implementeringar.
  • Skapa parametriserade implementeringar för att ge större flexibilitet.
  • Gör det möjligt för implementerare att tillhandahålla en mer specifik implementering i form av en åsidosättning.

Förutsättningar

Du måste konfigurera datorn för att köra .NET, inklusive C#-kompilatorn. C#-kompilatorn är tillgänglig med Visual Studio 2022 eller .NET SDK.

Scenarioöversikt

Den här självstudien börjar med version 1 av ett kundrelationsbibliotek. Du kan hämta startprogrammet på vår lagringsplats för exempel på GitHub. Företaget som skapade det här biblioteket avsåg kunder med befintliga program att använda sitt bibliotek. De tillhandahöll minimala gränssnittsdefinitioner för användare av deras bibliotek att implementera. Här är gränssnittsdefinitionen för en kund:

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

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

De definierade ett andra gränssnitt som representerar en ordning:

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

Från dessa gränssnitt kan teamet skapa ett bibliotek för sina användare för att skapa en bättre upplevelse för sina kunder. Deras mål var att skapa en djupare relation med befintliga kunder och förbättra deras relationer med nya kunder.

Nu är det dags att uppgradera biblioteket för nästa version. En av de begärda funktionerna möjliggör en lojalitetsrabatt för kunder som har många beställningar. Den här nya lojalitetsrabatten tillämpas när en kund gör en beställning. Den specifika rabatten är en egenskap för varje enskild kund. Varje implementering av ICustomer kan ange olika regler för lojalitetsrabatten.

Det mest naturliga sättet att lägga till den här funktionen är att förbättra ICustomer gränssnittet med en metod för att tillämpa eventuella lojalitetsrabatter. Det här designförslaget orsakade oro bland erfarna utvecklare: "Gränssnitt är oföränderliga när de har släppts! Gör inte en icke-bakåtkompatibel förändring!" Du bör använda standardgränssnittsimplementeringar för att uppgradera gränssnitt. Biblioteksförfattarna kan lägga till nya medlemmar i gränssnittet och tillhandahålla en standardimplementering för dessa medlemmar.

Standardgränssnittsimplementeringar gör det möjligt för utvecklare att uppgradera ett gränssnitt samtidigt som alla implementorer kan åsidosätta implementeringen. Biblioteksanvändare kan acceptera standardimplementeringen som en icke-icke-bakåtkompatibel ändring. Om deras affärsregler skiljer sig åt kan de åsidosättas.

Uppgradera med standardgränssnittsmetoder

Teamet kom överens om den mest sannolika standardimplementeringen: en lojalitetsrabatt för kunder.

Uppgraderingen bör ge funktioner för att ange två egenskaper: antalet beställningar som krävs för att vara berättigad till rabatten och procentandelen av rabatten. De här funktionerna gör det till ett perfekt scenario för standardgränssnittsmetoder. Du kan lägga till en metod i ICustomer gränssnittet och tillhandahålla den mest sannolika implementeringen. Alla befintliga och alla nya implementeringar kan använda standardimplementeringen eller tillhandahålla sina egna.

Lägg först till den nya metoden i gränssnittet, inklusive metodens brödtext:

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

Biblioteksförfattaren skrev ett första test för att kontrollera implementeringen:

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()}");

Observera följande del av testet:

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

Det cast från SampleCustomer till ICustomer är nödvändigt. Klassen SampleCustomer behöver inte tillhandahålla någon implementering för ComputeLoyaltyDiscount; som tillhandahålls av ICustomer gränssnittet. Klassen ärver dock SampleCustomer inte medlemmar från dess gränssnitt. Regeln har inte ändrats. För att anropa en metod som deklarerats och implementerats i gränssnittet måste variabeln vara typen av gränssnitt, ICustomer i det här exemplet.

Ange parameterisering

Standardimplementeringen är för restriktiv. Många konsumenter av det här systemet kan välja olika tröskelvärden för antal inköp, en annan medlemskapslängd eller en annan procentrabatt. Du kan ge fler kunder en bättre uppgraderingsupplevelse genom att ange dessa parametrar. Nu ska vi lägga till en statisk metod som anger de tre parametrarna som styr standardimplementeringen:

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

Det finns många nya språkfunktioner som visas i det lilla kodfragmentet. Gränssnitt kan nu innehålla statiska medlemmar, inklusive fält och metoder. Olika åtkomstmodifierare är också aktiverade. De andra fälten är privata, den nya metoden är offentlig. Någon av modifierarna tillåts för gränssnittsmedlemmar.

Program som använder den allmänna formeln för att beräkna lojalitetsrabatten, men olika parametrar, behöver inte tillhandahålla en anpassad implementering. de kan ange argumenten via en statisk metod. Följande kod anger till exempel en "kunduppskattning" som belönar alla kunder med mer än en månads medlemskap:

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

Utöka standardimplementeringen

Den kod som du har lagt till hittills har gett en praktisk implementering för de scenarier där användarna vill ha något som liknar standardimplementeringen eller för att tillhandahålla en orelaterad uppsättning regler. För en sista funktion ska vi omstrukturera koden lite för att aktivera scenarier där användare kanske vill bygga vidare på standardimplementeringen.

Överväg en start som vill locka nya kunder. De erbjuder 50 % rabatt på en ny kunds första beställning. Annars får befintliga kunder standardrabatten. Biblioteksförfattaren måste flytta standardimplementeringen till en protected static metod så att alla klasser som implementerar det här gränssnittet kan återanvända koden i implementeringen. Standardimplementeringen av gränssnittsmedlemmen anropar även den här delade metoden:

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

I en implementering av en klass som implementerar det här gränssnittet kan åsidosättningen anropa metoden för statisk hjälp och utöka logiken för att ge rabatten "ny kund":

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

Du kan se hela den färdiga koden i vår exempellagringsplats på GitHub. Du kan hämta startprogrammet på vår lagringsplats för exempel på GitHub.

Dessa nya funktioner innebär att gränssnitt kan uppdateras på ett säkert sätt när det finns en rimlig standardimplementering för de nya medlemmarna. Utforma noggrant gränssnitt för att uttrycka enkla funktionella idéer som implementeras av flera klasser. Det gör det enklare att uppgradera dessa gränssnittsdefinitioner när nya krav identifieras för samma funktionella idé.