Quando escrever métodos de extensão para suas próprias classes?

8

Eu vi recentemente uma base de código que tinha uma classe de dados Addressdefinida em algum lugar e depois em um lugar diferente:

fun Address.toAnschrift() = let { address ->
    Anschrift().apply {
        // mapping code here...
    }
}

Achei confuso não ter esse método no endereço diretamente. Existem padrões estabelecidos ou práticas recomendadas quando usar métodos de extensão? Ou o livro ainda precisa ser escrito?

Observe que, apesar da sintaxe Kotlin no meu exemplo, estou interessado nas melhores práticas gerais, pois também se aplicaria ao C # ou a outros idiomas com esses recursos.

Robert Jack Will
fonte
4
Com "Anschrift" sendo uma versão em alemão de "endereço", esta parece ser uma conversão específica de país de um tipo de endereço neutro. Desejar manter o código específico do país em outro lugar faz sentido. Esta não é uma resposta para sua pergunta real, mas explica por que eles fizeram isso neste exemplo .
R. Schmitz
1
»Os métodos de extensão permitem" adicionar "métodos a tipos existentes sem criar um novo tipo derivado, recompilar ou modificar o tipo original« docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… . »Para uma biblioteca de classes que você implementou, você não deve usar métodos de extensão ... Se desejar adicionar funcionalidade significativa a uma biblioteca da qual você possui o código-fonte, siga as diretrizes padrão do .NET Framework para versão de montagem« tl; dr se você possui o código, não há necessidade de métodos de extensão.
9788 Thomas Junk

Respostas:

15

Os métodos de extensão não fornecem algo que não possa ser feito de outras maneiras. Eles são açúcar sintático para tornar o desenvolvimento mais agradável, sem fornecer um benefício técnico real.


Os métodos de extensão são relativamente novos. Padrões e convenções geralmente são decididos pela opinião popular (além de alguma experiência de longo prazo sobre o que acaba sendo uma má idéia), e não acho que estamos no ponto em que podemos traçar uma linha definitiva com a qual todos concordam.

No entanto, posso ver vários argumentos, com base nas coisas que ouvi de colegas de trabalho.

1. Eles geralmente estão substituindo métodos estáticos.

Os métodos de extensão são mais comumente baseados em coisas que seriam métodos estáticos, não métodos de classe. Eles se parecem com métodos de classe, mas na verdade não são.

Métodos de extensão simplesmente não devem ser considerados uma alternativa para métodos de classe; mas como uma maneira de fornecer aos métodos estáticos sintaticamente alternados uma aparência e um "método de classe".

2. Quando o método pretendido for específico para um projeto consumidor, mas não a biblioteca de origem.

Só porque você desenvolve a biblioteca e o consumidor não significa que você está disposto a colocar a lógica onde quer que ela se encaixe.

Por exemplo:

  • Você tem uma PersonDtoturma vinda do seu DataLayerprojeto.
  • Seu WebUIprojeto deseja converter um PersonDtopara um PersonViewModel.

Você não pode (e não gostaria de) adicionar esse método de conversão ao seu DataLayerprojeto. Como esse método está no escopo do WebUIprojeto, ele deve residir dentro desse projeto.

Um método de extensão permite que você tenha esse método acessível globalmente dentro do seu projeto (e possíveis consumidores do seu projeto), sem exigir que a DataLayerbiblioteca o implemente.

3. Se você acha que as classes de dados devem conter apenas dados.

Por exemplo, sua Personclasse contém as propriedades para uma pessoa. Mas você deseja ter alguns métodos que formatam alguns dos dados:

public class Person
{
    //The properties...

    public SecurityQuestionAnswers GetSecurityAnswers()
    {
        return new SecurityQuestionAnswers()
        {
            MothersMaidenName = this.Mother.MaidenName,
            FirstPetsName = this.Pets.OrderbyDescending(x => x.Date).FirstOrDefault()?.Name
        };
    }
}

Eu já vi muitos colegas de trabalho que odeiam a idéia de misturar dados e lógica em uma classe. Não concordo com coisas simples, como formatação de dados, mas admito que, no exemplo acima, parece sujo Personter que depender SecurityQuestionAnswers.

Colocar esse método em um método de extensão evita o manuseio da classe de dados pura.

Observe que esse argumento é de estilo. Isso é semelhante às classes parciais. Há pouco ou nenhum benefício técnico em fazê-lo, mas ele permite que você separe o código em vários arquivos se você o considerar mais limpo (por exemplo, se muitas pessoas usarem a classe, mas não se importarem com seus métodos personalizados adicionais).

4. Métodos auxiliares

Na maioria dos projetos, acabo tendo aulas auxiliares. Essas são classes estáticas que geralmente fornecem uma coleção de métodos para facilitar a formatação.

Por exemplo, uma empresa em que trabalhei tinha um formato de data e hora específico que eles queriam usar. Em vez de colar a string de formato em todo o lugar ou transformar a string de formato em uma variável global, optei pelo método de extensão DateTime:

public static string ToCompanyFormat(this DateTime dt)
{
    return dt.ToString("...");
} 

