Golang

Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy Já leu

14 min de leitura

Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy
Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e

Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e Padrões


Patterns em Go: idiomáticos, não traduzidos

Design Patterns do livro Gang of Four foram concebidos para linguagens orientadas a objetos com herança. Aplicar esses padrões literalmente em Go — tentando recriar classes abstratas com interfaces e herança com embedding — produz código artificialmente complexo.

A abordagem correta é diferente: entender o problema que cada padrão resolve e implementá-lo da forma mais natural em Go. Muitos padrões clássicos se tornam simplesmente "usar interfaces" ou "passar funções como parâmetros". Outros se traduzem naturalmente. Este artigo foca nos padrões mais relevantes para o dia a dia de um desenvolvedor Go.


Repository Pattern

O padrão Repository abstrai o acesso a dados por trás de uma interface, permitindo que a lógica de negócio não conheça detalhes de persistência.

// Definição da interface — no pacote de domínio ou casos de uso
type RepositorioProduto interface {
    Criar(ctx context.Context, p *Produto) error
    BuscarPorID(ctx context.Context, id int64) (*Produto, error)
    BuscarPorSlug(ctx context.Context, slug string) (*Produto, error)
    Listar(ctx context.Context, filtro FiltroProduto) ([]*Produto, error)
    Atualizar(ctx context.Context, p *Produto) error
    Deletar(ctx context.Context, id int64) error
    Contar(ctx context.Context, filtro FiltroProduto) (int64, error)
}

// Implementação PostgreSQL
type repoProdutoPostgres struct {
    db *pgxpool.Pool
}

func NovoRepoProdutoPostgres(db *pgxpool.Pool) RepositorioProduto {
    return &repoProdutoPostgres{db: db}
}

func (r *repoProdutoPostgres) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
    query := `
        SELECT id, nome, slug, descricao, preco, estoque, ativo, criado_em
        FROM produtos
        WHERE id = $1 AND deletado_em IS NULL
    `
    rows, err := r.db.Query(ctx, query, id)
    if err != nil {
        return nil, fmt.Errorf("buscar produto: %w", err)
    }

    produto, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Produto])
    if errors.Is(err, pgx.ErrNoRows) {
        return nil, ErrProdutoNaoEncontrado
    }
    return produto, err
}

func (r *repoProdutoPostgres) Listar(ctx context.Context, f FiltroProduto) ([]*Produto, error) {
    query := `
        SELECT id, nome, slug, descricao, preco, estoque, ativo, criado_em
        FROM produtos
        WHERE deletado_em IS NULL
          AND ($1::bigint IS NULL OR categoria_id = $1)
          AND ($2::boolean IS NULL OR ativo = $2)
          AND ($3::text IS NULL OR nome ILIKE '%' || $3 || '%')
        ORDER BY nome
        LIMIT $4 OFFSET $5
    `
    rows, err := r.db.Query(ctx, query,
        f.CategoriaID, f.Ativo, f.Busca,
        f.Limite, f.Offset,
    )
    if err != nil {
        return nil, fmt.Errorf("listar produtos: %w", err)
    }
    return pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Produto])
}

// Implementação em memória — para testes
type repoProdutoMemoria struct {
    mu      sync.RWMutex
    dados   map[int64]*Produto
    proximo int64
}

func NovoRepoProdutoMemoria() RepositorioProduto {
    return &repoProdutoMemoria{
        dados:   make(map[int64]*Produto),
        proximo: 1,
    }
}

func (r *repoProdutoMemoria) Criar(_ context.Context, p *Produto) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    p.ID = r.proximo
    copia := *p
    r.dados[r.proximo] = &copia
    r.proximo++
    return nil
}

func (r *repoProdutoMemoria) BuscarPorID(_ context.Context, id int64) (*Produto, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    p, existe := r.dados[id]
    if !existe {
        return nil, ErrProdutoNaoEncontrado
    }
    copia := *p
    return &copia, nil
}

// ... demais métodos

O repositório em memória é usado em testes de integração — sem banco de dados, com velocidade máxima e comportamento idêntico ao repositório real.


Factory Pattern

Factory encapsula a criação de objetos complexos, centralizando a lógica de construção e escondendo detalhes de implementação.

// Factory simples: função que retorna uma interface
type Notificador interface {
    Enviar(ctx context.Context, dest, assunto, corpo string) error
    Nome() string
}

// Implementações concretas
type notificadorEmail struct {
    host     string
    porta    int
    usuario  string
    senha    string
}

func (n *notificadorEmail) Nome() string { return "email" }
func (n *notificadorEmail) Enviar(ctx context.Context, dest, assunto, corpo string) error {
    fmt.Printf("[EMAIL] Para: %s | Assunto: %s\n", dest, assunto)
    return nil
}

type notificadorSMS struct {
    apiKey string
    origem string
}

func (n *notificadorSMS) Nome() string { return "sms" }
func (n *notificadorSMS) Enviar(ctx context.Context, dest, _, corpo string) error {
    fmt.Printf("[SMS] Para: %s | Mensagem: %s\n", dest, corpo)
    return nil
}

type notificadorPush struct {
    serverKey string
}

func (n *notificadorPush) Nome() string { return "push" }
func (n *notificadorPush) Enviar(ctx context.Context, dest, assunto, corpo string) error {
    fmt.Printf("[PUSH] Token: %s | %s: %s\n", dest, assunto, corpo)
    return nil
}

// Factory: cria o notificador correto baseado na configuração
type ConfigNotificador struct {
    Tipo     string
    Host     string
    Porta    int
    APIKey   string
    Usuario  string
    Senha    string
}

func CriarNotificador(cfg ConfigNotificador) (Notificador, error) {
    switch strings.ToLower(cfg.Tipo) {
    case "email":
        if cfg.Host == "" {
            return nil, errors.New("host SMTP obrigatório para email")
        }
        return &notificadorEmail{
            host:    cfg.Host,
            porta:   cfg.Porta,
            usuario: cfg.Usuario,
            senha:   cfg.Senha,
        }, nil

    case "sms":
        if cfg.APIKey == "" {
            return nil, errors.New("API key obrigatória para SMS")
        }
        return &notificadorSMS{apiKey: cfg.APIKey}, nil

    case "push":
        if cfg.APIKey == "" {
            return nil, errors.New("server key obrigatória para push")
        }
        return &notificadorPush{serverKey: cfg.APIKey}, nil

    default:
        return nil, fmt.Errorf("tipo de notificador desconhecido: %s", cfg.Tipo)
    }
}

// Factory com registro dinâmico — extensível sem modificar o factory
type FactoryNotificador struct {
    construtores map[string]func(ConfigNotificador) (Notificador, error)
}

func NovoFactoryNotificador() *FactoryNotificador {
    f := &FactoryNotificador{
        construtores: make(map[string]func(ConfigNotificador) (Notificador, error)),
    }

    // Registra tipos padrão
    f.Registrar("email", func(cfg ConfigNotificador) (Notificador, error) {
        return &notificadorEmail{host: cfg.Host, porta: cfg.Porta}, nil
    })
    f.Registrar("sms", func(cfg ConfigNotificador) (Notificador, error) {
        return &notificadorSMS{apiKey: cfg.APIKey}, nil
    })

    return f
}

func (f *FactoryNotificador) Registrar(tipo string, construtor func(ConfigNotificador) (Notificador, error)) {
    f.construtores[strings.ToLower(tipo)] = construtor
}

func (f *FactoryNotificador) Criar(cfg ConfigNotificador) (Notificador, error) {
    construtor, existe := f.construtores[strings.ToLower(cfg.Tipo)]
    if !existe {
        return nil, fmt.Errorf("tipo '%s' não registrado", cfg.Tipo)
    }
    return construtor(cfg)
}

Strategy Pattern

Strategy define uma família de algoritmos intercambiáveis. Em Go, isso é simplesmente passar funções ou interfaces como parâmetros.

// Estratégia como tipo de função — a forma mais Go
type EstrategiaOrdenacao func(itens []Produto) []Produto

func ordenarPorPrecoAsc(itens []Produto) []Produto {
    copia := make([]Produto, len(itens))
    copy(copia, itens)
    sort.Slice(copia, func(i, j int) bool {
        return copia[i].Preco < copia[j].Preco
    })
    return copia
}

func ordenarPorPrecoDesc(itens []Produto) []Produto {
    copia := make([]Produto, len(itens))
    copy(copia, itens)
    sort.Slice(copia, func(i, j int) bool {
        return copia[i].Preco > copia[j].Preco
    })
    return copia
}

func ordenarPorNome(itens []Produto) []Produto {
    copia := make([]Produto, len(itens))
    copy(copia, itens)
    sort.Slice(copia, func(i, j int) bool {
        return copia[i].Nome < copia[j].Nome
    })
    return copia
}

func ordenarPorRelevancia(query string) EstrategiaOrdenacao {
    return func(itens []Produto) []Produto {
        // ordenação por relevância com base na query
        copia := make([]Produto, len(itens))
        copy(copia, itens)
        sort.Slice(copia, func(i, j int) bool {
            ri := strings.Contains(strings.ToLower(copia[i].Nome), strings.ToLower(query))
            rj := strings.Contains(strings.ToLower(copia[j].Nome), strings.ToLower(query))
            return ri && !rj
        })
        return copia
    }
}

// Serviço que usa a estratégia
type ServicoCatalogo struct {
    repo RepositorioProduto
}

func (s *ServicoCatalogo) Listar(
    ctx context.Context,
    filtro FiltroProduto,
    ordenar EstrategiaOrdenacao,
) ([]*Produto, error) {
    produtos, err := s.repo.Listar(ctx, filtro)
    if err != nil {
        return nil, err
    }

    if ordenar == nil {
        return produtos, nil
    }

    // Converte []*Produto para []Produto, ordena, converte de volta
    itens := make([]Produto, len(produtos))
    for i, p := range produtos {
        itens[i] = *p
    }

    ordenados := ordenar(itens)

    resultado := make([]*Produto, len(ordenados))
    for i := range ordenados {
        copia := ordenados[i]
        resultado[i] = &copia
    }

    return resultado, nil
}

// Strategy como interface — quando o estado é necessário
type EstrategiaDesconto interface {
    Calcular(preco float64, cliente *Cliente) float64
    Descricao() string
}

type descontoPercentual struct {
    percentual float64
}

func NovoDescontoPercentual(p float64) EstrategiaDesconto {
    return &descontoPercentual{percentual: p}
}

func (d *descontoPercentual) Calcular(preco float64, _ *Cliente) float64 {
    return preco * (1 - d.percentual/100)
}

func (d *descontoPercentual) Descricao() string {
    return fmt.Sprintf("%.0f%% de desconto", d.percentual)
}

type descontoVIP struct {
    percentualBase    float64
    percentualBonus   float64
    minimoCompras     int
}

func (d *descontoVIP) Calcular(preco float64, cliente *Cliente) float64 {
    if cliente.TotalCompras() >= d.minimoCompras {
        return preco * (1 - (d.percentualBase+d.percentualBonus)/100)
    }
    return preco * (1 - d.percentualBase/100)
}

func (d *descontoVIP) Descricao() string {
    return fmt.Sprintf("Desconto VIP: %.0f%% base + %.0f%% bonus",
        d.percentualBase, d.percentualBonus)
}

type semDesconto struct{}

func (s *semDesconto) Calcular(preco float64, _ *Cliente) float64 { return preco }
func (s *semDesconto) Descricao() string                           { return "sem desconto" }

// Context que usa a estratégia
type CalculadorPreco struct {
    estrategia EstrategiaDesconto
}

func NovoCalculadorPreco(e EstrategiaDesconto) *CalculadorPreco {
    if e == nil {
        e = &semDesconto{}
    }
    return &CalculadorPreco{estrategia: e}
}

func (c *CalculadorPreco) AlterarEstrategia(e EstrategiaDesconto) {
    c.estrategia = e
}

func (c *CalculadorPreco) Calcular(preco float64, cliente *Cliente) (float64, string) {
    final := c.estrategia.Calcular(preco, cliente)
    return final, c.estrategia.Descricao()
}

Observer Pattern

Observer notifica múltiplos componentes quando um evento ocorre, sem que o emissor conheça os receptores.

// Observer via canais — idiomático em Go
type EventoPedido struct {
    Tipo      string
    PedidoID  int64
    ClienteID int64
    Total     float64
    OcorridoEm time.Time
}

type ObservadorPedido interface {
    Processar(ctx context.Context, evento EventoPedido) error
    Nome() string
}

type BusEventosPedido struct {
    mu          sync.RWMutex
    observadores []ObservadorPedido
}

func NovoBusEventosPedido() *BusEventosPedido {
    return &BusEventosPedido{}
}

func (b *BusEventosPedido) Registrar(obs ObservadorPedido) {
    b.mu.Lock()
    defer b.mu.Unlock()
    b.observadores = append(b.observadores, obs)
}

func (b *BusEventosPedido) Publicar(ctx context.Context, evento EventoPedido) {
    b.mu.RLock()
    obs := make([]ObservadorPedido, len(b.observadores))
    copy(obs, b.observadores)
    b.mu.RUnlock()

    for _, o := range obs {
        o := o
        go func() {
            if err := o.Processar(ctx, evento); err != nil {
                slog.Error("observador falhou",
                    "observador", o.Nome(),
                    "evento", evento.Tipo,
                    "err", err,
                )
            }
        }()
    }
}

// Observadores concretos
type observadorEmail struct{ mailer Mailer }

func (o *observadorEmail) Nome() string { return "email" }
func (o *observadorEmail) Processar(ctx context.Context, e EventoPedido) error {
    if e.Tipo != "pedido.confirmado" {
        return nil
    }
    return o.mailer.EnviarConfirmacaoPedido(ctx, e.ClienteID, e.PedidoID, e.Total)
}

type observadorEstoque struct{ repo RepositorioProduto }

func (o *observadorEstoque) Nome() string { return "estoque" }
func (o *observadorEstoque) Processar(ctx context.Context, e EventoPedido) error {
    if e.Tipo != "pedido.cancelado" {
        return nil
    }
    return o.repo.ReporEstoque(ctx, e.PedidoID)
}

type observadorAnalytics struct{ cliente ClienteAnalytics }

func (o *observadorAnalytics) Nome() string { return "analytics" }
func (o *observadorAnalytics) Processar(ctx context.Context, e EventoPedido) error {
    return o.cliente.RegistrarEvento(ctx, e.Tipo, map[string]any{
        "pedido_id": e.PedidoID,
        "total":     e.Total,
    })
}

Decorator Pattern

Decorator adiciona comportamento a um objeto sem modificar sua estrutura. Em Go, é implementado criando uma struct que envolve outra struct implementando a mesma interface.

// Decorator de cache sobre um repositório
type repoProdutoComCache struct {
    repo  RepositorioProduto
    cache Cache
    ttl   time.Duration
}

func NovoRepoProdutoComCache(repo RepositorioProduto, cache Cache, ttl time.Duration) RepositorioProduto {
    return &repoProdutoComCache{repo: repo, cache: cache, ttl: ttl}
}

func (r *repoProdutoComCache) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
    chave := fmt.Sprintf("produto:%d", id)

    // Tenta o cache primeiro
    if val, existe := r.cache.Get(ctx, chave); existe {
        var p Produto
        if err := json.Unmarshal([]byte(val), &p); err == nil {
            return &p, nil
        }
    }

    // Cache miss — busca no repositório
    produto, err := r.repo.BuscarPorID(ctx, id)
    if err != nil {
        return nil, err
    }

    // Armazena no cache
    if dados, err := json.Marshal(produto); err == nil {
        r.cache.Set(ctx, chave, string(dados), r.ttl)
    }

    return produto, nil
}

func (r *repoProdutoComCache) Atualizar(ctx context.Context, p *Produto) error {
    if err := r.repo.Atualizar(ctx, p); err != nil {
        return err
    }
    // Invalida o cache após atualização
    r.cache.Delete(ctx, fmt.Sprintf("produto:%d", p.ID))
    return nil
}

// Delega os demais métodos diretamente
func (r *repoProdutoComCache) Criar(ctx context.Context, p *Produto) error {
    return r.repo.Criar(ctx, p)
}
func (r *repoProdutoComCache) BuscarPorSlug(ctx context.Context, slug string) (*Produto, error) {
    return r.repo.BuscarPorSlug(ctx, slug)
}
func (r *repoProdutoComCache) Listar(ctx context.Context, f FiltroProduto) ([]*Produto, error) {
    return r.repo.Listar(ctx, f)
}
func (r *repoProdutoComCache) Deletar(ctx context.Context, id int64) error {
    if err := r.repo.Deletar(ctx, id); err != nil {
        return err
    }
    r.cache.Delete(ctx, fmt.Sprintf("produto:%d", id))
    return nil
}
func (r *repoProdutoComCache) Contar(ctx context.Context, f FiltroProduto) (int64, error) {
    return r.repo.Contar(ctx, f)
}

// Decorator de logging
type repoProdutoComLog struct {
    repo   RepositorioProduto
    logger *slog.Logger
}

func NovoRepoProdutoComLog(repo RepositorioProduto, logger *slog.Logger) RepositorioProduto {
    return &repoProdutoComLog{repo: repo, logger: logger}
}

func (r *repoProdutoComLog) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
    inicio := time.Now()
    produto, err := r.repo.BuscarPorID(ctx, id)

    r.logger.Debug("repo.BuscarPorID",
        "id", id,
        "duração_ms", time.Since(inicio).Milliseconds(),
        "encontrado", produto != nil,
        "erro", err,
    )
    return produto, err
}

// Composição de decorators no main
func configurarRepoProduto(
    db *pgxpool.Pool,
    cache Cache,
    logger *slog.Logger,
) RepositorioProduto {
    var repo RepositorioProduto
    repo = NovoRepoProdutoPostgres(db)                     // base
    repo = NovoRepoProdutoComCache(repo, cache, time.Hour) // + cache
    repo = NovoRepoProdutoComLog(repo, logger)             // + logging
    return repo
}

Builder Pattern

Builder constrói objetos complexos passo a passo:

type ConsultaSQL struct {
    tabela     string
    condicoes  []string
    args       []any
    ordenacao  []string
    limite     int
    offset     int
    joins      []string
}

type BuilderConsulta struct {
    c ConsultaSQL
}

func NovaConsulta(tabela string) *BuilderConsulta {
    return &BuilderConsulta{c: ConsultaSQL{tabela: tabela}}
}

func (b *BuilderConsulta) Onde(condicao string, args ...any) *BuilderConsulta {
    b.c.condicoes = append(b.c.condicoes, condicao)
    b.c.args = append(b.c.args, args...)
    return b
}

func (b *BuilderConsulta) OrderBy(campo string, desc bool) *BuilderConsulta {
    dir := "ASC"
    if desc {
        dir = "DESC"
    }
    b.c.ordenacao = append(b.c.ordenacao, campo+" "+dir)
    return b
}

func (b *BuilderConsulta) Limite(n int) *BuilderConsulta {
    b.c.limite = n
    return b
}

func (b *BuilderConsulta) Offset(n int) *BuilderConsulta {
    b.c.offset = n
    return b
}

func (b *BuilderConsulta) Join(join string) *BuilderConsulta {
    b.c.joins = append(b.c.joins, join)
    return b
}

func (b *BuilderConsulta) Construir() (string, []any) {
    sql := "SELECT * FROM " + b.c.tabela

    for _, join := range b.c.joins {
        sql += " " + join
    }

    if len(b.c.condicoes) > 0 {
        sql += " WHERE " + strings.Join(b.c.condicoes, " AND ")
    }

    if len(b.c.ordenacao) > 0 {
        sql += " ORDER BY " + strings.Join(b.c.ordenacao, ", ")
    }

    if b.c.limite > 0 {
        sql += fmt.Sprintf(" LIMIT %d", b.c.limite)
    }

    if b.c.offset > 0 {
        sql += fmt.Sprintf(" OFFSET %d", b.c.offset)
    }

    return sql, b.c.args
}

// Uso
sql, args := NovaConsulta("produtos").
    Join("INNER JOIN categorias c ON c.id = produtos.categoria_id").
    Onde("produtos.ativo = $1", true).
    Onde("c.slug = $2", "eletronicos").
    OrderBy("produtos.preco", false).
    Limite(20).
    Offset(40).
    Construir()

Resumo do que foi coberto

Este artigo apresentou Design Patterns idiomáticos em Go: Repository com implementações intercambiáveis para PostgreSQL e memória, Factory com registro dinâmico de tipos, Strategy com funções e interfaces para algoritmos intercambiáveis, Observer com goroutines para notificação assíncrona, Decorator para adicionar comportamento — cache e logging — sem modificar implementações existentes, e Builder para construção passo a passo de objetos complexos. O próximo artigo constrói uma API RESTful completa do zero ao deploy.


Referências e leituras complementares

  • Go Patterns — Catálogo de padrões idiomáticos em Go. https://github.com/tmrts/go-patterns

  • Refactoring Guru — Go — Implementações de todos os padrões GoF em Go. https://refactoring.guru/design-patterns/go

  • Functional Options — Dave Cheney — Artigo seminal sobre o padrão de opções funcionais. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

  • Go Blog: Error handling and Go — Como o padrão de erros em Go se relaciona com os padrões clássicos. https://go.dev/blog/error-handling-and-go

  • Repository Pattern in Go — Artigo prático sobre implementação do padrão Repository. https://threedots.tech/post/repository-pattern-in-go/

  • Practical Go — Real world advice — Dave Cheney sobre padrões práticos em Go. https://dave.cheney.net/practical-go


Próximo artigo: Artigo 48 — APIs RESTful completas: projeto do zero ao deploy**


you asked

Sim


claude response

Comentários

Mais em Golang

Variáveis, tipos primitivos e declaração curta
Variáveis, tipos primitivos e declaração curta

&nbsp; O sistema de tipos do Go &eacute; sua primeira linha de defesa!&nbsp;...

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

O artigo anterior introduziu o select como mecanismo para aguardar múltiplos...

Composição vs herança: o jeito Go de reutilizar código
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...