Tutorial: Actualización de interfaces con métodos de interfaz predeterminados

Puede definir una implementación cuando se declara un miembro de una interfaz. El escenario más común es agregar de forma segura a miembros a una interfaz ya publicada y utilizada por clientes incontables.

En este tutorial aprenderá lo siguiente:

  • Extender interfaces de forma segura mediante la adición de métodos con implementaciones.
  • Crear implementaciones con parámetros para proporcionar mayor flexibilidad.
  • Permitir que los implementadores proporcionen una implementación más específica en forma de una invalidación.

Requisitos previos

Debe configurar la máquina para ejecutar .NET, incluido el compilador de C#. El compilador de C# está disponible con Visual Studio 2022 o el SDK de .NET.

Información general del escenario

Este tutorial comienza con la versión 1 de una biblioteca de relaciones con clientes. Puede obtener la aplicación de inicio en nuestro repositorio de ejemplo en GitHub. La empresa que creó esta biblioteca se dirigía a los clientes con aplicaciones existentes para adoptar sus bibliotecas. Proporcionan definiciones de una interfaz mínima para que la implemente los usuarios de su biblioteca. Esta es la definición de interfaz para un cliente:

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

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

Definieron una segunda interfaz que representa un pedido:

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

En esas interfaces, el equipo pudo generar una biblioteca para sus usuarios con el fin de crear una mejor experiencia para los clientes. Su objetivo era crear una relación más estrecha con los clientes existentes y mejorar sus relaciones con los clientes nuevos.

Ahora, es momento de actualizar la biblioteca para la próxima versión. Una de las características solicitadas permite un descuento por fidelidad para los clientes que tienen muchos pedidos. Este nuevo descuento por fidelidad se aplica cada vez que un cliente realiza un pedido. El descuento específico es una propiedad de cada cliente individual. Cada implementación de ICustomer puede establecer reglas diferentes para el descuento por fidelidad.

La forma más natural para agregar esta funcionalidad es mejorar la interfaz ICustomer con un método para aplicar los descuentos por fidelización. Esta sugerencia de diseño es motivo de preocupación entre los desarrolladores experimentados: "Las interfaces son inmutables una vez que se han publicado. ¡No hagas un cambio importante!" Debe usar implementaciones de interfaz predeterminadas para actualizar interfaces. Los autores de bibliotecas pueden agregar a nuevos miembros a la interfaz y proporcionar una implementación predeterminada para esos miembros.

Las implementaciones de interfaces predeterminadas permiten a los desarrolladores actualizar una interfaz mientras siguen permitiendo que los implementadores invaliden esa implementación. Los usuarios de la biblioteca pueden aceptar la implementación predeterminada como un cambio sin importancia. Si sus reglas de negocios son diferentes, se puede invalidar.

Actualización con los métodos de interfaz predeterminados

El equipo estuvo de acuerdo en la más probable implementación predeterminada: un descuento por fidelidad para los clientes.

La actualización debe proporcionar la funcionalidad para establecer dos propiedades: el número de pedidos necesario para poder recibir el descuento y el porcentaje del descuento. Estas características lo convierten en un escenario perfecto para los métodos de interfaz predeterminados. Puede agregar un método a la interfaz de ICustomer y proporcionar la implementación más probable. Todas las implementaciones existentes y nuevas pueden usar la implementación predeterminada o proporcionar una propia.

En primer lugar, agregue el nuevo método a la interfaz, incluido su cuerpo:

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

El autor de la biblioteca escribió una primera prueba para comprobar la implementación:

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

Tenga en cuenta la siguiente parte de la prueba:

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

La conversión de SampleCustomer a ICustomer es necesaria. La clase SampleCustomer no necesita proporcionar una implementación para ComputeLoyaltyDiscount. La interfaz ICustomer la proporciona. Sin embargo, la clase SampleCustomer no hereda miembros de sus interfaces. Esa no ha cambiado. Para poder llamar a cualquier método declarado e implementado en la interfaz, la variable debe ser del tipo de la interfaz: ICustomer en este ejemplo.

Proporcionar parametrización

La implementación predeterminada es demasiado restrictiva. Muchos consumidores de este sistema pueden elegir diferentes umbrales para el número de compras, una longitud diferente de la pertenencia o un porcentaje diferente del descuento. Puede proporcionar una mejor experiencia de actualización para más clientes proporcionando una manera de establecer esos parámetros. Vamos a agregar un método estático que establezca esos tres parámetros controlando la implementación predeterminada:

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

Hay muchas funcionalidades nuevas de lenguaje que se muestran en este pequeño fragmento de código. Las interfaces ahora pueden incluir miembros estáticos, incluidos campos y métodos. También están habilitados diferentes modificadores de acceso. Los otros campos son privados, el nuevo método es público. Todos los modificadores están permitidos en los miembros de la interfaz.

Las aplicaciones que usan la fórmula general para calcular el descuento por fidelidad, pero diferentes parámetros, no necesitan proporcionar una implementación personalizada: pueden establecer los argumentos a través de un método estático. Por ejemplo, el siguiente código establece una "apreciación de cliente" que recompensa a cualquier cliente con más de una pertenencia al mes:

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

Extender la implementación predeterminada

El código que ha agregado hasta ahora ha proporcionado una implementación adecuada para los escenarios donde los usuarios quieren algo similar a la implementación predeterminada, o para proporcionar un conjunto de reglas no relacionado. Para una característica final, vamos a refactorizar el código un poco para habilitar los escenarios donde es posible que los usuarios deseen crear en la implementación predeterminada.

Considere una startup que desea captar nuevos clientes. Ofrecen un descuento del 50 % en el primer pedido de un cliente nuevo. De lo contrario, los clientes existentes obtienen el descuento estándar. El autor de la biblioteca necesita mover la implementación predeterminada a un método protected static para que cualquier clase que implemente esta interfaz pueda reutilizar el código en su implementación. La implementación predeterminada del miembro de la interfaz llama a este método compartido así:

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

En una implementación de una clase que implementa esta interfaz, la invalidación puede llamar al método auxiliar estático y ampliar esa lógica para proporcionar el descuento de "cliente nuevo":

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

Puede ver todo el código terminado en nuestro repositorio de ejemplos en GitHub. Puede obtener la aplicación de inicio en nuestro repositorio de ejemplo en GitHub.

Estas nuevas características significan que las interfaces se pueden actualizar de forma segura cuando hay una implementación predeterminada razonable para esos nuevos miembros. Diseñe cuidadosamente las interfaces para expresar ideas funcionales implementadas con varias clases. Esto facilita la actualización de esas definiciones de interfaz cuando se descubren nuevos requisitos para esa misma idea funcional.