Go: Composição vs Herança

Vinicius Pacheco
3 min readJul 3, 2016

--

Primeiramente uma coisa muito importante para dizer é que Go não tem herança. Num primeiro momento para pessoas que estão acostumadas com OOP, o fato de uma linguagem não ter herança parece um grande absurdo. Isso é variável de pessoa para pessoa, no entanto eu prefiro seguir a linha de Joshua Block que diz: “Prefira composição em vez de herança”. Isso porque para Block a herança é uma forma de quebrar o encapsulamento. Martin Fowler trata do mesmo assunto nesse artigo.

Outro ponto para entender, é que herança NÃO é polimorfismo. O polimorfismo pode ser adquirido de outras formas. Porém é inegável que herança gera economia de código. Mas então, como ter economia de código e polimorfismo? A resposta é muito simples: Interfaces e composição. Go oferece os dois :)

Composição

Primeiramente eu vou falar de composição. No código abaixo eu crio dois tipos: "pai" e "filho"

package maintype pai struct {
nome string
idade int
}
type filho struct {
pai //pai compondo o tipo filho
email string
}

Observe que o tipo "pai" está compondo o tipo "filho". Na prática isso significa dizer que os campos e métodos que o tipo "pai" possuir também vão estar no tipo filho. Com isso, mesmo sem as propriedades nome e idade escritas diretamente no tipo "filho", pela composição eu consigo acessar as mesmas. Já o tipo "pai" não tem acesso a propriedade email, pois só o "filho" a possui, como no exemplo abaixo:

func main() {
pai := new(pai)
pai.nome="João"
pai.idade=50
filho := new(filho)
filho.nome = "Carlos"
filho.idade = 20
filho.email = "carlos@teste.com"
}

Interface

O fato da composição existir não significa em nenhum momento que "pai" e "filho" são do mesmo tipo. Para que isso ocorra, temos que fazer através de uma interface

type familia interface {
dados() string
}

Em Go, para que um tipo assine uma interface basta que o tipo utilize todos os métodos de uma determinada interface. Sendo assim, se quisermos que "pai" e "filho" sejam uma "familia" temos que fazer com que os dois tipos possuam o método "dados".

type pai struct {
nome string
idade int
}
func (p pai) dados() string {
return fmt.Sprintf("Nome: %s, Idade: %d", p.nome, p.idade)
}
type filho struct {
pai
email string
}
func (f filho) dados() string {
return fmt.Sprintf("Nome: %s, Idade: %d, Email: %s", f.nome, f.idade, f.email)
}

Agora tanto "pai" como "filho" além dos seus próprios tipos, são do tipo família. Para mostrar o polimorfismo vou escrever uma função que só recebe o tipo família para mostrar o conteúdo do método "dados".

func mostraDados(membro familia) {
fmt.Println(membro.dados())
}

Com minhas estruturas, interface, composições e função declaradas, vou chama a função "mostraDados" na minha função "main" para ter o resultado final. Vamos ver como ficou o código completo do exemplo:

package mainimport(
"fmt"
)
type familia interface {
dados() string
}
type pai struct {
nome string
idade int
}
func (p pai) dados() string {
return fmt.Sprintf("Nome: %s, Idade: %d", p.nome, p.idade)
}
type filho struct {
pai
email string
}
func (f filho) dados() string {
return fmt.Sprintf("Nome: %s, Idade: %d, Email: %s", f.nome, f.idade, f.email)
}
func mostraDados(membro familia) {
fmt.Println(membro.dados())
}
func main() {
pai := new(pai)
pai.nome="João"
pai.idade=50
filho := new(filho)
filho.nome = "Carlos"
filho.idade = 20
filho.email = "carlos@teste.com"
mostraDados(pai)
mostraDados(filho)
}

Com o código acima nós possuímos a seguinte saída após executar um "go run":

Nome: João, Idade: 50
Nome: Carlos, Idade: 20, Email: carlos@teste.com

Conclusão

Mesmo sem herança, o Go em seu design mostra que é possível trabalhar com reaproveitamento de código e polimorfismo utilizando composição e interfaces. É óbvio que para quem está muito acostumado com heranças é uma quebra de paradigma, mas composição e interfaces além de poderosos são uma ótima prática.

--

--

Vinicius Pacheco

Software Engineer at Eventbrite. Fan of patterns, clean code, Star Wars, coffee and Boca jrs desde Lejos