Padrões de projeto em Go
LidoEsta aula explora padrões de projeto em Go, apresentando os padrões Functional Options, Strategy e Factory, com exemplos práticos e discussão sobre a abordagem idiomática versus a clássica. O objetivo é mostrar como aplicar esses padrões de forma eficiente no contexto da linguagem Go.
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
- Effective Go
- Go by Example
- Design Patterns in Go - Refactoring Guru
- Go Patterns - GitHub
- Documentação oficial da linguagem Go
- Functional Options Pattern - Medium
Exercícios
- Implemente uma estrutura
Configcom campos comoHost,PorteTimeout, e use o padrão Functional Options para permitir configuração opcional. Crie um construtorNewConfigque aplica as opções. - Crie um sistema de pagamento com o padrão Strategy. Defina uma interface
PaymentStrategycom o métodoPay(amount float64)e implemente duas estratégias:CreditCardePayPal. Em seguida, crie uma função que aceita uma estratégia e um valor, e executa o pagamento. - 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. - Refatore o código a seguir para usar o padrão Functional Options em vez de passar muitos parâmetros. A função
NewClientdeve aceitar opções para URL, token e timeout. - 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.
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
}
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)
}
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")
}
}
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
}
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)