Como usar interfaces no Go



Em seu tempo livre da obra principal, o autor do material consulta o Go e analisa o código. Naturalmente, durante uma atividade desse tipo, ele lê muitos códigos escritos por outras pessoas. Recentemente, o autor deste artigo tem a impressão (sim, impressão, sem estatística) de que os programadores têm mais probabilidade de trabalhar com interfaces no "estilo Java".

Este post contém recomendações do autor sobre o uso ideal de interfaces no Go, com base em sua experiência na criação de código.

Nos exemplos deste post, usaremos dois pacotes animal e circus . Muitas coisas neste post descrevem o trabalho com código que limita o uso regular de pacotes.

Como não fazer


Um fenômeno muito comum que observo:

 package animals type Animal interface { Speaks() string } //  Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animals" func Perform(a animal.Animal) string { return a.Speaks() } 

Esse é o chamado uso de interfaces no estilo Java. Pode ser caracterizado pelas seguintes etapas:

  1. Defina uma interface.
  2. Defina um tipo que satisfaça o comportamento da interface.
  3. Defina métodos que satisfazem a implementação da interface.

Em resumo, estamos lidando com "tipos de escrita que satisfazem interfaces". Este código tem seu próprio cheiro distinto, sugerindo os seguintes pensamentos:

  • Apenas um tipo satisfaz a interface, sem nenhuma intenção de expandi-la ainda mais.
  • As funções geralmente usam tipos concretos em vez de tipos de interface.

Como fazer


As interfaces no Go incentivam uma abordagem preguiçosa, o que é bom. Em vez de escrever tipos que satisfaçam interfaces, você deve escrever interfaces que atendam aos requisitos práticos reais.

O que significa: em vez de definir Animal no pacote animals , defina-o no ponto de uso, ou seja, o pacote circus * .

 package animals type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

Uma maneira mais natural de fazer isso é assim:

  1. Definir tipos
  2. Defina a interface no ponto de uso.

Essa abordagem reduz a dependência dos componentes do pacote de animals . Reduzir dependências é o caminho certo para criar software tolerante a falhas.

Lei da cama


Há um bom princípio para escrever um bom software. Essa é a lei de Postel , que geralmente é formulada da seguinte maneira:
"Seja conservador sobre o que você refere e liberal sobre o que você aceita"
Em termos de Go, a lei é:

"Aceitar interfaces, retornar estruturas"

Em suma, esta é uma regra muito boa para projetar coisas estáveis ​​e tolerantes a falhas * . A principal unidade de código no Go é uma função. Ao projetar funções e métodos, é útil aderir ao seguinte padrão:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

Aqui aceitamos tudo o que implementa uma interface que pode ser qualquer coisa, incluindo uma interface vazia. Um valor de um tipo específico é sugerido. Obviamente, limitar o que pode ser faz sentido. Como diz um provérbio Go:

"A interface vazia não diz nada", Rob Pike

Portanto, é altamente recomendável impedir que funções aceitem a interface{} .

Exemplo de Aplicação: Imitação


Um exemplo impressionante dos benefícios da aplicação da lei Postel são os casos de teste. Digamos que você tenha uma função parecida com esta:

 func Takes(db Database) error 

Se o Database for uma interface, no código de teste você poderá simplesmente fornecer uma imitação da implementação do Database de Database sem a necessidade de passar um objeto de banco de dados real.

Quando a definição de uma interface com antecedência é aceitável


Para dizer a verdade, a programação é uma maneira bastante gratuita de expressar idéias. Não há regras inabaláveis. Obviamente, você sempre pode definir interfaces com antecedência, sem medo de ser preso pela polícia de códigos. No contexto de muitos pacotes, se você conhece suas funções e pretende aceitar uma interface específica dentro do pacote, faça-o.

Definir uma interface geralmente cheira a excesso de engenharia, mas há situações em que você obviamente deve fazer exatamente isso. Em particular, os seguintes exemplos vêm à mente:

  • Interfaces seladas
  • Tipos de dados abstratos
  • Interfaces Recursivas

A seguir, consideramos brevemente cada um deles.

Interfaces seladas


Interfaces seladas só podem ser discutidas no contexto de vários pacotes. Uma interface selada é uma interface com métodos não exportados. Isso significa que usuários fora deste pacote não podem criar tipos que satisfazem essa interface. Isso é útil para emular um tipo de variante para procurar exaustivamente os tipos que satisfazem a interface.

Se você definiu algo como isto:

 type Fooer interface { Foo() sealed() } 

Somente o pacote definido pela Fooer pode usá-lo e criar algo de valor a partir dele. Isso permite criar operadores de chave de força bruta para tipos.

A interface selada também permite que as ferramentas de análise captem facilmente qualquer correspondência de padrão sem colisão. O pacote sumtypes do BurntSushi tem como objetivo resolver este problema.

Tipos de dados abstratos


Outro caso de definição antecipada de uma interface envolve a criação de tipos de dados abstratos. Eles podem ser selados ou não.

Um bom exemplo disso é o pacote de sort , que faz parte da biblioteca padrão. Ele define uma coleção classificável da seguinte maneira

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

Esse trecho de código incomoda muitas pessoas, porque se você quiser usar o pacote de sort , precisará implementar métodos para a interface. Muitos não gostam da necessidade de adicionar três linhas de código adicionais.

No entanto, acho que essa é uma forma muito elegante de genéricos no Go. Seu uso deve ser mais frequentemente incentivado.

Opções de design alternativas e ao mesmo tempo elegantes exigirão tipos de pedidos mais altos. Neste post, não os consideraremos.

Interfaces Recursivas


Este é provavelmente outro exemplo de código com um estoque, mas há momentos em que é simplesmente impossível evitar usá-lo. Manipulações simples permitem obter algo como

 type Fooer interface { Foo() Fooer } 

Um padrão de interface recursiva obviamente exigirá sua definição com antecedência. A recomendação de definição de interface de ponto de uso não é aplicável aqui.

Esse padrão é útil para criar contextos com trabalho subsequente neles. O código carregado pelo contexto geralmente se fecha dentro do pacote exportando apenas os contextos (ala o pacote tensor ); portanto, na prática, não vejo esse caso com tanta frequência. Posso dizer-lhe algo mais sobre padrões contextuais, mas deixe para outro post.

Conclusão


Apesar do fato de um dos títulos da postagem exibir "Como não fazer", não estou tentando proibir nada. Em vez disso, quero fazer com que os leitores pensem com mais frequência sobre as condições das fronteiras, pois é nesses casos que surgem várias situações de emergência.

Acho o princípio da declaração no ponto de uso extremamente útil. Como resultado de sua aplicação na prática, não encontro problemas que ocorram se eu o negligenciar.

No entanto, também ocasionalmente escrevo interfaces no estilo Java. Normalmente, isso acontece se, pouco antes, eu escrevi muito código em Java ou Python. O desejo de complicar demais e "apresentar tudo na forma de classes" às vezes se manifesta muito fortemente, especialmente se você escrever o código Go depois de escrever muito código orientado a objetos.

Assim, este post também serve como um lembrete para si mesmo sobre como é o caminho para escrever código, que não causará dor de cabeça posteriormente. Aguardando seus comentários!

imagem

Source: https://habr.com/ru/post/pt411029/


All Articles