Há pouco tempo, um colega retweetou um excelente post
Como usar as interfaces de uso . Ele discute alguns erros ao usar as interfaces no Go e também fornece algumas recomendações sobre como elas devem ser usadas.
No artigo mencionado acima, o autor cita a interface do pacote de
classificação da biblioteca padrão como um exemplo de um tipo de dados abstrato. No entanto, parece-me que esse exemplo não revela muito bem a ideia quando se trata de aplicativos reais. Especialmente sobre aplicativos que implementam a lógica de um campo comercial ou resolvem problemas do mundo real.
Além disso, ao usar interfaces no Go, geralmente há um debate sobre a superengenharia. E também acontece que, depois de ler esse tipo de recomendação, as pessoas não apenas param de abusar das interfaces, mas tentam abandoná-las completamente, privando-as de usar um dos mais fortes conceitos de programação em princípio (e um dos pontos fortes do Go in particular). A propósito, sobre os erros típicos no Go, há um
bom relatório de Stive Francia, do Docker. Lá, em particular, as interfaces são mencionadas várias vezes.
Em geral, concordo com o autor do artigo. Não obstante, pareceu-me que o tópico de usar interfaces como tipos abstratos de dados foi revelado superficialmente; portanto, gostaria de desenvolvê-lo um pouco e refletir sobre esse tópico com você.
Consulte o original
No início do artigo, o autor fornece um pequeno exemplo de código, com a ajuda do qual ele aponta erros ao usar interfaces que os desenvolvedores costumam criar. Aqui está o código.
package animal type Animal interface { Speaks() string }
package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() }
O autor chama essa abordagem de
"uso da interface no estilo Java" . Quando declaramos uma interface, implementamos o único tipo e métodos que satisfarão essa interface. Concordo com o autor, a abordagem é mais ou menos.O código mais idiomático do artigo original é o seguinte:
package animal
package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
Aqui, em geral, tudo é claro e compreensível. A idéia básica:
“Primeiro declare os tipos e somente depois declare as interfaces no ponto de uso” . Isto está correto. Mas agora vamos desenvolver uma pequena idéia sobre como você pode usar interfaces como tipos de dados abstratos. O autor, aliás, ressalta que, em tal situação, não há nada de errado em declarar a interface
"inicial" . Vamos trabalhar com o mesmo código.
Vamos brincar com abstrações
Então, nós temos um circo e há animais. Dentro do circo, existe um método bastante abstrato
chamado `Perform ' , que pega a interface do'
Speaker` e faz o animal emitir sons. Por exemplo, ele fará o cachorro latir a partir do exemplo acima. Crie um domador de animais. Como ele não é burro aqui, geralmente também podemos fazê-lo emitir sons. Nossa interface é bastante abstrata. :)
package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" }
Até agora, tudo bem. Nós estamos indo além. Vamos ensinar nosso domador a dar comandos para animais de estimação? Até agora, teremos um comando de
voz . :)
package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" }
package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d)
Mmmm, interessante, não é? Parece que nosso colega não está feliz por ter se tornado um animal de estimação nesse contexto? : D O que fazer?
Speaker parece que uma abstração não é muito adequada aqui. Criaremos uma versão mais adequada (ou melhor, retornaremos de alguma forma a primeira versão do
"exemplo errado" ) e, em seguida, alteraremos a notação do método.
package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { }
Isso não muda nada, você diz, o código ainda será executado, porque ambas as interfaces implementam um método, e você estará certo em geral.
No entanto, este exemplo captura uma ideia importante. Quando falamos sobre tipos de dados abstratos, o contexto é crucial. A introdução de uma nova interface, pelo menos, tornou o código em uma ordem de magnitude mais óbvia e legível.
A propósito, uma das maneiras de forçar o domador a não executar o comando
"voz" é simplesmente adicionar um método que ele não deveria ter. Vamos adicionar esse método, ele fornecerá informações sobre se o animal de estimação é treinável.
package circus type Animal interface { Speaker IsTrained() bool }
Agora, o domador não pode ser colocado no lugar de um animal de estimação.
Expandir Comportamento
Forçaremos nossos animais de estimação, para variar, a executar outros comandos, além disso, vamos adicionar um gato.
package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" }
package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" }
Ótimo, agora podemos dar ordens diferentes aos nossos animais, e eles os cumprirão. Em um grau ou outro ...: D
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d)
Nossos gatos domésticos não são particularmente receptivos ao treinamento. Portanto, ajudaremos o domador e garantiremos que ele não sofra com eles.
package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") }
Isso é melhor. Diferente da interface inicial do
Animal , que duplica o
Speaker , agora temos a interface
`Animal ' (que é essencialmente um tipo de dados abstrato) que implementa um comportamento bastante significativo.
Vamos discutir os tamanhos da interface
Agora vamos refletir sobre um problema como o uso de interfaces amplas.
Essa é uma situação em que usamos interfaces com um grande número de métodos. Nesse caso, a recomendação é mais ou menos assim:
"As funções devem aceitar interfaces contendo os métodos de que precisam" .
Em geral, concordo que as interfaces devem ser pequenas, mas, neste caso, o contexto importa novamente. Vamos voltar ao nosso código e ensinar nosso domador a
"elogiar" seu animal de estimação.
Em resposta a elogios, o animal emitirá uma voz.
package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() }
Parece que está tudo bem, usamos a interface mínima necessária. Não há nada supérfluo. Mas aqui novamente o problema. Droga, agora podemos
"elogiar" o outro treinador e ele
"dará uma voz" . : D Pegue? .. Contexto sempre importa.
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d)
Por que eu estou? Nesse caso, a melhor solução ainda seria usar uma interface mais ampla (representando o tipo de dados abstratos
"pet" ). Como queremos aprender a elogiar um animal de estimação, não qualquer criatura que possa emitir sons.
package circus
Muito melhor. Podemos elogiar o animal de estimação, mas não podemos elogiar o domador. O código novamente se tornou mais simples e mais óbvio.
Agora um pouco sobre a Lei da Cama
O último ponto que gostaria de abordar é a recomendação de que devemos aceitar um tipo abstrato e retornar uma estrutura específica. No artigo original, essa menção é dada na seção que descreve a chamada
Lei de Postel .
O autor cita a própria lei:.
"Seja conservador com o que você faz, seja liberal com você aceita"
E interpreta em relação ao idioma Go
"Go": "Aceitar interfaces, retornar estruturas"
func funcName(a INTERFACETYPE) CONCRETETYPE
Você sabe, em geral, eu concordo, esta é uma boa prática. No entanto, quero enfatizar novamente. Não tome isso literalmente. O diabo está nos detalhes. Como sempre, o contexto é importante.
Nem sempre uma função deve retornar um tipo específico. I.e. se você precisar de um tipo abstrato, retorne-o. Não é necessário tentar reescrever o código, evitando abstrações.
Aqui está um pequeno exemplo. Um elefante apareceu em um circo
"africano" próximo, e você pediu aos proprietários do circo que emprestassem um elefante para um novo show. Para você, neste caso, é importante, apenas que o elefante possa executar todos os mesmos comandos que outros animais de estimação. O tamanho de um elefante ou a presença de uma tromba nesse contexto não importa.
package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} }
package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e)
Como você pode ver, como os parâmetros específicos do elefante que o distinguem de outros animais de estimação não são importantes para nós, podemos usar a abstração e retornar a interface nesse caso será bastante apropriado.
Resumir
O contexto é crucial quando se trata de abstrações. Não negligencie abstrações e tenha medo delas, assim como você não deve abusar delas. Você não deve aceitar recomendações como regras. Existem abordagens que foram testadas com o tempo; há abordagens que ainda precisam ser testadas. Espero ter conseguido abrir um pouco mais profundamente o tópico do uso de interfaces como tipos abstratos de dados e fugir dos exemplos usuais da biblioteca padrão.
Obviamente, para algumas pessoas, este post pode parecer óbvio demais, e exemplos são sugados do dedo. Para outros, meus pensamentos podem ser controversos e os argumentos não convincentes. No entanto, alguém pode se inspirar e começar a pensar um pouco mais profundamente não apenas sobre o código, mas também sobre a essência das coisas, bem como abstrações em geral.
O principal, amigos, é que você constantemente desenvolva e receba o verdadeiro prazer do trabalho. Bom para todos!
PS. O código de exemplo e a versão final podem ser encontrados
no GitHub .