Isso é melhor do que um método auxiliar estático normal? Acho que sim. Ele limpa a sintaxe. Ao invés de

DateTimeHelper.ToCompanyFormat(myObj.CreatedOn);

Eu poderia fazer:

myObj.CreatedOn.ToCompanyFormat();

Isso é semelhante a uma versão fluente da mesma sintaxe. Gosto mais, mesmo que não exista nenhum benefício técnico em ter um sobre o outro.


Todos esses argumentos são subjetivos. Nenhum deles cobre um caso que nunca poderia ser coberto.

  • Todo método de extensão pode ser facilmente reescrito para um método estático normal.
  • O código pode ser separado em arquivos usando classes parciais, mesmo que não existam métodos de extensão.

Mas, novamente, podemos aceitar sua afirmação de que eles nunca são necessários ao extremo:

  • Por que precisamos de métodos de classe, se podemos sempre criar métodos estáticos com um nome que indique claramente que é para uma classe específica?

A resposta a esta pergunta, e a sua, permanece a mesma:

  • Sintaxe mais agradável
  • Maior legibilidade e menos pedantry de código (o código chegará ao ponto em menos caracteres)
  • O Intellisense fornece resultados contextualmente significativos (os métodos de extensão são mostrados apenas no tipo correto. As classes estáticas são utilizáveis ​​em qualquer lugar).

Uma menção extra específica:

  • Os métodos de extensão permitem um método de encadeamento de códigos; que se tornou mais popular ultimamente, em oposição à sintaxe de quebra de método de baunilha. Preferências sintáticas semelhantes podem ser vistas na sintaxe do método do LINQ.
  • Embora o encadeamento de métodos também possa ser feito usando métodos de classe; lembre-se de que isso não se aplica aos métodos estáticos. Os métodos de extensão são mais comumente baseados em coisas que seriam métodos estáticos, não métodos de classe.
Flater
fonte
5
O ponto 2. é o que eu acho mais convincente e a razão pela qual eu mesmo fiz isso. Às vezes, você tem uma classe amplamente usada em toda a sua base de código e um método que parece que "deveria" estar na classe, exceto pelo fato de envolver algum projeto ou biblioteca de terceiros que você deseja encapsular. Os métodos de extensão para suas próprias classes são uma maneira lógica de lidar com isso.
Astrid
@ Astrid: Eu acho que essa é a razão mais técnica para fazer isso; sim.
Flater
Resposta muito boa. Um caso de uso específico do C # é que as interfaces não têm comportamento. Os métodos de extensão nos permitem fornecer métodos.
RubberDuck
O que você quer dizer com "benefícios técnicos?" Coisas como desempenho? A legibilidade do código é igualmente importante, se não mais.
Robert Harvey
@RobertHarvey Por técnico, quero dizer qualquer coisa que não seja apenas açúcar sintático. Por exemplo, espalhar uma classe parcial por muitos arquivos no mesmo conjunto não tem vantagem técnica. Espalhar uma classe e seus métodos de extensão por diferentes assemblies sim. Obviamente, a legibilidade do código ainda é importante, mas não do ponto de vista técnico.
Flater
5

Concordo com Flater que não houve tempo suficiente para a convenção se cristalizar, mas temos o exemplo das próprias bibliotecas de classes do .NET Framework. Considere que, quando os métodos LINQ foram adicionados ao .NET, eles não foram adicionados diretamente IEnumerable, mas como métodos de extensão.

O que podemos tirar disso? LINQ é

  1. um conjunto coeso de funcionalidade que nem sempre é necessário,
  2. deve ser usado no estilo de encadeamento de métodos (por exemplo, em A.B().C()vez de C(B(A))) e
  3. não requer acesso ao estado privado do objeto

Se você tiver todos esses três recursos, os métodos de extensão farão muito sentido. De fato, eu argumentaria que, se um conjunto de métodos tiver o recurso nº 3 e um dos dois primeiros, considere torná-los métodos de extensão.

Métodos de extensão:

  1. Permitem evitar poluir a API principal de uma classe,
  2. Não apenas permite o estilo de encadeamento de métodos, mas também manipula nullvalores de maneira mais flexível , pois um método de extensão chamado de um nullvalor simplesmente recebe o nullprimeiro argumento,
  3. Impedir que você acesse / modifique acidentalmente o estado privado / protegido em um método que não deve ter acesso a ele

Os métodos de extensão têm mais um benefício: eles ainda podem ser usados ​​como um método estático e podem ser passados ​​diretamente como um valor para uma função de ordem superior. Então, se MyMethodé um método de extensão, em vez de dizer myList.Select(x => x.MyMethod()), você pode simplesmente usar myList.Select(MyMethod). Eu acho isso melhor.

James Jensen
fonte
4
O motivo pelo qual o LINQ foi implementado como um conjunto de métodos de extensão é que ele pretende ser um conjunto comum de operações em uma interface e você não pode colocar implementações em interfaces. Não tem nada a ver com seus pontos (1) e (2). Isso contrasta com a solução de Java para o mesmo problema, que é permitir implementações nas definições de interface (o que abre a porta para um mundo de abuso de interface).
Ant P
"e você não pode colocar implementações em interfaces". Ainda não, mas que a declaração não será mais verdadeiro quando C # 8 atinge nossas máquinas :)
David Arno
@DavidArno realmente? Estou fora do mundo C # há um tempo. Isso é uma vergonha. Parece um passo atrás para mim ... #
P Ant #
4

