Fundamentos de Programação/02 - Código que Presta/Semana 05 - POO de Verdade8 min
Encapsulamento
- 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.
É 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:
- devolver a coleção interna — quem recebe pode mutá-la (4. Mutabilidade)
- tipo de implementação no retorno ou no parâmetro —
*sql.Rowsna assinatura amarra você ao banco - ordem de chamadas obrigatória não expressa no tipo (
Init()antes deRun()) - 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
[]Toumap[K]Vinterno, sem clone *sql.DB,*redis.Client,*http.Clientem assinatura pública- erro cru de driver subindo em vez de erro de domínio (
%wcom 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
- 2. Abstração — esconder decisão é o que permite simplificar o contrato
- 3. Interfaces — o contrato que fica quando você esconde o resto
- 4. Mutabilidade — semana 1, o retorno de slice interno é o vazamento nº 1
- 2. Single Responsibility Principle — semana 6, um invariante por tipo
Parte de Semana 05 - POO de Verdade · 00 - MOC Fundamentos de Programação