Condividi tramite


Esercitazione: Aggiornare le interfacce con i metodi di interfaccia predefiniti

È possibile definire un'implementazione quando si dichiara un membro di un'interfaccia. Lo scenario più comune consiste nell'aggiungere in modo sicuro i membri a un'interfaccia già rilasciata e usata da client innumerabili.

In questa esercitazione si apprenderà come:

  • Estendere le interfacce in modo sicuro aggiungendo metodi con implementazioni.
  • Creare implementazioni con parametri per offrire maggiore flessibilità.
  • Abilitare gli implementatori per fornire un'implementazione più specifica sotto forma di override.

Prerequisiti

È necessario configurare il computer per eseguire .NET, incluso il compilatore C#. Il compilatore C# è disponibile con Visual Studio 2022 o .NET SDK.

Panoramica dello scenario

Questa esercitazione inizia con la versione 1 di una libreria di relazioni con i clienti. È possibile ottenere l'applicazione iniziale nel repository di esempi in GitHub. L'azienda che ha creato questa libreria intendeva che i clienti con applicazioni esistenti adottassero la loro libreria. Hanno fornito definizioni di interfaccia minime per gli utenti della libreria da implementare. Ecco la definizione dell'interfaccia per un cliente:

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

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

Hanno definito una seconda interfaccia che rappresenta un ordine:

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

Da queste interfacce, il team potrebbe creare una libreria per gli utenti per creare un'esperienza migliore per i clienti. Il loro obiettivo era quello di creare una relazione più profonda con i clienti esistenti e migliorare le loro relazioni con i nuovi clienti.

A questo punto, è il momento di aggiornare la libreria per la versione successiva. Una delle funzionalità richieste consente uno sconto fedeltà per i clienti che hanno un sacco di ordini. Questo nuovo sconto fedeltà viene applicato ogni volta che un cliente effettua un ordine. Lo sconto specifico è una proprietà di ogni singolo cliente. Ogni implementazione di ICustomer può impostare regole diverse per lo sconto fedeltà.

Il modo più naturale per aggiungere questa funzionalità è migliorare l'interfaccia ICustomer con un metodo per applicare qualsiasi sconto fedeltà. Questo suggerimento di progettazione ha causato preoccupazione per gli sviluppatori esperti: "Le interfacce sono immutabili dopo il rilascio! Non apportare modifiche di rilievo!" È consigliabile usare le implementazioni di interfaccia predefinite per l'aggiornamento delle interfacce. Gli autori della libreria possono aggiungere nuovi membri all'interfaccia e fornire un'implementazione predefinita per tali membri.

Le implementazioni predefinite dell'interfaccia consentono agli sviluppatori di aggiornare un'interfaccia, consentendo comunque agli implementatori di eseguire l'override di tale implementazione. Gli utenti della libreria possono accettare l'implementazione predefinita come modifica non di rilievo. Se le regole aziendali sono diverse, possono sostituirle.

Eseguire l'aggiornamento con metodi di interfaccia predefiniti

Il team ha concordato l'implementazione predefinita più probabile: uno sconto fedeltà per i clienti.

L'aggiornamento deve fornire la funzionalità per impostare due proprietà: il numero di ordini necessari per essere idonei allo sconto e la percentuale dello sconto. Queste funzionalità lo rendono uno scenario perfetto per i metodi di interfaccia predefiniti. È possibile aggiungere un metodo all'interfaccia ICustomer e fornire l'implementazione più probabile. Tutte le implementazioni esistenti e tutte le nuove implementazioni possono usare l'implementazione predefinita o fornire le proprie implementazioni.

Aggiungere prima di tutto il nuovo metodo all'interfaccia, incluso il corpo del metodo :

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

L'autore della libreria ha scritto un primo test per verificare l'implementazione:

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

Si noti la parte seguente del test:

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

Il cast da SampleCustomer a ICustomer è necessario. La SampleCustomer classe non deve fornire un'implementazione per ComputeLoyaltyDiscount, fornita dall'interfaccia ICustomer . Tuttavia, la SampleCustomer classe non eredita i membri dalle relative interfacce. Questa regola non è cambiata. Per chiamare qualsiasi metodo dichiarato e implementato nell'interfaccia, la variabile deve essere il tipo dell'interfaccia, ICustomer in questo esempio.

Specificare la parametrizzazione

L'implementazione predefinita è troppo restrittiva. Molti consumatori di questo sistema possono scegliere soglie diverse per il numero di acquisti, una lunghezza diversa di appartenenza o uno sconto percentuale diverso. È possibile offrire un'esperienza di aggiornamento migliore per più clienti fornendo un modo per impostare tali parametri. Aggiungere ora un metodo statico che imposta i tre parametri che controllano l'implementazione predefinita:

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

Esistono molte nuove funzionalità del linguaggio illustrate in questo piccolo frammento di codice. Le interfacce possono ora includere membri statici, inclusi campi e metodi. Sono abilitati anche modificatori di accesso diversi. Gli altri campi sono privati, il nuovo metodo è pubblico. Uno dei modificatori è consentito nei membri dell'interfaccia.

Le applicazioni che usano la formula generale per calcolare lo sconto fedeltà, ma parametri diversi, non devono fornire un'implementazione personalizzata; possono impostare gli argomenti tramite un metodo statico. Ad esempio, il codice seguente imposta un "apprezzamento dei clienti" che premia qualsiasi cliente con più di un mese di appartenenza:

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

Estendere l'implementazione predefinita

Il codice aggiunto finora ha fornito un'implementazione pratica per questi scenari in cui gli utenti vogliono un'implementazione simile all'implementazione predefinita o per fornire un set di regole non correlato. Per una funzionalità finale, verrà eseguito il refactoring del codice per abilitare gli scenari in cui gli utenti potrebbero voler compilare l'implementazione predefinita.

Si consideri una startup che vuole attirare nuovi clienti. Offrono uno sconto di 50% al primo ordine di un nuovo cliente. In caso contrario, i clienti esistenti ottengono lo sconto standard. L'autore della libreria deve spostare l'implementazione predefinita in un protected static metodo in modo che qualsiasi classe che implementi questa interfaccia possa riutilizzare il codice nell'implementazione. L'implementazione predefinita del membro dell'interfaccia chiama anche questo metodo condiviso:

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 un'implementazione di una classe che implementa questa interfaccia, l'override può chiamare il metodo helper statico ed estendere tale logica per fornire lo sconto "nuovo cliente":

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

È possibile visualizzare l'intero codice completato nel repository di esempi in GitHub. È possibile ottenere l'applicazione iniziale nel repository di esempi in GitHub.

Queste nuove funzionalità indicano che le interfacce possono essere aggiornate in modo sicuro quando è disponibile un'implementazione predefinita ragionevole per i nuovi membri. Progettare con attenzione le interfacce per esprimere singole idee funzionali implementate da più classi. In questo modo è più semplice aggiornare tali definizioni di interfaccia quando vengono individuati nuovi requisiti per la stessa idea funzionale.