Compartilhar via


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

Você pode definir uma implementação ao declarar 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, você aprenderá a:

  • Estenda as interfaces com segurança adicionando métodos com implementações.
  • Crie implementações parametrizadas para fornecer 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 seu computador para executar o .NET, incluindo o compilador C#. O compilador C# está disponível com o Visual Studio 2022 ou o SDK do .NET.

Visã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 exemplos no GitHub. A empresa que criou essa biblioteca pretendia que os clientes com aplicativos existentes adotassem sua biblioteca. Eles forneceram definições mínimas de interface para os usuários de sua biblioteca implementarem. Esta é 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 pode criar uma biblioteca para que seus usuários criem uma experiência melhor para seus clientes. Seu objetivo era criar uma relação mais profunda com os clientes existentes e melhorar suas relações com novos clientes.

Agora, é hora de atualizar a biblioteca para a próxima versão. Um dos recursos solicitados permite um desconto de fidelidade para clientes que têm muitos pedidos. Esse novo desconto de fidelidade é aplicado sempre que um cliente faz um pedido. O desconto específico é uma propriedade de cada cliente individual. Cada implementação de ICustomer pode definir regras diferentes para o desconto de fidelidade.

A maneira mais natural de adicionar essa funcionalidade é aprimorar a ICustomer interface com um método para aplicar qualquer desconto de fidelidade. Essa sugestão de design causou preocupação entre desenvolvedores experientes: "As interfaces são imutáveis depois que são lançadas! Não faça uma alteração significativa! 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 os implementadores substituam essa implementação. Os usuários da biblioteca podem aceitar a implementação padrão como uma alteração da falha. Se as regras de negócio forem diferentes, elas poderão substituir a implementação.

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 qualificado para o desconto e o percentual 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()}");

Essa conversão de SampleCustomer até ICustomer é necessária. 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 desse sistema podem escolher limites diferentes para o número de compras, um tamanho diferente de associaçã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 novos recursos de linguagem mostrados nesse pequeno fragmento de código. As interfaces agora podem incluir membros estáticos, incluindo campos e métodos. Modificadores de acesso diferentes também estão habilitados. Os outros campos são privados, o novo método é público. Qualquer dos modificadores são permitidos em membros de interface.

Os 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 por meio 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 de regras não relacionado. Como última funcionalidade, vamos refatorar o código um pouco para habilitar cenários em que os usuários possam querer expandir sobre a implementação padrão.

Considere uma startup que deseja atrair novos clientes. Eles oferecem um desconto de 50% no primeiro pedido de um novo cliente. Caso contrário, os clientes existentes obterão 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 exemplos 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. Crie cuidadosamente interfaces para expressar ideias funcionais simples 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.