Padrões de projeto são soluções reutilizáveis para problemas comuns em desenvolvimento de software. Em Go, muitos padrões clássicos (como os do livro da Gang of Four) podem ser adaptados ou simplificados devido às características únicas da linguagem, como interfaces implícitas, funções como valores de primeira classe e composição. Nesta aula, vamos explorar três padrões importantes: Functional Options, Strategy e Factory, e discutir como escrever código idiomático em Go, priorizando simplicidade e clareza.

Vamos começar com o padrão Functional Options, que é uma técnica muito utilizada em Go para configurar objetos de forma flexível e legível. Em seguida, veremos o Strategy, que permite variar algoritmos em tempo de execução, e o Factory, que centraliza a criação de objetos. Por fim, compararemos a aplicação desses padrões com o estilo clássico, destacando as diferenças e boas práticas.

Functional Options

O padrão Functional Options é uma forma elegante de configurar opções em construtores ou funções de inicialização. Em Go, é comum ter estruturas com muitos campos opcionais, e passar todos como parâmetros seria inviável. Com Functional Options, definimos uma função que recebe um ponteiro para a estrutura e modifica seus campos, e depois passamos essas funções como argumentos para o construtor.

Esse padrão é amplamente utilizado em bibliotecas Go populares, como o pacote net/http (em Server) e o driver oficial do MongoDB. Ele oferece uma API limpa, extensível e de fácil leitura, além de manter a compatibilidade retroativa quando novas opções são adicionadas.

package main

import (
    "fmt"
    "time"
)

// Server é uma estrutura que representa um servidor com várias opções.
type Server struct {
    Addr     string
    Port     int
    Timeout  time.Duration
    MaxConns int
    TLS      bool
}

// Option é uma função que modifica um Server.
type Option func(*Server)

// WithPort define a porta do servidor.
func WithPort(p int) Option {
    return func(s *Server) { s.Port = p }
}

// WithTimeout define o timeout do servidor.
func WithTimeout(t time.Duration) Option {
    return func(s *Server) { s.Timeout = t }
}

// WithMaxConns define o número máximo de conexões.
func WithMaxConns(mc int) Option {
    return func(s *Server) { s.MaxConns = mc }
}

// WithTLS habilita TLS.
func WithTLS() Option {
    return func(s *Server) { s.TLS = true }
}

// NewServer cria um novo Server com as opções fornecidas.
func NewServer(addr string, opts ...Option) *Server {
    // Valores padrão
    s := &Server{
        Addr:     addr,
        Port:     8080,
        Timeout:  30 * time.Second,
        MaxConns: 100,
        TLS:      false,
    }
    // Aplica as opções
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func main() {
    s := NewServer("localhost", WithPort(9090), WithTimeout(10*time.Second), WithTLS())
    fmt.Printf("%+v\n", s)
}

Uma das vantagens desse padrão é que ele permite adicionar novas opções sem quebrar o código existente, pois os parâmetros são variádicos. Além disso, a leitura do código fica muito mais clara: NewServer("localhost", WithPort(9090), WithTLS()) é autoexplicativo.

Strategy

O padrão Strategy permite definir uma família de algoritmos, encapsulá-los e torná-los intercambiáveis. Em Go, isso é naturalmente implementado com interfaces. A interface define um contrato, e diferentes implementações podem ser usadas em tempo de execução.

Um exemplo comum é um sistema de cálculo de frete, onde diferentes transportadoras têm diferentes estratégias de cálculo. Com interfaces, podemos trocar a estratégia sem modificar o código que a utiliza.

package main

import "fmt"

// CalculadoraFrete define a interface para estratégias de cálculo de frete.
type CalculadoraFrete interface {
    Calcular(peso float64) float64
}

// FreteComum é uma estratégia de frete fixo por peso.
type FreteComum struct{}

func (f FreteComum) Calcular(peso float64) float64 {
    return peso * 2.0
}

// FreteExpresso é uma estratégia de frete com taxa fixa mais adicional.
type FreteExpresso struct{}

func (f FreteExpresso) Calcular(peso float64) float64 {
    return 20.0 + peso*3.0
}

// FreteInternacional é uma estratégia de frete para encomendas internacionais.
type FreteInternacional struct{}

func (f FreteInternacional) Calcular(peso float64) float64 {
    return 50.0 + peso*5.0
}

// Pedido representa um pedido com peso e uma estratégia de frete.
type Pedido struct {
    Peso      float64
    Estrategia CalculadoraFrete
}

// CalcularFrete usa a estratégia definida para calcular o frete.
func (p Pedido) CalcularFrete() float64 {
    return p.Estrategia.Calcular(p.Peso)
}

func main() {
    pedido := Pedido{Peso: 10, Estrategia: FreteComum{}}
    fmt.Printf("Frete comum: R$ %.2f\n", pedido.CalcularFrete())

    pedido.Estrategia = FreteExpresso{}
    fmt.Printf("Frete expresso: R$ %.2f\n", pedido.CalcularFrete())

    pedido.Estrategia = FreteInternacional{}
    fmt.Printf("Frete internacional: R$ %.2f\n", pedido.CalcularFrete())
}

O padrão Strategy em Go é particularmente simples porque não precisamos de hierarquias de classes como em linguagens orientadas a objetos; basta implementar a interface. Isso promove a composição e a flexibilidade.

Factory

O padrão Factory é usado para encapsular a criação de objetos, permitindo que o código cliente não conheça a lógica de instanciação. Em Go, podemos implementar funções construtoras que retornam interfaces, dando flexibilidade para retornar diferentes tipos concretos.

Um exemplo clássico é uma fábrica de formas geométricas. Dependendo do tipo solicitado, retornamos um círculo ou um quadrado, ambos implementando a interface Forma.

package main

import (
    "fmt"
    "math"
)

// Forma define o contrato para formas geométricas.
type Forma interface {
    Area() float64
}

// Circulo é uma forma concreta.
type Circulo struct {
    Raio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

// Quadrado é outra forma concreta.
type Quadrado struct {
    Lado float64
}

func (q Quadrado) Area() float64 {
    return q.Lado * q.Lado
}

// FactoryForma cria uma Forma baseada no tipo e nos parâmetros.
func FactoryForma(tipo string, param float64) (Forma, error) {
    switch tipo {
    case "circulo":
        return Circulo{Raio: param}, nil
    case "quadrado":
        return Quadrado{Lado: param}, nil
    default:
        return nil, fmt.Errorf("tipo de forma desconhecido: %s", tipo)
    }
}

func main() {
    forma1, err := FactoryForma("circulo", 5.0)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Área do círculo: %.2f\n", forma1.Area())

    forma2, _ := FactoryForma("quadrado", 4.0)
    fmt.Printf("Área do quadrado: %.2f\n", forma2.Area())
}

O Factory é útil quando a criação do objeto envolve lógica complexa, como leitura de configurações ou seleção de implementações com base em condições. Em Go, muitas vezes preferimos funções construtoras simples (como NewServer) a um padrão Factory formal, a menos que haja uma hierarquia de tipos.

Idiomático vs clássico

Go tem sua própria filosofia de design, que muitas vezes difere dos padrões clássicos de outras linguagens. O estilo idiomático em Go prioriza simplicidade, legibilidade e composição, evitando complexidade desnecessária. Por exemplo, em vez de usar herança e polimorfismo extensivo, Go favorece interfaces pequenas e funções de primeira classe.

Padrões como Strategy e Factory podem ser implementados de forma mais direta em Go, sem a necessidade de classes abstratas ou fábricas complexas. O padrão Functional Options é um exemplo de um padrão que surgiu especificamente para resolver problemas de configuração em Go, e não tem equivalente direto em linguagens clássicas.

É importante avaliar se um padrão clássico faz sentido em Go. Muitas vezes, uma solução mais simples e direta é preferível. Por exemplo, em vez de usar um Factory para criar objetos, podemos usar uma simples função construtora. Em vez de usar um Strategy com interfaces e structs, podemos passar uma função como parâmetro. O código idiomático Go tende a ser mais enxuto e fácil de manter.

// Exemplo de Strategy usando funções (mais idiomático)
type Operacao func(int, int) int

func Soma(a, b int) int { return a + b }
func Subtracao(a, b int) int { return a - b }

func Executar(a, b int, op Operacao) int {
    return op(a, b)
}

func main() {
    fmt.Println(Executar(5, 3, Soma))      // 8
    fmt.Println(Executar(5, 3, Subtracao)) // 2
}

Ao decidir entre um padrão clássico e uma abordagem idiomática, pergunte-se: "Qual é a solução mais simples que resolve o problema?" Em Go, menos é mais.

Boas práticas

Ao aplicar padrões de projeto em Go, considere as seguintes recomendações:

  • Prefira interfaces pequenas (com um ou dois métodos) para maximizar a reutilização.
  • Use o padrão Functional Options para APIs que podem crescer em configurações.
  • Evite criar hierarquias complexas; use composição em vez de herança.
  • Quando um padrão clássico parecer excessivo, simplifique usando funções como parâmetros.
  • Documente bem as opções e estratégias com comentários claros.

Referências

Exercícios

  1. Implemente uma estrutura Config com campos como Host, Port e Timeout, e use o padrão Functional Options para permitir configuração opcional. Crie um construtor NewConfig que aplica as opções.
  2. ✓ Resposta:
    type Config struct {
        Host    string
        Port    int
        Timeout time.Duration
    }
    
    type Option func(*Config)
    
    func WithHost(h string) Option { return func(c *Config) { c.Host = h } }
    func WithPort(p int) Option { return func(c *Config) { c.Port = p } }
    func WithTimeout(t time.Duration) Option { return func(c *Config) { c.Timeout = t } }
    
    func NewConfig(opts ...Option) *Config {
        c := &Config{Host: "localhost", Port: 8080, Timeout: 30 * time.Second}
        for _, opt := range opts { opt(c) }
        return c
    }
  3. Crie um sistema de pagamento com o padrão Strategy. Defina uma interface PaymentStrategy com o método Pay(amount float64) e implemente duas estratégias: CreditCard e PayPal. Em seguida, crie uma função que aceita uma estratégia e um valor, e executa o pagamento.
  4. ✓ Resposta:
    type PaymentStrategy interface {
        Pay(amount float64) string
    }
    
    type CreditCard struct{}
    func (c CreditCard) Pay(amount float64) string {
        return fmt.Sprintf("Paid %.2f using Credit Card", amount)
    }
    
    type PayPal struct{}
    func (p PayPal) Pay(amount float64) string {
        return fmt.Sprintf("Paid %.2f using PayPal", amount)
    }
    
    func ProcessPayment(strategy PaymentStrategy, amount float64) string {
        return strategy.Pay(amount)
    }
  5. Implemente uma fábrica (Factory) que cria diferentes tipos de animais (Cachorro, Gato) a partir de um nome. Cada animal deve ter um método Som() que retorna o som característico.
  6. ✓ Resposta:
    type Animal interface {
        Som() string
    }
    
    type Cachorro struct{}
    func (c Cachorro) Som() string { return "Au au" }
    
    type Gato struct{}
    func (g Gato) Som() string { return "Miau" }
    
    func NewAnimal(tipo string) (Animal, error) {
        switch tipo {
        case "cachorro": return Cachorro{}, nil
        case "gato": return Gato{}, nil
        default: return nil, fmt.Errorf("animal desconhecido")
        }
    }
  7. Refatore o código a seguir para usar o padrão Functional Options em vez de passar muitos parâmetros. A função NewClient deve aceitar opções para URL, token e timeout.
  8. ✓ Resposta:
    type Client struct {
        URL     string
        Token   string
        Timeout time.Duration
    }
    
    type ClientOption func(*Client)
    
    func WithURL(url string) ClientOption { return func(c *Client) { c.URL = url } }
    func WithToken(token string) ClientOption { return func(c *Client) { c.Token = token } }
    func WithTimeout(t time.Duration) ClientOption { return func(c *Client) { c.Timeout = t } }
    
    func NewClient(opts ...ClientOption) *Client {
        c := &Client{URL: "http://default.com", Timeout: 10 * time.Second}
        for _, opt := range opts { opt(c) }
        return c
    }
  9. Explique por que o padrão Strategy em Go pode ser implementado de forma mais simples usando funções, e dê um exemplo de código que demonstre isso.
  10. ✓ Resposta: Em Go, funções são valores de primeira classe, então podemos passar uma função diretamente como parâmetro, evitando a necessidade de uma interface e structs separados. Exemplo:
    type Operacao func(int, int) int
    
    func Soma(a, b int) int { return a + b }
    func Sub(a, b int) int { return a - b }
    
    func Executar(a, b int, op Operacao) int { return op(a, b) }
    
    // uso: Executar(5, 3, Soma)