Tutorial: 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'aggiunta sicura di membri a un'interfaccia già rilasciata e usata da innumerevoli client.

Questa esercitazione illustra come:

  • Estendere le interfacce in modo sicuro aggiungendo metodi con implementazioni.
  • Creare implementazioni con parametri per una 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 raccolta di relazioni con i clienti. L'applicazione di base è disponibile nel repository di esempi in GitHub. L'azienda che ha realizzato questa raccolta intende farla adottare dai clienti con le applicazioni esistenti. Agli utenti della raccolta vengono fornite definizioni di interfaccia minime da implementare. Ecco la definizione di 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; }
}

Viene definita una seconda interfaccia che rappresenta un ordine:

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

Da queste interfacce il team potrebbe realizzare una raccolta per consentire agli utenti di creare un'esperienza migliore per i clienti. L'obiettivo è consolidare le relazioni con i clienti esistenti e migliorare quelle con i nuovi clienti.

Ora è il momento di aggiornare la raccolta per la versione successiva. Una delle funzionalità richieste è la definizione di uno sconto fedeltà per i clienti con molti 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 ruoli diversi per lo sconto fedeltà.

Il modo più naturale per aggiungere questa funzionalità consiste nell'ottimizzare 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 che causano un'interruzione!" Si dovrebbero usare le implementazioni di interfaccia predefinite per l'aggiornamento delle interfacce. Gli autori della raccolta possono aggiungere nuovi membri all'interfaccia a cui applicare un'implementazione predefinita.

Le implementazioni di interfaccia predefinite consentono agli sviluppatori di aggiornare un'interfaccia, ma possono comunque essere sottoposte a override da qualsiasi implementatore. Gli utenti della raccolta possono accettare l'implementazione predefinita come modifica che non causa interruzioni. Se le regole business sono diverse, possono eseguire l'override.

Aggiornare con metodi di interfaccia predefiniti

Il team concorda sull'implementazione predefinita più probabile: uno sconto fedeltà per i clienti.

L'aggiornamento dovrà fornire la funzionalità per impostare due proprietà: il numero di ordini necessario per avere diritto allo sconto e la percentuale dello sconto. Queste funzionalità lo rendono lo 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 quelle nuove possono usare l'implementazione predefinita o una personalizzata.

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 raccolta scrive 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 classe SampleCustomer non deve necessariamente fornire un'implementazione per ComputeLoyaltyDiscount, perché viene fornita dall'interfaccia ICustomer. Tuttavia, la classe SampleCustomer 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, in questo esempio ICustomer.

Fornire la parametrizzazione

L'implementazione predefinita è troppo restrittiva. Molti utenti di questo sistema potrebbero scegliere soglie diverse per il numero di acquisti, una durata diversa dell'iscrizione o una percentuale di sconto diversa. È possibile fornire un'esperienza di aggiornamento migliore per più clienti offrendo la possibilità di impostare questi parametri. A questo scopo, aggiungere un metodo statico che imposta questi 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;
}

In questo piccolo frammento di codice sono visibili molte nuove funzionalità del linguaggio. Le interfacce possono ora includere membri statici, tra cui campi e metodi. Sono inoltre abilitati diversi modificatori di accesso. Gli altri campi sono privati, mentre il nuovo metodo è pubblico. Per i membri dell'interfaccia è consentito qualsiasi modificatore.

Per le applicazioni in cui si usa la formula generale per calcolare lo sconto fedeltà, ma con parametri diversi, non è necessario fornire un'implementazione personalizzata perché è possibile impostare gli argomenti tramite un metodo statico. Il codice seguente imposta ad esempio un "apprezzamento del cliente" che premia qualsiasi cliente che si sia iscritto da più di un mese:

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 offre un'implementazione utile per gli scenari in cui gli utenti vogliono qualcosa di simile all'implementazione predefinita o per fornire un set non correlato di regole. Per una funzionalità finale, verrà eseguito il refactoring del codice per abilitare scenari in cui gli utenti potrebbero scegliere di sviluppare ulteriormente l'implementazione predefinita.

Si supponga ad esempio che una startup voglia attirare nuovi clienti. Offre uno sconto del 50% sul primo ordine di un nuovo cliente. Altrimenti, i clienti esistenti ottengono lo sconto standard. L'autore della raccolta deve trasferire l'implementazione predefinita in un metodo protected static in modo che qualsiasi classe che implementi questa interfaccia possa riutilizzare il codice nella propria implementazione. L'implementazione predefinita del membro di interfaccia chiama anche questo metodo predefinito:

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 questa logica per fornire lo sconto "nuovo cliente":

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

È possibile visualizzare il codice completo nel repository degli esempi su GitHub. L'applicazione di base è disponibile nel repository di esempi in GitHub.

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