Tutorial: Atualizar interfaces com métodos de interface padrão

Você pode definir uma implementação quando declara um membro de uma interface. O cenário mais comum é adicionar membros com segurança a uma interface já lançada e usada por inúmeros clientes.

Neste tutorial, irá aprender a:

  • Estenda interfaces com segurança adicionando métodos com implementações.
  • Crie implementações parametrizadas para proporcionar maior flexibilidade.
  • Permitir que os implementadores forneçam uma implementação mais específica na forma de uma substituição.

Pré-requisitos

Você precisa configurar sua máquina para executar o .NET, incluindo o compilador C#. O compilador C# está disponível com o Visual Studio 2022 ou o SDK do .NET.

Descrição geral do cenário

Este tutorial começa com a versão 1 de uma biblioteca de relacionamento com o cliente. Você pode obter o aplicativo inicial em nosso repositório de amostras no GitHub. A empresa que construiu esta biblioteca pretendia que os clientes com aplicações existentes adotassem a sua biblioteca. Eles forneceram definições mínimas de interface para os usuários de sua biblioteca implementarem. Aqui está a definição de interface para um cliente:

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

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

Eles definiram uma segunda interface que representa uma ordem:

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

A partir dessas interfaces, a equipe poderia construir uma biblioteca para seus usuários para criar uma experiência melhor para seus clientes. O seu objetivo era criar uma relação mais profunda com os clientes existentes e melhorar as suas relações com os novos clientes.

Agora, é hora de atualizar a biblioteca para a próxima versão. Uma das funcionalidades solicitadas permite um desconto de fidelização para clientes que tenham muitas encomendas. Este novo desconto de fidelização é aplicado sempre que um cliente faz uma encomenda. O desconto específico é uma propriedade de cada cliente individual. Cada implementação de pode definir regras diferentes para o desconto de ICustomer fidelidade.

A maneira mais natural de adicionar essa funcionalidade é aprimorar a ICustomer interface com um método para aplicar qualquer desconto de fidelidade. Esta sugestão de design causou preocupação entre desenvolvedores experientes: "As interfaces são imutáveis depois de lançadas! Não faça uma mudança de rutura!" Você deve usar implementações de interface padrão para atualizar interfaces. Os autores da biblioteca podem adicionar novos membros à interface e fornecer uma implementação padrão para esses membros.

As implementações de interface padrão permitem que os desenvolvedores atualizem uma interface enquanto ainda permitem que quaisquer implementadores substituam essa implementação. Os usuários da biblioteca podem aceitar a implementação padrão como uma alteração contínua. Se as suas regras de negócio forem diferentes, podem substituir-se.

Atualizar com métodos de interface padrão

A equipe concordou com a implementação padrão mais provável: um desconto de fidelidade para os clientes.

A atualização deve fornecer a funcionalidade para definir duas propriedades: o número de pedidos necessários para ser elegível para o desconto e a porcentagem do desconto. Esses recursos o tornam um cenário perfeito para métodos de interface padrão. Você pode adicionar um método à ICustomer interface e fornecer a implementação mais provável. Todas as implementações existentes e novas podem usar a implementação padrão ou fornecer suas próprias.

Primeiro, adicione o novo método à interface, incluindo o corpo do método:

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

O autor da biblioteca escreveu um primeiro teste para verificar a implementação:

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 a seguinte parte do teste:

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

Esse elenco de SampleCustomer para é ICustomer necessário. A SampleCustomer classe não precisa fornecer uma implementação para ComputeLoyaltyDiscount, que é fornecida pela ICustomer interface. No entanto, a SampleCustomer classe não herda membros de suas interfaces. Essa regra não mudou. Para chamar qualquer método declarado e implementado na interface, a variável deve ser o tipo da interface, ICustomer neste exemplo.

Fornecer parametrização

A implementação padrão é muito restritiva. Muitos consumidores deste sistema podem escolher limites diferentes para o número de compras, uma duração diferente de adesão ou um desconto percentual diferente. Você pode fornecer uma melhor experiência de atualização para mais clientes fornecendo uma maneira de definir esses parâmetros. Vamos adicionar um método estático que define esses três parâmetros que controlam a implementação padrão:

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

Há muitos recursos de linguagem novos mostrados nesse pequeno fragmento de código. As interfaces agora podem incluir membros estáticos, incluindo campos e métodos. Diferentes modificadores de acesso também estão habilitados. Os outros campos são privados, o novo método é público. Qualquer um dos modificadores é permitido em membros da interface.

Aplicativos que usam a fórmula geral para calcular o desconto de fidelidade, mas parâmetros diferentes, não precisam fornecer uma implementação personalizada; eles podem definir os argumentos através de um método estático. Por exemplo, o código a seguir define uma "apreciação do cliente" que recompensa qualquer cliente com mais de um mês de associação:

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

Estender a implementação padrão

O código que você adicionou até agora forneceu uma implementação conveniente para os cenários em que os usuários querem algo como a implementação padrão ou para fornecer um conjunto não relacionado de regras. Para um recurso final, vamos refatorar um pouco o código para habilitar cenários em que os usuários podem querer criar sobre a implementação padrão.

Considere uma startup que quer atrair novos clientes. Eles oferecem um desconto de 50% na primeira encomenda de um novo cliente. Caso contrário, os clientes existentes obtêm o desconto padrão. O autor da biblioteca precisa mover a implementação padrão para um protected static método para que qualquer classe que implemente essa interface possa reutilizar o código em sua implementação. A implementação padrão do membro da interface também chama esse método compartilhado:

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

Em uma implementação de uma classe que implementa essa interface, a substituição pode chamar o método auxiliar estático e estender essa lógica para fornecer o desconto de "novo cliente":

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

Você pode ver todo o código concluído em nosso repositório de exemplos no GitHub. Você pode obter o aplicativo inicial em nosso repositório de amostras no GitHub.

Esses novos recursos significam que as interfaces podem ser atualizadas com segurança quando há uma implementação padrão razoável para esses novos membros. Projete cuidadosamente interfaces para expressar ideias funcionais únicas implementadas por várias classes. Isso facilita a atualização dessas definições de interface quando novos requisitos são descobertos para essa mesma ideia funcional.