“Default Implementations” em interfaces no C#. Novidade bem-vinda ou erro da Microsoft?

Uma das features mais polêmicas do C# 8 é, sem dúvidas, a possibilidade de incluirmos implementações padrões para métodos em interfaces. Há quem tenha gostado da novidade. Há quem entenda que a Microsoft cometeu um grande erro.

Essa funcionalidade já existe em outras linguagens, como Java há algum tempo e, nessa linguagem, nos parece que ela faz mais sentido. Afinal, em Java, essa ideia potencializa a utilização de outro recurso que é o fornecimento de uma função anônima, como parâmetro, onde uma interface é esperada, porém quando essa interface possuir apenas um método carecendo implementação.

De nossa parte, voltando para C#, entendemos que o problema não está na feature em si, mas na forma como a Microsoft a está promovendo.

Uma forma nova de resolver problemas que já foram resolvidos

Mads Torgersen, líder de design da linguagem, escreveu um post que, em nossa opinião, gerou mais confusão do que esclarecimento.

Em seu exemplo, que vem sendo repetido exaustivamente sempre que essa funcionalidade é explicada, ele demonstra uma interface com um único método e com potencialmente diversas implementações.

public interface ILogger
{
  void Log(LogLevel level, string message);
}

Segundo o argumento do post, caso fosse necessário adicionar um novo método a interface, isto implicaria na atualização de todas as classes que a implementam, gerando quebra. Então, segundo ele, faria sentido poder fornecer uma implementação padrão – eliminando a necessidade de revisitar todas as implementações da interface para adicionar o novo método. Isso seria especialmente útil para fornecedores de bibliotecas terceiras que não tem ação direta sobre como seus usuários implementam evoluções.

public interface ILogger
{
  void Log(LogLevel level, string message);
  void Log(Exception ex) => Log(LogLevel.Error, ex.ToString());
}

O problema, acreditamos, é que interfaces, que não são um conceito fácil de entender, dessa forma, confundem-se muito com classes abstratas. Além disso, esse exemplo é excessivamente trivial e tem pouca relação com interfaces reais.

Na prática, acreditamos, será difícil fornecer implementações padrões razoáveis e, na maioria dos casos, veremos a proliferação de NotImplemementedException sendo lançadas tornando o que hoje é um problema resolvido antes da compilação um problema em tempo de execução.

É importante destacar que o problema indicado por Mads já conta com boas soluções alternativas, desde sempre, em C#. Por exemplo, quando uma interface precisa ser expandida, poderíamos criar uma nova, que herde da original adicionando os elementos necessários.

public interface ILogger
{
  void Log(LogLevel level, string message);
}

public interface IExtendedLogger : ILogger
{
  void Log(Exception ex);
}

Embora essa técnica implique, também, em revisão de código (que Mads está tentando evitar), nos parece mais coerente. Afinal, sempre que um método precisasse das capacidades estendidas da interface, deixaria isso explícito e o volume de modificações nos códigos “cliente” seria minimizado.

Outra abordagem plenamente razoável, até mais flexível, seria resolver o problema de implementação com métodos de extensão quando houvesse um código padrão razoável o suficiente.

public interface ILogger
{
  void Log(LogLevel level, string message);
}

public static class LoggerUtils 
{
  public static void Log(this ILogger logger, Exception ex) => 
    logger.Log(LogLevel.Error, ex.ToString());
}

Essa abordagem, inclusive, também tem a vantagem de permitir que, se for necessário, cada implementação possa substituir o código “padrão” por um específico.

Uma abordagem confusa de design

Além do post de Mads, a Microsoft também publicou um tutorial apontando, ainda, uma outra abordagem para a utilidade da feature.

No tutorial, a Microsoft transforma interfaces quase em “classes abstratas”, porém, com autorização para herança múltipla. Afinal, interfaces agora suportam modificadores de visibilidade, atributos estáticos e mais.

As ideias propostas tem seus méritos, mas implicam em uma nova construção de raciocínio e, mais uma vez, não acreditamos que os benefícios compensem a complexidade acidental.

Uma possibilidade interessante

Em nosso entendimento, entretanto, há um aspecto ainda pouco explorado que pode ser interessante: a implementação padrão de métodos em interfaces autoriza uma implementação, mesmo que rudimentar, de Traits

Traits é uma abordagem de modelagem onde compomos um tipo a partir de várias implementações independentes.

Traits, como recurso de modelagem, amplifica as potencialidades da linguagem e permitem a adoção de ideias que deixam o código mais expressivo quanto a sua intencionalidade.

public interface CanBounce
{
  void Bounce() => Console.WriteLine("Bounce!"); 
}

public interface CanRoll
{
   void Roll() => Console.WriteLine("Roll"); 
}

public class Ball : CanBounce, CanRoll {};
public class Cylinder: CanRoll {};
public class Cube : CanBounce {};

Há, também, material da Microsoft explorando essa possibilidade.

Embora a adição de suporte a Traits seja uma novidade interessante, acreditamos, que para adicionar esse novo conceito, seria mais interessante a criação de outra palavra-chave, não usando interfaces, mesmo que sem modificações na CIL.

Nosso veredito

De forma geral, raramente recomendaríamos utilizar implementações padrão em interfaces, exceto para explorar Traits como recurso de modelagem. De qualquer maneira, nos preocupa o “tamanho” da linguagem e a complexidade inerente a este tamanho.

No fim, o recurso não é nem bom, nem ruim. Dependerá do uso que cada um irá fazer. De qualquer forma, é preocupante que boa parte dos materiais expostos pela Microsoft apontem para formas de adoção que, em primeira vista, parecem equivocadas.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
9 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
André Pontes
André Pontes
4 anos atrás

A primeira coisa que pensei quando vi isso foi justamente a possibilidade de múltiplas heranças e interfaces substituindo classes abstratas. Com essa funcionalidade, as próprias classes abstratas ficam meio sem sentido, você não acha? Será que a estratégia não é, aos poucos, eliminar as classes abstratas em favor dessas superinterfaces ao mesmo tempo em que a linguagem passa a ter múltiplas heranças?

Allan Silva
Allan Silva
4 anos atrás

Se não me engano, o Java fez isso para conseguir implementar a API de streams, e não quebrar a retrocompatibilidade, assim, p. ex., todos que herdam de List(interface em java), não precisam implementar a função `stream` (herdada da interface Collection).

Note que o mesmo vale para o exemplo da microsoft. E apesar de semelhante, é diferente de uma classe abstrata. A classe abstrata, ao meu ver, mantém muito mais informação em relação àquele Tipo, do que a interface. Um caso de uso do `ILogger` seria:

Eu (e milhares de outras pessoas) tenho uma classe (MyLogger), que implementa a `ILogger` disponibilizada por uma lib da Microdoft. Eu quero sempre que essa lib da MS esteja atualizada na sua última versão. A Microsoft decide adicionar um novo membro na interface `ILogger`, podemos supor 2 opções para os usuários da lib:

1 – Implementar o novo membro de interface e lançar um novo release;
2 – Fixar versão da lib.

Com implementação default, eu posso sempre me manter atualizado com a última versão da lib da MS, sem me preocupar de implementar o novo membro de interface. Na verdade, a sensação que tenho é que sem default impl, a quantidade de NIE são bem maiores. Com essa nova abordagem, eu como um mantenedor de lib, posso ter mais segurança nas minhas dependencias, posso dizer aceito a libmslogger>=1.1, p. ex.

Traits, acho que também é algo diferente, pois pensando em Rust, nós implementamos o trait X para a estrutura Y, e quando queremos utilizar a implementação do trait, precisamos que ele esteja no escopo para ter acesso às funções. Default impl, são realmente partes da interface, não estando desconectado da classe, como no caso dos traits que estão desconectados das structs.

“Afinal, interfaces agora suportam modificadores de visibilidade, atributos estáticos e mais.” – No exemplo, isso difere também de uma classe abstrata. Perceba que o novo membro da interface com default impl é `decimal ComputeLoyaltyDiscount()` e `protected static decimal DefaultLoyaltyDiscount` serve para dar suporte à ele, não são membros de instância (coisa que vc encontraria em uma classe abstrata).

O que é beeem bizarro e eu não confiaria aos usuários (eu incluso), é a possibilidade de parametrizar, provida por `void SetLoyaltyThresholds(TimeSpan ago, int minimumOrders, decimal percentageDiscount)`. Se fosse parecido com uma classe abstrata, essa abordagem seria desnecessária também.

Elemar Júnior
Elemar Júnior
4 anos atrás

Você concorda, que o caso de Logger, só faz sentido por ser possível entregar uma boa implementação default, como disse no post?

Allan Silva
Allan Silva
4 anos atrás

Com certeza, o mantenedor de uma lib, teria que ter pelo menos uma implementação default aplicável para o novo membro de interface, para usar esse recurso. Não faria sentido, criar uma implementação default e lançar uma NIE, pq se vc precisar lançar NIE, vc não precisa de uma implementação default.

Elemar Júnior
Elemar Júnior
4 anos atrás

Então, segundo minha perspectiva, cenários com boas implementações default são raras. Logo, esse não deveria ser o principal argumento dessa feature

Allan Silva
Allan Silva
4 anos atrás

Sim, vai do desenvolvedor usar a feature com cuidado. Acho também, que é possível que a MS explore bastante essa feature no .netcore, ao entregar atualizações de pacotes individuais.

Vitor Fitzner
Vitor Fitzner
4 anos atrás

Entender as features da linguagem e como o compilador funciona é um diferencial na solução de problemas diariamente, mas é preciso ter muita cautela na adoção de certas features.

Acredito que profissionais mais voltados a princípios, padrões e práticas ágeis irão evitar o uso de interfaces com implementações. Alguns dos motivos mais claros seriam:
* Herança é o tipo de acoplamento mais forte que existe;
* Programe para interfaces e não para implementações;
* Prefira composição em vez de herança;
* Confundir o conceito de interfaces em Orientação a Objetos;

A preocupação de receber erros em tempo de execução compete mais a ausência de testes do que a feature da linguagem em si.

Elemar Júnior
Elemar Júnior
4 anos atrás

Concordo com quase tudo. Mas, o compilador pode, sim, antecipar e previnir erros de tempo de execução. Veja, por exemplo, que esse é um dos principais argumentos de RUST: a linguagem inteira é projetada para impedir erros mais comuns.

Novamente, acho que a única “dentro” de implementação padrão em interface seja a possibilidade de modelar traits.

Antonio Maniero
Antonio Maniero
4 anos atrás

Eu concordo que trait deveria ser o melhor uso do mecanismo, mas o que fizeram ficou longe de servir como trait, inclusive mesmo para fazer o básico ainda só funciona com essa gambiarra: `((CanRoll)a).Roll();` -> https://dotnetfiddle.net/wsGduC.

Embora eu ache que o trait é a demonstração que OOP não deu tão certo quanto alguns acham 😛

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
EximiaCo 2024 - Todos os direitos reservados
9
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

“Default Implementations” em interfaces no C#. Novidade bem-vinda ou erro da Microsoft?

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

“Default Implementations” em interfaces no C#. Novidade bem-vinda ou erro da Microsoft?

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?