Zelfstudie: Interfaces bijwerken met standaardinterfacemethoden

U kunt een implementatie definiëren wanneer u een lid van een interface declareert. Het meest voorkomende scenario is om leden veilig toe te voegen aan een interface die al is uitgebracht en gebruikt door talloze clients.

In deze zelfstudie leert u het volgende:

  • Breid interfaces veilig uit door methoden met implementaties toe te voegen.
  • Maak geparameteriseerde implementaties om meer flexibiliteit te bieden.
  • Implementeerfuncties in staat stellen om een specifiekere implementatie te bieden in de vorm van een onderdrukking.

Vereisten

U moet uw computer instellen om .NET uit te voeren, inclusief de C#-compiler. De C#-compiler is beschikbaar met Visual Studio 2022 of de .NET SDK.

Overzicht van scenario

Deze zelfstudie begint met versie 1 van een klantrelatiebibliotheek. U kunt de starterstoepassing downloaden in onze opslagplaats met voorbeelden op GitHub. Het bedrijf dat deze bibliotheek heeft gebouwd, bedoelde klanten met bestaande toepassingen om hun bibliotheek te gebruiken. Ze hebben minimale interfacedefinities geleverd die gebruikers van hun bibliotheek kunnen implementeren. Dit is de interfacedefinitie voor een klant:

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

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

Ze hebben een tweede interface gedefinieerd die een order vertegenwoordigt:

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

Vanuit deze interfaces kan het team een bibliotheek bouwen voor hun gebruikers om een betere ervaring voor hun klanten te creëren. Hun doel was om een diepere relatie met bestaande klanten te creëren en hun relaties met nieuwe klanten te verbeteren.

Nu is het tijd om de bibliotheek voor de volgende release te upgraden. Een van de aangevraagde functies maakt een loyaliteitskorting mogelijk voor klanten die veel orders hebben. Deze nieuwe loyaliteitskorting wordt toegepast wanneer een klant een bestelling doet. De specifieke korting is een eigenschap van elke afzonderlijke klant. Elke implementatie van ICustomer kan verschillende regels instellen voor de loyaliteitskorting.

De meest natuurlijke manier om deze functionaliteit toe te voegen, is door de ICustomer interface te verbeteren met een methode om eventuele loyaliteitskorting toe te passen. Deze ontwerpsuggesties veroorzaakten zorgen voor ervaren ontwikkelaars: 'Interfaces zijn onveranderbaar zodra ze zijn uitgebracht! Breng geen belangrijke wijziging aan!" U moet standaardinterface-implementaties gebruiken voor het upgraden van interfaces. Auteurs van de bibliotheek kunnen nieuwe leden toevoegen aan de interface en een standaard implementatie bieden voor die leden.

Met standaardinterface-implementaties kunnen ontwikkelaars een interface upgraden terwijl ze eventuele implementors in staat stellen om die implementatie te overschrijven. Gebruikers van de bibliotheek kunnen de standaard implementatie accepteren als een niet-belangrijke wijziging. Als hun bedrijfsregels afwijken, kunnen ze overschrijven.

Upgraden met standaardinterfacemethoden

Het team is overeengekomen over de meest waarschijnlijke standaard implementatie: een loyaliteitskorting voor klanten.

De upgrade moet de functionaliteit bieden om twee eigenschappen in te stellen: het aantal orders dat in aanmerking moet komen voor de korting en het percentage van de korting. Deze functies maken het een perfect scenario voor standaardinterfacemethoden. U kunt een methode toevoegen aan de ICustomer interface en de meest waarschijnlijke implementatie bieden. Alle bestaande en eventuele nieuwe implementaties kunnen gebruikmaken van de standaard implementatie of hun eigen implementatie bieden.

Voeg eerst de nieuwe methode toe aan de interface, inclusief de hoofdtekst van de methode:

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

De auteur van de bibliotheek heeft een eerste test geschreven om de implementatie te controleren:

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

Let op het volgende gedeelte van de test:

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

Die cast van SampleCustomer naar ICustomer is noodzakelijk. De SampleCustomer klasse hoeft geen implementatie te bieden voor ComputeLoyaltyDiscount; die wordt geleverd door de ICustomer interface. De SampleCustomer klasse neemt echter geen leden over van de interfaces. Deze regel is niet gewijzigd. Als u een methode wilt aanroepen die in de interface is gedeclareerd en geïmplementeerd, moet de variabele het type interface zijn, ICustomer in dit voorbeeld.

Parameterisatie opgeven

De standaard implementatie is te beperkend. Veel consumenten van dit systeem kunnen verschillende drempelwaarden kiezen voor het aantal aankopen, een andere lengte van het lidmaatschap of een ander percentage korting. U kunt een betere upgrade-ervaring bieden voor meer klanten door een manier te bieden om deze parameters in te stellen. We gaan een statische methode toevoegen waarmee deze drie parameters worden ingesteld waarmee de standaard implementatie wordt beheerd:

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

Er zijn veel nieuwe taalmogelijkheden die worden weergegeven in dat kleine codefragment. Interfaces kunnen nu statische leden bevatten, waaronder velden en methoden. Er zijn ook verschillende toegangsaanpassingen ingeschakeld. De andere velden zijn privé, de nieuwe methode is openbaar. Alle modifiers zijn toegestaan voor interfaceleden.

Toepassingen die gebruikmaken van de algemene formule voor het berekenen van de loyaliteitskorting, maar verschillende parameters, hoeven geen aangepaste implementatie te bieden; ze kunnen de argumenten instellen via een statische methode. Met de volgende code wordt bijvoorbeeld een 'klantwaardering' ingesteld die elke klant beloont met meer dan één maand lidmaatschap:

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

De standaard implementatie uitbreiden

De code die u tot nu toe hebt toegevoegd, biedt een handige implementatie voor die scenario's waarin gebruikers iets willen zoals de standaard implementatie, of om een niet-gerelateerde set regels te bieden. Voor een laatste functie gaan we de code een beetje herstructureren om scenario's mogelijk te maken waarin gebruikers mogelijk willen bouwen op de standaard implementatie.

Overweeg een startup die nieuwe klanten wil aantrekken. Ze bieden een korting van 50% op de eerste bestelling van een nieuwe klant. Anders krijgen bestaande klanten de standaardkorting. De auteur van de bibliotheek moet de standaard implementatie verplaatsen naar een protected static methode, zodat elke klasse die deze interface implementeert, de code opnieuw kan gebruiken in de implementatie. De standaard implementatie van het interfacelid roept deze gedeelde methode ook aan:

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 een implementatie van een klasse die deze interface implementeert, kan de onderdrukking de statische helpermethode aanroepen en die logica uitbreiden om de korting 'nieuwe klant' te bieden:

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

U kunt de volledige voltooide code zien in onze opslagplaats met voorbeelden op GitHub. U kunt de starterstoepassing downloaden in onze opslagplaats met voorbeelden op GitHub.

Deze nieuwe functies betekenen dat interfaces veilig kunnen worden bijgewerkt wanneer er een redelijke standaard implementatie is voor deze nieuwe leden. Ontwerp interfaces zorgvuldig om één functionele ideeën uit te drukken die door meerdere klassen zijn geïmplementeerd. Hierdoor is het eenvoudiger om deze interfacedefinities bij te werken wanneer er nieuwe vereisten worden gedetecteerd voor hetzelfde functionele idee.