Golang

Composição vs herança: o jeito Go de reutilizar código Já leu

9 min de leitura

Composição vs herança: o jeito Go de reutilizar código
A herança é um dos pilares da orientação a objetos clássica. Em Java, C++ e C#, reutilizar comportamento significa criar hierarquias de classes — uma classe filha herda campos e métodos da classe pai, que pode herdar de

A herança é um dos pilares da orientação a objetos clássica. Em Java, C++ e C#, reutilizar comportamento significa criar hierarquias de classes — uma classe filha herda campos e métodos da classe pai, que pode herdar de outra, formando árvores de herança que crescem com o projeto.

Go rejeitou esse modelo deliberadamente. Não existe herança em Go. Não existe extends, não existe classe base, não existe polimorfismo por hierarquia de tipos. Em seu lugar, Go oferece dois mecanismos complementares: composição via embedding e polimorfismo via interfaces. A combinação dos dois cobre todos os casos de uso da herança — sem suas desvantagens.


O problema da herança profunda

Antes de ver como Go resolve o problema, vale entender o que Go está evitando. Hierarquias de herança profunda criam acoplamento rígido entre classes. Uma mudança na classe base pode quebrar silenciosamente todas as subclasses. O problema do diamante — quando uma classe herda de duas classes que compartilham um ancestral comum — exige regras de resolução complexas. E o pior: a herança força o design a ser decidido cedo, quando menos se sabe sobre o problema.

// Hierarquia típica em Java — frágil e rígida
Animal
  └── Vertebrado
        ├── Mamífero
        │     ├── Cão
        │     └── Gato
        └── Ave
              ├── Pinguim
              └── Águia

O que acontece quando um Pinguim herda de Ave mas não pode voar? Quando um Ornitorrinco é mamífero mas bota ovos? A hierarquia quebra ou exige contorções.


Composição: construir com partes

O princípio da composição diz que um tipo deve ter comportamentos, não ser uma especialização de outro tipo. Em Go, isso é feito embutindo tipos dentro de outros tipos.

package main

import "fmt"

type Logger struct {
    Prefixo string
}

func (l Logger) Log(mensagem string) {
    fmt.Printf("[%s] %s\n", l.Prefixo, mensagem)
}

type ConexaoDB struct {
    Host string
    Porta int
}

func (c ConexaoDB) Conectar() {
    fmt.Printf("Conectando em %s:%d\n", c.Host, c.Porta)
}

// Servico compõe Logger e ConexaoDB
type Servico struct {
    Nome string
    Logger
    ConexaoDB
}

func main() {
    s := Servico{
        Nome:      "PedidoService",
        Logger:    Logger{Prefixo: "INFO"},
        ConexaoDB: ConexaoDB{Host: "localhost", Porta: 5432},
    }

    s.Log("Serviço iniciado")   // método promovido de Logger
    s.Conectar()                // método promovido de ConexaoDB
    fmt.Println(s.Nome)
}

Servico não é um Logger nem uma ConexaoDB. Ele tem um logger e tem uma conexão. Essa distinção semântica é fundamental — e reflete com mais precisão como os sistemas reais funcionam.


Composição com interfaces: o verdadeiro polimorfismo

A herança oferece polimorfismo por hierarquia — uma função que aceita Animal pode receber Cão ou Gato. Go oferece polimorfismo por interface — uma função que aceita Falante pode receber qualquer tipo que implemente Falar(), independentemente de onde esse tipo foi definido ou de quem o criou.

package main

import (
    "fmt"
    "math"
)

// Interfaces pequenas e focadas
type Descritivel interface {
    Descricao() string
}

type Calculavel interface {
    Calcular() float64
}

// Composição de interfaces
type Item interface {
    Descritivel
    Calculavel
}

type Produto struct {
    Nome  string
    Preco float64
    Qtd   int
}

func (p Produto) Descricao() string {
    return fmt.Sprintf("%s (x%d)", p.Nome, p.Qtd)
}

