trilha

Fundamentos de Programação/02 - Código que Presta/Semana 05 - POO de Verdade8 min

Encapsulamento

Perguntas-guia
  • Encapsular é esconder dados ou esconder decisões que podem mudar?
  • Se todo campo privado tem getter e setter público, o que sobrou de encapsulamento?
  • O que é um invariante de classe, e quem é responsável por mantê-lo?
  • Que detalhe do seu código atual vaza para quem chama e não deveria?

Conceito

Encapsulamento não é esconder dados. É esconder decisões que podem mudar.

O objetivo é que trocar uma decisão interna não quebre ninguém de fora. Se o saldo é guardado como float, como centavos em int, ou calculado a partir de um extrato de lançamentos — isso é decisão sua, e ninguém de fora deveria depender dela.

Getter + setter para todo campo não é encapsulamento

É o campo público com cerimônia. Você expôs exatamente a mesma decisão (o campo existe, tem esse tipo, é livremente gravável) com duas chamadas de método no meio.

O teste: se você trocasse a representação interna, esses getters/setters sobreviveriam? Se não, eles são a representação vazando.

O que encapsulamento compra: a capacidade de garantir um invariante.

Um invariante de classe é uma propriedade que sempre vale entre operações: saldo nunca negativo, lista sempre ordenada, total sempre igual à soma dos itens, conexão sempre aberta enquanto o objeto vive.

E aqui está o ponto: se qualquer um pode escrever, ninguém pode garantir. O invariante só é sustentável se todas as escritas passam por código que você controla. Encapsulamento é a condição de possibilidade do invariante — não uma boa prática estética.

Tell, don't ask. Compare:

conta.SetSaldo(conta.GetSaldo() - valor)   // a regra mora FORA
conta.Debita(valor)                        // a regra mora DENTRO

Na primeira, a validação de saldo insuficiente está em quem chama — e vai estar duplicada, inconsistente e esquecida em algum lugar. Na segunda, existe um lugar.

O que "vaza" na prática, em ordem de frequência:

  1. devolver a coleção interna — quem recebe pode mutá-la (4. Mutabilidade)
  2. tipo de implementação no retorno ou no parâmetro*sql.Rows na assinatura amarra você ao banco
  3. ordem de chamadas obrigatória não expressa no tipo (Init() antes de Run())
  4. erro específico da implementação subindo — quem trata precisa saber que existe Redis lá embaixo

Lei de Deméter ("não fale com estranhos"): a.PegaB().PegaC().Fazer() te acopla a três tipos e a duas decisões de estrutura que não são suas. Cada . extra é uma suposição sobre a forma interna de outra coisa.

Em Go

A unidade de encapsulamento é o pacote, não o tipo. Isso é a diferença mais importante em relação a Java, C# ou C++ — e a que mais confunde quem vem de lá.

Java Go
Marcador private, protected, public maiúscula = exportado, minúscula = não
Escopo do "privado" a classe o pacote
private entre dois tipos do mesmo pacote inacessível acessível

Ou seja: dentro do mesmo pacote, tudo é visível. Não existe campo privado por tipo. Consequência prática enorme: desenhar encapsulamento em Go é desenhar pacotes. Se dois tipos não deveriam ver as tripas um do outro, eles precisam estar em pacotes diferentes.

Não existe construtor. A convenção é NewX() *X, e ela é só convenção — nada impede X{...}. Para forçar a passagem pelo construtor, deixe pelo menos um campo obrigatório não exportado: um cliente de outro pacote não consegue preenchê-lo no literal.

"Faça o valor zero útil" é um princípio de projeto de Go, não um detalhe. Quando var b bytes.Buffer e var mu sync.Mutex já funcionam sem inicialização, você não precisa de construtor nem de checagem de "inicializado?". É a forma mais forte de encapsulamento: não há estado inválido possível.

internal/ é encapsulamento imposto pelo compilador, no nível de módulo: um pacote em a/internal/b só pode ser importado por código dentro de a/. É a resposta de Go para "público para o meu projeto, privado para o mundo".

O vazamento nº 1 em Go é devolver slice ou map interno — porque a cópia do header é rasa e o chamador ganha acesso de escrita ao seu array (4. Mutabilidade, semana 1).

Respostas às perguntas-guia

1. Encapsular é esconder dados ou esconder decisões que podem mudar?

Conceito: decisões. Dados são um caso particular — o interessante é esconder escolhas (representação, algoritmo, dependência, formato).

Em Go: e a fronteira onde você esconde é o pacote. Um campo minúsculo não está escondido do arquivo vizinho do mesmo pacote.

2. Se todo campo privado tem getter e setter público, o que sobrou de encapsulamento?

Conceito: nada. Você reproduziu o campo público com mais linhas, e amarrou a API à representação.

Em Go: o idioma é ainda mais direto — se o campo é para ser lido e escrito livremente, exporte o campo (Nome string) e pare de fingir. Getter em Go só se justifica quando há cálculo, validação ou cópia defensiva. E nem se chama GetNome(): a convenção é Nome() para ler e SetNome() para escrever.

3. O que é um invariante de classe, e quem é responsável por mantê-lo?

Conceito: propriedade que sempre vale entre operações. Responsável: o tipo que possui o dado — e ele só consegue se todas as escritas passarem por ele.

