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.
Veja também
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.
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?
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.
Você concorda, que o caso de Logger, só faz sentido por ser possível entregar uma boa implementação default, como disse no post?
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.
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
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.
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.
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.
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 😛