func (p Produto) Calcular() float64 {
    return p.Preco * float64(p.Qtd)
}

type Desconto struct {
    Descricao_ string
    Percentual float64
    Base       float64
}

func (d Desconto) Descricao() string {
    return fmt.Sprintf("%s (%.0f%%)", d.Descricao_, d.Percentual)
}

func (d Desconto) Calcular() float64 {
    return -(d.Base * d.Percentual / 100)
}

func totalPedido(itens []Item) float64 {
    total := 0.0
    for _, item := range itens {
        valor := item.Calcular()
        fmt.Printf("  %-30s R$ %8.2f\n", item.Descricao(), valor)
        total += valor
    }
    return total
}

func main() {
    itens := []Item{
        Produto{Nome: "Teclado Mecânico", Preco: 350.00, Qtd: 1},
        Produto{Nome: "Mouse Gamer",      Preco: 180.00, Qtd: 2},
        Produto{Nome: "Mousepad XL",      Preco: 90.00,  Qtd: 1},
        Desconto{Descricao_: "Cupom PROMO10", Percentual: 10, Base: 800.00},
    }

    fmt.Println("=== Resumo do Pedido ===")
    total := totalPedido(itens)
    fmt.Printf("  %-30s R$ %8.2f\n", "TOTAL", total)
}

Delegação explícita vs promoção

O embedding promove métodos automaticamente. Mas às vezes a delegação explícita — chamar o método do campo diretamente — é mais clara, especialmente quando o tipo externo precisa interceptar ou modificar o comportamento:

package main

import "fmt"

type Repositorio struct{}

func (r *Repositorio) Salvar(entidade string) error {
    fmt.Println("Salvando:", entidade)
    return nil
}

// RepositorioComLog intercepta o comportamento sem herança
type RepositorioComLog struct {
    repo   *Repositorio
    logger Logger
}

func (r *RepositorioComLog) Salvar(entidade string) error {
    r.logger.Log("Iniciando salvamento de: " + entidade)
    err := r.repo.Salvar(entidade)  // delegação explícita
    if err != nil {
        r.logger.Log("Erro ao salvar: " + err.Error())
        return err
    }
    r.logger.Log("Salvamento concluído: " + entidade)
    return nil
}

func main() {
    repo := &RepositorioComLog{
        repo:   &Repositorio{},
        logger: Logger{Prefixo: "REPO"},
    }

    repo.Salvar("Usuario{id:1, nome:Ana}")
}

Esse padrão é conhecido como Decorator — adiciona comportamento a um objeto sem modificar sua implementação original. Em Go, ele é implementado naturalmente através de composição, sem necessidade de nenhum framework.


Substituindo comportamento sem herança

Em linguagens com herança, sobrescrever um método significa redeclará-lo na subclasse. Em Go, o mecanismo equivalente é definir o método no tipo externo — ele terá precedência sobre o método promovido pelo embedding:

package main

import "fmt"

type Animal struct {
    Nome string
}

func (a Animal) Falar() string {
    return "..."
}

func (a Animal) Apresentar() {
    fmt.Printf("%s diz: %s\n", a.Nome, a.Falar())
}

type Cachorro struct {
    Animal
}

func (c Cachorro) Falar() string {
    return "Au au!"
}

type Gato struct {
    Animal
}

func (g Gato) Falar() string {
    return "Miau!"
}

func main() {
    c := Cachorro{Animal: Animal{Nome: "Rex"}}
    g := Gato{Animal: Animal{Nome: "Whiskers"}}

    fmt.Println(c.Falar())      // Au au!
    fmt.Println(g.Falar())      // Miau!

    // Atenção: Apresentar() chama Animal.Falar(), não Cachorro.Falar()
    c.Apresentar()  // Rex diz: ...  ← não chama o método sobrescrito
    g.Apresentar()  // Whiskers diz: ...
}