A força motriz por trás dos métodos de extensão é a capacidade de adicionar a funcionalidade necessária a todas as classes ou interfaces de um determinado tipo, independentemente de estarem seladas ou em outra montagem. Você pode ver evidências disso com o código LINQ, adicionando funções a todos os IEnumerable<T>objetos, sejam elas listas ou pilhas, etc. O conceito era a prova futura das funções, para que, se você tivesse criado o seu próprio, IEnumerable<T>pudesse usá-lo automaticamente.

Dito isto, o recurso de idioma é novo o suficiente para que você não tenha nenhuma orientação sobre quando usá-los ou não. No entanto, eles permitem várias coisas anteriormente impossíveis:

  • APIs fluentes (consulte Declarações fluentes para obter um exemplo de como adicionar APIs fluentes a praticamente qualquer objeto)
  • Integrando recursos (o NetCore Asp.NET MVC usa métodos de extensão para conectar dependências e recursos no código de inicialização)
  • Localizando o impacto de um recurso (seu exemplo na declaração de abertura)

Para ser justo, apenas o tempo dirá a que distância está longe demais ou em que momento os métodos de extensão se tornam um obstáculo, e não um benefício. Como mencionado em outra resposta, não há nada em um método de extensão que não possa ser feito com um método estático. No entanto, quando usados ​​criteriosamente, podem facilitar a leitura do código.

Que tal usar intencionalmente métodos de extensões no mesmo assembly?

Eu acho que vale a pena ter a funcionalidade principal bem testada e confiável, e depois não mexer com ela até que você precise. O novo código que depende dele, mas precisa fornecer funcionalidade exclusivamente para o benefício do novo código, pode usar os Métodos de extensão para adicionar quaisquer recursos para dar suporte ao novo código. Essas funções são de interesse apenas para o novo código, e o escopo dos métodos de extensão é limitado ao espaço para nome em que são declarados.

Eu não descartaria cegamente o uso de métodos de extensão, mas consideraria se a nova funcionalidade deve ser realmente adicionada ao código principal existente que é bem testado.

Berin Loritsch
fonte
1
+1 para apis fluentes.
Robert Harvey
@RobertHarvey, Meu último parágrafo sob o título "dentro da mesma assembléia"? Se você tiver o código-fonte para esse assembly, poderá fazer o que quiser com ele, mas uma vez compilado, é selado.
Berin Loritsch
Removi meu comentário, pois aparentemente eu não estava comunicando efetivamente meu argumento.
Robert Harvey
1

Existem padrões estabelecidos ou práticas recomendadas quando usar métodos de extensão?

Um dos motivos mais comuns para o uso de métodos de extensão é como um meio de "estender, não modificar" interfaces. Em vez de ter IFooe IFoo2, onde este introduz um novo método Bar, Baré adicionado a IFooum método de extensão.

Uma das razões pelas quais o Linq usa métodos de extensão Where, Selectetc , é para permitir que os usuários definam suas próprias versões desses métodos, que também são usados ​​pela sintaxe de consulta do Linq.

Além disso, a abordagem que eu uso, que parece se encaixar no que normalmente vejo na estrutura .NET, é:

  1. Coloque métodos diretamente relacionados à classe dentro da própria classe,
  2. Coloque todos os métodos que não estão relacionados à funcionalidade principal da classe nos métodos de extensão.

Então, se eu tenho um Option<T>, eu ia colocar coisas como HasValue, Value, ==, etc dentro do próprio tipo. Mas algo como uma Mapfunção, que converte a mônada de T1para T2, eu colocaria um método de extensão:

public static Option<TOutput> Map<TInput, TOutput>(this Option<TInput> input, 
                                                   Func<TInput, TOutput> f) => ...
David Arno
fonte
1
One of the reasons Linq uses extension methods for Where, Select etc is in order to allow users to define their own versions of these methods, which are then used by the Linq query syntax too. -- Você tem certeza sobre isso? A sintaxe do Linq faz uso extensivo de expressões lambda (ou seja, funções de primeira classe), para que não seja necessário escrever suas próprias versões de Selecte Where. Não conheço nenhum caso em que as pessoas estejam lançando suas próprias versões dessas funções.
Robert Harvey
Dito isto, acho a sua resposta a mais convincente de todas as respostas até agora. Os métodos de extensão são uma ótima maneira de desenvolver interfaces, e é exatamente por isso que eles foram criados para o Linq.
Robert Harvey
@RobertHarvey, você não precisa, mas pode .
David Arno
@RobertHarvey Se você alternar o if (!predicate(item))código para if (predicate(item)), os resultados mudam à medida que minha versão do Where.
David Arno
Sim, eu entendo. Mas ninguém jamais faria isso; eles simplesmente mudariam o predicado. Esse é o objetivo de ter predicados: alterar a condição sem alterar a função.
Robert Harvey #