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 }
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:
- Defina uma interface.
- Defina um tipo que satisfaça o comportamento da interface.
- 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:
- Definir tipos
- 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 {
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!