Em Go: o tipo e o resto do pacote, porque no pacote todo mundo pode escrever no campo. Se manter o invariante é crítico, o tipo merece um pacote próprio.

4. Que detalhe do seu código atual vaza para quem chama e não deveria?

Conceito: comece pelos retornos e parâmetros públicos e pergunte "isso é conceito do meu domínio ou da minha implementação?".

Em Go, os quatro suspeitos habituais:

  • método devolvendo []T ou map[K]V interno, sem clone
  • *sql.DB, *redis.Client, *http.Client em assinatura pública
  • erro cru de driver subindo em vez de erro de domínio (%w com sentinel próprio)
  • struct com todos os campos exportados e um New() que valida — quem usa o literal pula a validação silenciosamente

Trade-offs

Do conceito:

  • Encapsular compra liberdade de mudar o interior; cobra código de fronteira (validação, conversão, cópia) e uma API a mais para manter.
  • Invariante forte compra confiança e cobra rigidez: toda escrita passa por um funil.
  • Tell-don't-ask concentra a regra e engorda o tipo — levado ao extremo, produz a classe deus (1. Code Smells).

Em Go:

  • Visibilidade por pacote é mais simples (dois níveis, sem protected) e mais grossa: você não consegue esconder algo de um vizinho de pacote. O custo é ter que dividir em mais pacotes do que você dividiria em Java.
  • Valor zero útil elimina construtor e estado inválido; cobra desenhar o tipo para que "tudo zerado" seja um estado legítimo — o que não sempre é possível.
  • Sem construtor obrigatório, o literal é sempre uma porta lateral, a menos que você ponha um campo não exportado.

Exemplo prático

package main

import (
	"errors"
	"fmt"
	"slices"
)

var ErrSaldoInsuficiente = errors.New("saldo insuficiente")

// Campos NÃO exportados: de outro pacote, ninguém constrói pelo literal
// nem escreve direto. O invariante é "saldo >= 0" e "saldo == soma(lancamentos)".
type Conta struct {
	saldo       int // em centavos: decisão interna, invisível de fora
	lancamentos []int
}

func NovaConta(inicial int) (*Conta, error) {
	if inicial < 0 {
		return nil, errors.New("saldo inicial negativo")
	}
	c := &Conta{}
	if inicial > 0 {
		c.registra(inicial)
	}
	return c, nil
}

func (c *Conta) registra(v int) {
	c.saldo += v
	c.lancamentos = append(c.lancamentos, v)
}

// Tell, don't ask: a REGRA mora aqui, não em quem chama.
func (c *Conta) Debita(v int) error {
	if v <= 0 {
		return errors.New("valor deve ser positivo")
	}
	if c.saldo < v {
		return fmt.Errorf("debitar %d: %w (tem %d)", v, ErrSaldoInsuficiente, c.saldo)
	}
	c.registra(-v)
	return nil
}

func (c *Conta) Credita(v int) error {
	if v <= 0 {
		return errors.New("valor deve ser positivo")
	}
	c.registra(v)
	return nil
}

// Sem "Get". E devolve em reais: a unidade interna (centavos) não vaza.
func (c *Conta) Saldo() float64 { return float64(c.saldo) / 100 }

// VAZA: quem recebe pode mutar o histórico interno
func (c *Conta) LancamentosVazando() []int { return c.lancamentos }

// CORRETO: cópia defensiva na fronteira de saída
func (c *Conta) Lancamentos() []int { return slices.Clone(c.lancamentos) }

// O invariante, verificável
func (c *Conta) invarianteOK() bool {
	soma := 0
	for _, v := range c.lancamentos {
		soma += v
	}
	return c.saldo >= 0 && c.saldo == soma
}

func main() {
	c, _ := NovaConta(10000) // R$ 100,00
	fmt.Printf("saldo: R$ %.2f\n", c.Saldo())

	if err := c.Debita(3050); err != nil {
		fmt.Println("erro:", err)
	}
	fmt.Printf("após débito: R$ %.2f\n", c.Saldo())

	// a regra é impossível de burlar de fora
	err := c.Debita(999999)
	fmt.Println("débito grande:", err)
	fmt.Println("é ErrSaldoInsuficiente?", errors.Is(err, ErrSaldoInsuficiente))
	fmt.Println("invariante intacto?", c.invarianteOK())

	// o vazamento, e a correção
	c.LancamentosVazando()[0] = 999999
	fmt.Println("depois de mutar o retorno vazado, invariante:", c.invarianteOK())

	c2, _ := NovaConta(10000)
	c2.Lancamentos()[0] = 999999
	fmt.Println("depois de mutar a cópia defensiva, invariante:", c2.invarianteOK())

	// DENTRO DO MESMO PACOTE, o campo minúsculo é acessível.
	// Isto NÃO compilaria em Java; em Go compila, porque a fronteira é o PACOTE.
	c2.saldo = -1
	fmt.Println("após escrita direta no mesmo pacote, invariante:", c2.invarianteOK())
}

A última parte é o que surpreende quem vem de Java: c2.saldo = -1 compila, porque estamos no mesmo pacote. Em Go, encapsulamento entre esses dois códigos só existiria se Conta morasse em outro pacote.

Relacionado


Parte de Semana 05 - POO de Verdade · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas