Compartir a través de


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 miembros de forma segura a una interfaz ya publicada y usada por innumerables clientes.

En este tutorial, aprenderá a:

  • Amplíe las interfaces de forma segura mediante la adición de métodos con implementaciones.
  • Cree 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.

Prerrequisitos

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

Información general sobre el escenario

Este tutorial comienza con la versión 1 de una biblioteca de relaciones con el cliente. Puede obtener la aplicación de inicio en nuestro repositorio de ejemplos en GitHub. La empresa que creó esta biblioteca pretende que los clientes con aplicaciones existentes adopten su biblioteca. Proporcionaron definiciones de interfaz mínimas para que los usuarios de su biblioteca las implementen. 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 orden:

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

A partir de esas interfaces, el equipo podría crear una biblioteca para que sus usuarios creen una mejor experiencia para sus clientes. Su objetivo era crear una relación más profunda con los clientes existentes y mejorar sus relaciones con los nuevos clientes.

Ahora, es el momento de actualizar la biblioteca para la próxima versión. Una de las características solicitadas permite un descuento de fidelidad para los clientes que tienen un montón de pedidos. Este nuevo descuento de 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 manera más natural de agregar esta funcionalidad es mejorar la ICustomer interfaz con un método para aplicar cualquier descuento de fidelidad. Esta sugerencia de diseño causó preocupación entre los desarrolladores experimentados: "Las interfaces son inmutables una vez que se han publicado! "No haga un cambio disruptivo". Debe usar implementaciones predeterminadas de interfaz para actualizar interfaces. Los autores de la biblioteca pueden agregar nuevos miembros a la interfaz y proporcionar una implementación predeterminada para esos miembros.

Las implementaciones de interfaz 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 no importante. Si sus reglas de negocios son diferentes, se puede invalidar.

Actualización con métodos de interfaz predeterminados

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

La actualización debe proporcionar la funcionalidad para establecer dos propiedades: el número de pedidos necesarios para ser aptos para 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 ICustomer interfaz y proporcionar la implementación más probable. Todas las implementaciones existentes y cualquier implementación nueva pueden usar la implementación predeterminada o proporcionar su propia.

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

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

Observe la siguiente parte de la prueba:

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

Esa conversión de SampleCustomer a ICustomer es necesaria. La SampleCustomer clase no necesita proporcionar una implementación para ComputeLoyaltyDiscount; eso lo proporciona la interfaz ICustomer. Sin embargo, la SampleCustomer clase no hereda miembros de sus interfaces. Esa regla no ha cambiado. Para llamar a cualquier método declarado e implementado en la interfaz, la variable debe ser el 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 umbrales diferentes para el número de compras, una longitud diferente de pertenencia o un descuento porcentual diferente. 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 establece esos tres parámetros que controlan 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 de lenguaje nuevas que se muestran en ese pequeño fragmento de código. Las interfaces ahora pueden incluir miembros estáticos, incluidos campos y métodos. También se habilitan modificadores de acceso diferentes. Los demás 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 distintos 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 código siguiente establece una "apreciación del cliente" que recompensa a cualquier cliente con más de un mes de pertenencia:

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

Extensión de la implementación predeterminada

El código que ha agregado hasta ahora ha proporcionado una implementación cómoda para esos escenarios en los que los usuarios quieren algo parecido 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 escenarios en los que es posible que los usuarios quieran compilar en la implementación predeterminada.

Considera un startup que quiere atraer 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 debe mover la implementación predeterminada a un protected static método para que cualquier clase que implemente esta interfaz pueda reutilizar el código en su implementación. La implementación predeterminada del miembro de interfaz también llama a este método compartido:

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 ejemplos 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 interfaces para expresar ideas funcionales únicas implementadas por varias clases. Esto facilita la actualización de esas definiciones de interfaz cuando se detectan nuevos requisitos para esa misma idea funcional.