Esse exemplo revela uma diferença importante entre embedding e herança real: métodos promovidos não são virtuais. Apresentar() pertence a Animal e chama Animal.Falar() — não o Falar() do tipo externo. Esse comportamento é previsível e explícito, mas surpreende quem espera polimorfismo por herança.

Para obter o comportamento polimórfico desejado, a solução é usar interfaces:

type Falante interface {
    Falar() string
    Apresentar()
}

// Cada tipo implementa Apresentar() diretamente
func (c Cachorro) Apresentar() {
    fmt.Printf("%s diz: %s\n", c.Nome, c.Falar())
}

func (g Gato) Apresentar() {
    fmt.Printf("%s diz: %s\n", g.Nome, g.Falar())
}

func apresentarTodos(animais []Falante) {
    for _, a := range animais {
        a.Apresentar()
    }
}

func main() {
    animais := []Falante{
        Cachorro{Animal: Animal{Nome: "Rex"}},
        Gato{Animal: Animal{Nome: "Whiskers"}},
    }
    apresentarTodos(animais)
    // Rex diz: Au au!
    // Whiskers diz: Miau!
}

O padrão funcional: injeção de comportamento

Outra forma de composição em Go é injetar comportamento através de funções ou interfaces como campos:

package main

import "fmt"

type Validador func(string) error

type FormularioCadastro struct {
    validarEmail Validador
    validarSenha Validador
}

func novoFormulario(validarEmail, validarSenha Validador) *FormularioCadastro {
    return &FormularioCadastro{
        validarEmail: validarEmail,
        validarSenha: validarSenha,
    }
}

func (f *FormularioCadastro) Submeter(email, senha string) error {
    if err := f.validarEmail(email); err != nil {
        return fmt.Errorf("email inválido: %w", err)
    }
    if err := f.validarSenha(senha); err != nil {
        return fmt.Errorf("senha inválida: %w", err)
    }
    fmt.Println("Cadastro realizado com sucesso!")
    return nil
}

func main() {
    import_strings := func(s string) error {
        if len(s) == 0 {
            return fmt.Errorf("não pode ser vazio")
        }
        return nil
    }

    validarSenhaForte := func(s string) error {
        if len(s) < 8 {
            return fmt.Errorf("mínimo de 8 caracteres")
        }
        return nil
    }

    form := novoFormulario(import_strings, validarSenhaForte)

    form.Submeter("user@exemplo.com", "senhaforte123")
    form.Submeter("", "123")
}

Comparativo: herança vs composição

Aspecto Herança Composição em Go
Acoplamento Alto — subclasse depende da base Baixo — tipos independentes
Flexibilidade Rígida — hierarquia definida cedo Flexível — combinações livres
Polimorfismo Por hierarquia de tipos Por interfaces implícitas
Reutilização Vertical — ao longo da hierarquia Horizontal — qualquer combinação
Testabilidade Difícil — dependências ocultas Fácil — injeção de dependência natural
Evolução Mudanças na base afetam tudo Tipos evoluem independentemente

Resumo do que foi coberto

Este artigo apresentou a filosofia de composição do Go como substituta da herança: embedding para reutilização de código, interfaces para polimorfismo, delegação explícita para o padrão Decorator, a diferença entre métodos promovidos e métodos virtuais, e injeção de comportamento via funções. Esses padrões juntos formam o núcleo do design idiomático em Go.


Referências e leituras complementares


Comentários

Mais em Golang

Pacotes: visibilidade, nomenclatura e organização de projeto
Pacotes: visibilidade, nomenclatura e organização de projeto

Em Go, todo arquivo fonte pertence a um pacote. Não existe código solto fora...

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se vari&aacute;veis s&atilde;o os substantivos de um programa, operadores s&a...

Channels: comunicação entre goroutines
Channels: comunicação entre goroutines

No artigo anterior, goroutines foram apresentadas como unidades de execução c...