Tutoriel : Mettre à jour les interfaces avec les méthodes d’interface par défaut

Vous pouvez définir une implémentation lorsque vous déclarez un membre d’une interface. Le scénario le plus courant consiste à ajouter de manière sécurisée des membres à une interface déjà publiée et utilisée par de nombreux clients.

Ce didacticiel vous montre comment effectuer les opérations suivantes :

  • Étendre des interfaces de manière sécurisée en ajoutant des méthodes avec des implémentations
  • Créer des implémentations paramétrables pour fournir une plus grande flexibilité
  • Permettre aux implémenteurs de fournir une implémentation plus spécifique sous la forme d’un remplacement

Prérequis

Vous devez configurer votre ordinateur pour exécuter .NET, y compris le compilateur C#. Le compilateur C# est disponible avec Visual Studio 2022 ou le Kit de développement logiciel (SDK) .NET.

Présentation du scénario

Ce tutoriel commence par la version 1 d’une bibliothèque de relation client. Vous pouvez obtenir l’application de démarrage à partir de notre dépôt samples sur GitHub. L’entreprise qui a créé cette bibliothèque espérait la voir adoptée par les clients ayant des applications existantes. Elle a fourni des définitions d’interface minimales destinées à être implémentées par les utilisateurs de sa bibliothèque. Voici la définition d’interface pour un client :

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

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

Elle a défini une deuxième interface qui représente une commande :

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

À partir de ces interfaces, l’équipe a pu générer une bibliothèque pour les utilisateurs permettant de créer une meilleure expérience pour ses clients. Son objectif était de créer une relation plus étroite avec les clients existants et d’améliorer ses relations avec les nouveaux clients.

À présent, il est temps de mettre à niveau la bibliothèque pour la prochaine version. L’une des fonctionnalités demandées permet une remise de fidélité pour les clients qui ont un grand nombre de commandes. Cette nouvelle remise de fidélité est appliquée chaque fois qu’un client passe une commande. La remise spécifique est une propriété de chaque client. Chaque implémentation de ICustomer peut définir des règles différentes pour la remise de fidélité.

La façon la plus naturelle d’ajouter cette fonctionnalité consiste à améliorer l’interface ICustomer avec une méthode pour appliquer une remise de fidélité. Cette suggestion de conception a suscité des préoccupations chez les développeurs expérimentés : « Les interfaces sont immuables une fois qu’elles ont été publiées ! C’est un changement cassant! Implémentations d’interface par défaut pour la mise à niveau des interfaces. Les auteurs de bibliothèque peuvent ajouter de nouveaux membres à l’interface et fournir une implémentation par défaut pour ces membres.

Les implémentations d’interface par défaut permettent aux développeurs de mettre à niveau une interface tout en laissant la possibilité aux implémenteurs de substituer cette implémentation. Les utilisateurs de la bibliothèque peuvent accepter l’implémentation par défaut en tant que modification sans rupture. Si leurs règles métier sont différentes, ils peuvent opérer un remplacement.

Mettre à niveau avec les méthodes d’interface par défaut

L’équipe s’est mise d’accord sur l’implémentation par défaut la plus vraisemblable : une remise de fidélité pour les clients.

La mise à niveau doit fournir la fonctionnalité permettant de définir deux propriétés : le nombre de commandes nécessaires pour pouvoir bénéficier de la remise et le pourcentage de celle-ci. Cela en fait un scénario parfait pour les méthodes d’interface par défaut. Vous pouvez ajouter une méthode à l’interface ICustomer et fournir l’implémentation la plus probable. Toutes les implémentations existantes et nouvelles peuvent utiliser l’implémentation par défaut ou fournir la leur.

Tout d’abord, ajoutez la nouvelle méthode à l’interface, y compris le corps de la méthode :

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

L’auteur de la bibliothèque a écrit un premier test pour vérifier l’implémentation :

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

Notez la partie suivante du test :

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

Ce cast de SampleCustomer en ICustomer est nécessaire. La classe SampleCustomer n’a pas besoin de fournir une implémentation pour ComputeLoyaltyDiscount ; elle est fournie par l’interface ICustomer. Toutefois, la classe SampleCustomer n’hérite pas les membres de ses interfaces. Cette règle n’a pas changé. Pour qu’il soit possible d’appeler n’importe quelle méthode déclarée et implémentée dans l’interface, la variable doit être du même type que l’interface, ICustomer dans cet exemple.

Fournir le paramétrage

C’est un bon début. Toutefois, l’implémentation par défaut est trop restrictive. De nombreux consommateurs de ce système peuvent choisir des seuils différents pour le nombre d’achats, une durée différente pour la qualité de membre ou un pourcentage de remise différent. Vous pouvez procurer une meilleure expérience de mise à niveau à davantage de clients en fournissant un moyen de définir ces paramètres. Nous allons ajouter une méthode statique qui définit ces trois paramètres de contrôle de l’implémentation par défaut :

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

Il existe de nombreuses nouvelles fonctionnalités de langage affichées dans ce petit fragment de code. Les interfaces peuvent maintenant inclure des membres statiques, notamment des champs et des méthodes. Différents modificateurs d’accès sont également activés. Les champs supplémentaires sont privés, tandis que la nouvelle méthode est publique. Tout modificateur est autorisé sur les membres d’interface.

Les applications qui utilisent la formule générale pour le calcul de la remise de fidélité, mais des paramètres différents, n’ont pas besoin de fournir une implémentation personnalisée ; elles peuvent définir les arguments par le biais d’une méthode statique. Par exemple, le code suivant définit une « évaluation du client » qui récompense tout client membre depuis plus d’un mois :

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

Étendre l’implémentation par défaut

Le code que vous avez ajouté jusqu’à présent a fourni une implémentation pratique pour les scénarios où les utilisateurs veulent quelque chose comme l’implémentation par défaut ou pour fournir un ensemble de règles non lié. En guise de dernière fonctionnalité, nous allons refactoriser le code un peu pour permettre les scénarios où les utilisateurs sont susceptibles de vouloir générer l’implémentation par défaut.

Imaginez une start-up qui souhaite attirer de nouveaux clients. Elle offre une remise de 50 % sur la première commande d’un nouveau client. Pour leur part, les clients existants bénéficient de la remise standard. L’auteur de la bibliothèque doit déplacer l’implémentation par défaut vers une méthode protected static afin que toute classe qui implémente cette interface puisse réutiliser le code dans son implémentation. L’implémentation par défaut du membre d’interface appelle également cette méthode partagée :

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

Dans une implémentation d’une classe qui implémente cette interface, le remplacement peut appeler la méthode d’assistance statique et étendre cette logique pour fournir la remise « nouveau client » :

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

Vous pouvez voir l’intégralité du code terminé dans notre dépôt d’exemples sur GitHub. Vous pouvez obtenir l’application de démarrage à partir de notre dépôt samples sur GitHub.

Ces nouvelles fonctionnalités signifient que les interfaces peuvent être mises à jour de manière sécurisée quand il existe une implémentation par défaut raisonnable pour les nouveaux membres. Concevez soigneusement les interfaces pour exprimer des idées fonctionnelles uniques pouvant être implémentées par plusieurs classes. Cela facilite la mise à niveau de ces définitions d’interface quand de nouvelles exigences sont découvertes pour cette même idée fonctionnelle.