Meus métodos devem retornar interfaces ou structs em Go?
Tags: go, oop, programming, design
Published: 2020-07-12 15:50:20


[English version](https://dev.to/lcaparelli/should-my-methods-return-structs-or-interfaces-in-go-3b7)
___
Como a maioria das coisas no design de software, *depende*. E não posso enfatizar isso o suficiente: *realmente depende* da situação e de que resultado você está tentando alcançar.

Já posso até ouvir alguns de vocês gritando uma regra muito útil (ou até mesmo provérbio, se preferir) *"Accept interfaces, return structs" ("Aceite interfaces, retorne structs")*. É um conselho muito bom, realmente, mas sempre há exceções. Nenhum padrão é tão flexível que se adapta a todas as situações.

Com isso dito, eu apostaria que essa regra *faz* sentido a maioria das vezes, então vamos dar uma olhada no raciocínio que nos leva a retornar structs.

Mas primeiro, algumas noções básicas sobre como as interfaces funcionam no Go. Se você já as conhece bem, sinta-se livre para pular para a próxima seção.

# Interfaces no Go

É importante ter em mente que as interfaces em Go não estão estritamente vinculadas a tipos. O que quero dizer com isso é que você nunca diz explicitamente que um tipo implementa uma interface específica. Tudo o que você precisa fazer para implementar uma interface no Go é implementar os métodos que satisfazem essa interface.

Pense em uma interface como um conjunto de comportamentos que são esperados de um tipo. Você normalmente não vê patos com um rótulo escrito "Pato", mas sabe que *é* um porque parece um pato, grasna como um pato, tem penas como um pato, etc. Você deduz o que é a partir de seu comportamento, e é exatamente isso que o Go faz em tempo de compilação.

> Caso esteja se perguntando, o Go usa ["structural typing", não "Duck Typing"] (https://en.wikipedia.org/wiki/Duck_typing#Comparison_with_other_type_systems), o que significa que tudo isso acontece em tempo de compilação, não em tempo de execução. Lembre-se de que Go é estaticamente tipado.

*Exemplo, por favor*. Dê uma olhada na seguinte interface `Pessoa`:

```go
type Person interface {
    Name() string
    Age() int
}
```
Uma possível implementação dessa interface é:

```go
type Gamer struct {
	name  string
	age   int
}

func NewGamer(name string, age int) *Gamer {
	return &Gamer{
		name: name,
		age:  age,
	}
}

func (g *Gamer) Name() string {
	return g.name
}

func (g *Gamer) Age() int {
	return g.age
}
```

A Go entende que `Gamer` é uma `Person`?

```go
func main() {
	var person Person = NewGamer("Jane Doe", 42)
	fmt.Println(person.Name())
}
```

A saída deste programa:

```
╰─ go run example.go
Jane Doe
```

Sim, o Go entende que o tipo `Gamer` é uma` Person` (ou que implementa a interface `Person`)!

# Quando retornar structs

Tudo bem, agora que compreendemos o funcionamento das interfaces em Go, vamos imaginar que desejamos estender o comportamento do `Gamer`. Também vamos dizer que tem um slice de jogos (`Game`):

```go
type Person interface {
    Name() string
    Age() int
}

type Game string
type Gamer struct {
	name  string
	age   int
	games []Game
}

func NewGamer(name string, age int) *Gamer {
	return &Gamer{
		name: name,
		age:  age,
	}
}

func (g *Gamer) Name() string {
	return g.name
}

func (g *Gamer) Age() int {
	return g.age
}

func (g *Gamer) Games() []Game {
	return g.games
}

func (g *Gamer) AddGame(game Game) {
	g.games = append(g.games, game)
}
```

Observe que o programa ainda funciona neste momento, pois o tipo `Gamer` ainda implementa `Person`, apenas adicionamos mais comportamento e estado a ele.

Agora, vamos tentar simplesmente modificar a função `main` que tínhamos antes para fazer uso dessas novas coisas que adicionamos:

```go
func main() {
	var person Person = NewGamer("Jane Doe", 42)
	fmt.Println(person.Name())
	person.AddGame("Metal Gear Solid 3")
}
```

Nos damos de cara com:

```
╰─ go run example.go
# command-line-arguments
./example.go.go:46:8: person.AddGame undefined (type Person has no field or method AddGame)
```

Isso ocorre porque a variável `person` é do tipo` Person`, que não possui o método `AddGame()`. Se criarmos uma variável `gamer` do tipo `*Gamer`, podemos acessar esses novos métodos legais e brilhantes:


```go
func main() {
	var person Person = NewGamer("Jane Doe", 42)
	fmt.Println(person.Name())
	
	var gamer *Gamer = NewGamer("John Doe", 29)
	gamer.AddGame("Metal Gear Solid 3")
	fmt.Println(gamer.Games())
}
```

A execução ocorre sem problemas:

```
╰─ go run example.go  
Jane Doe
[Metal Gear Solid 3]
```

Agora, e se a função `NewGamer()` na verdade retornasse a interface `Person`?

```go
func NewGamer(name string, age int) Person {
	return &Gamer{
		name: name,
		age:  age,
	}
}
```

Nesse cenário, nunca poderíamos fazer a seguinte atribuição:

```go
var gamer *Gamer = NewGamer("John Doe", 29)
```

Porque `NewGamer` retorna `Person`, não `*Gamer`. E mesmo se mudarmos o tipo de `gamer` para `Person`, ficaríamos presos ao comportamento da interface `Person` **apenas**. Perderíamos o acesso aos métodos que somente o tipo `Gamer` possui.

Essa é a principal motivação por trás do retorno de structs em vez de interfaces: deixa a cargo de quem está invocando a função ou método se quer seu retorno como uma interface ou se quer usá-lo como struct e, no final das contas, isso faz muito sentido.

# Quando retornar interfaces?

Vamos expandir um pouco as implementações anteriores do `Person`. Agora também teremos um tipo `Student`:

```go
type Student struct {
	name      string
	age       int
	knowledge int
}

func NewStudent(name string, age int) *Student {
	return &Student{
		name: name,
		age: age,
	}
}

func (s *Student) Name() string {
	return s.name
}

func (s *Student) Age() int {
	return s.age
}

func (s *Student) Study(hours int) {
	s.knowledge += hours
}
```

> Tornar-se uma pessoa cheia de conhecimento não é tão simples como o descrito aqui, mas esse modelo atende aos nossos requisitos. Lembre-se do princípio [KISS] (https://en.wikipedia.org/wiki/KISS_principle).

E se precisássemos imprimir os nomes de todas as pessoas que conhecemos? Nesse caso, uma função auxiliar retornando um slice de `Person` faria muito sentido, porque `Name()` faz parte da interface `Person`:

```go
func PrintAllNames() {
	for _, person := range getAllPeople() {
		fmt.Printf("Name: %s\n", person.Name())
	}
}

func getAllPeople() []Person {
	var people []Person
	// implementação da lógica
	return people
}
```

Isso é simples, mas um dos casos em que é realmente aceitável que um método ou função retorne uma interface em vez da própria estrutura.

Sem uma função como essa, você precisaria criar uma função separada para obter todas as instâncias de **cada** implementação de `Person`, outra para imprimir os nomes dessas instâncias para **cada** implementação de `Person` e uma última que orquestra tudo isso.

```go
func PrintAllNames() {
	printAllGamerNames()
	printAllStudentNames()
}

func printAllGamerNames() {
	for _, gamer := range getAllGamers() {
		fmt.Printf("Name: %s\n", gamer.Name())
	}
}

func getAllGamers() []Gamer {
	var gamers []Gamer
	// implementação da lógica
	return gamers
}

func printAllStudentNames() {
	for _, student := range getAllStudents() {
		fmt.Printf("Name: %s\n", student.Name())
	}
}

func getAllStudents() []Student {
	var students []Student
	// implementação da lógica
	return students
}
```

Vê aquela repetição desagradável de código? E pior: prepare-se para fazer isso toda vez que adicionar outra implementação do `Person`. Aposto que não está feliz com isso. E **ainda** pior: esse tipo de adição manual é suscetível a erros e aumenta a probabilidade de introduzir um bug.

Você poderia dizer *"bem, mas mesmo que você retornasse um slice de `Person`, você ainda precisaria escrever a lógica necessária para obter todas as instâncias de uma nova implementação de `Person`"* e você estaria correta/correto. Mas observe como você não precisa escrever uma lógica específica para imprimir os nomes, isso é feito apenas uma vez na versão anterior da função `PrintAllNames()`:

```go
func PrintAllNames() {
	for _, person := range getAllPeople() {
		fmt.Printf("Name: %s\n", person.Name())
	}
}
```

Resumindo: retornar structs é uma boa ideia na maioria das vezes, porque você deixa o usuário livre para escolher como usar seu retorno, mas há casos em que o uso de uma interface faz mais sentido e pode evitar a escrita de código desnecessária.

Obrigado por ler o meu artigo! Deixe-me saber sua opinião sobre os pontos que levantei e se você tiver uma visão diferente, diga qual é! Estou sempre disponível para uma discussão produtiva. :-)