Options Pattern

· 1159 words · 6 minute read

Sumário 🔗

  1. Valores pré-definidos
  2. Parâmetros de Configuração
  3. Options Pattern - Declaração
  4. Options Pattern - Implementação
  5. Options Pattern - Controle de Erros
  6. Na Prática
  7. Testes Unitários

O Options Pattern é um padrão de design utilizado para configurar objetos de forma flexível, muito útil quando uma função ou struct precisa de muitos parâmetros opcionais. Dessa forma evitamos o uso de múltiplas funções construtoras ou o excesso de parâmetros em funções, o que torna o código mais limpo e extensível.

Ao longo deste post vamos utilizar como exemplo um cliente http.

Valores pré-definidos 🔗

A forma mais simples de se criar uma instância de uma struct é utilizando valores pré-definidos ou constantes. Dessa forma, você perde a flexibilidade de modificar os valores em tempo de inicialização, como por exemplo diferentes configurações por ambiente.

 1type Client struct {
 2    baseURL string
 3    client *http.Client
 4}
 5
 6func NewClient(baseURL) *Client {
 7    return &Client{
 8        baseURL: baseURL,
 9        client: &http.Client{
10            Timeout: time.Duration(10) * time.Second,
11            Transport: &http.Transport{
12                ForceAttemptHTTP2: true,
13                MaxConnsPerHost: 5,
14                MaxIdleConns: 2,
15                MaxIdleConnsPerHost: 2,
16                TLSHandshakeTimeout: time.Duration(5) * time.Second,
17            },
18        },
19    }
20}

Parâmetros de Configuração 🔗

Podemos melhorar a flexibilidade do nosso cliente http utilizando uma struct de configuração que tem valores padrão e permite que o usuário modifique apenas os valores que deseja. Porém ainda assim, o usuário precisa conhecer todos os campos da struct e seus valores padrão, o que pode ser um pouco confuso.

 1type ClientConfig struct {
 2    BaseURL string
 3    ForceAttemptHTTP2 bool
 4    Timeout time.Duration
 5    MaxConnsPerHost int
 6    MaxIdleConns int
 7    MaxIdleConnsPerHost int
 8    TLSHandshakeTimeout time.Duration
 9}
10
11func NewClient(cfg ClientConfig) *Client {
12    if cfg.BaseURL == "" {
13        cfg.BaseURL = "https://api.example.com"
14    }
15
16    if cfg.Timeout == 0 {
17        cfg.Timeout = 10 * time.Second
18    }
19
20    if cfg.MaxConnsPerHost == 0 {
21        cfg.MaxConnsPerHost = 5
22    }
23
24    if cfg.MaxIdleConns == 0 {
25        cfg.MaxIdleConns = 2
26    }
27
28    if cfg.MaxIdleConnsPerHost == 0 {
29        cfg.MaxIdleConnsPerHost = 2
30    }
31
32    if cfg.TLSHandshakeTimeout == 0 {
33        cfg.TLSHandshakeTimeout = 5 * time.Second
34    }
35
36    return &Client{
37        baseURL: cfg.BaseURL,
38        client: &http.Client{
39            Timeout: cfg.Timeout,
40            Transport: &http.Transport{
41                ForceAttemptHTTP2: cfg.ForceAttemptHTTP2,
42                MaxConnsPerHost: cfg.MaxConnsPerHost,
43                MaxIdleConns: cfg.MaxIdleConns,
44                MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost,
45                TLSHandshakeTimeout: cfg.TLSHandshakeTimeout,
46            },
47        },
48    }
49}

Options Pattern - Declaração 🔗

O padrão Options Pattern é uma forma de encapsular as “opções” de configuração em funções que podem ser passadas para o construtor da struct. Isso permite que o usuário configure apenas os valores que deseja, sem precisar conhecer todos os campos da struct.

 1type Client struct {
 2    baseURL string
 3    client *http.Client
 4}
 5
 6type Option func(*Client)
 7
 8func NewClient(baseURL, opts ...Option) *Client {
 9    c := &Client{
10        baseURL: baseURL,
11        client: &http.Client{
12            Timeout: time.Duration(10) * time.Second,
13            Transport: &http.Transport{
14                ForceAttemptHTTP2: true,
15                MaxConnsPerHost: 5,
16                MaxIdleConns: 2,
17                MaxIdleConnsPerHost: 2,
18                TLSHandshakeTimeout: time.Duration(5) * time.Second,
19            },
20        },
21    }
22
23    for _, option := range opts {
24        option(c)
25    }
26
27    return c
28}

Options Pattern - Implementação 🔗

Agora podemos criar funções que modificam os valores da struct Client de forma flexível e extensível. Essas funções são chamadas de “opções” e podem ser passadas para o construtor da struct do cliente http.

 1// WithTimeout é uma opção que define o tempo limite para as requisições HTTP.
 2func WithTimeout(timeout time.Duration) Option {
 3    return func(c *Client) {
 4        c.client.Timeout = timeout
 5    }
 6}
 7
 8// WithHostname é uma opção que define o header X-Machine-Name com o hostname informado.
 9func WithHostname(hostname string) Option {
10    return func(c *Client) {
11        next := c.doFunc
12        c.doFunc = func(c *Client, req *http.Request) (*http.Response, error) {
13            hostname = url.QueryEscape(hostname)
14            req.Header.Set("X-Machine-Name", hostname)
15
16            return next(c, req)
17        }
18    }
19}

Options Pattern - Controle de Erros 🔗

É muito importante validar os erros que fazem sentido na “opção” que está sendo aplicada. Por exemplo, se o usuário passar um certificado SSL inválido, devemos retornar um erro.

 1// WithSSLCertFile sobrescreve os certificados padrões para o certificado passado por parametro.
 2func WithSSLCertFile(ctx context.Context, filepath string) (Option, error) {
 3    caCert, err := file.ReadHead(ctx, filepath, 0)
 4    if err != nil {
 5        return nil, err
 6    }
 7
 8    caCertPool := x509.NewCertPool()
 9    caCertPool.AppendCertsFromPEM(caCert)
10
11    return WithSSLCertPool(caCertPool), nil
12}
13
14withSSLCert, err := WithSSLCertFile(context.Background(), "/path/to/cert.pem")
15if err != nil {
16    panic(err) // não faça isso em casa
17}

Na Prática 🔗

Aqui temos um exemplo completo utilizando as “opcões” que criamos ao longo deste post. Vale ressaltar que a única “opção” que retorna erro é de sobrescrever os certificados, e por isso que validamos o erro apenas dela. Agora vamos ver como ficaria a implementação do cliente http utilizando tudo que aprendemos aqui.

 1func DoSomething() {
 2    opts := []Option{
 3        WithTimeout(20 * time.Second),
 4        WithHostname("golang-flow"),
 5    }
 6
 7    withSSLCertFile, err := WithSSLCertFile(context.Background(), "/path/to/cert.pem")
 8    if err != nil {
 9        panic(err) // não faça isso em casa
10    }
11
12    opts = append(opts, withSSLCertFile)
13
14    cli := NewClient("https://api.example.com", opts...)
15
16    // Do ...
17}

Testes Unitários 🔗

Criar testes unitários para as “opções” é muito simples, vamos ver como fazer isso.

 1// setupTestServer configura um servidor de teste para simular requisições HTTP.
 2func setupTestServer() (string, *http.ServeMux, func()) {
 3    router := http.NewServeMux()
 4    srv := httptest.NewServer(router)
 5
 6    return srv.URL, router, func() { srv.Close() }
 7}
 8
 9// TestOption_WithTimeout testa a opção WithTimeout para o cliente http.
10func TestOption_WithHostname(t *testing.T) {
11    url, router, tearDown := setupTestServer()
12    defer tearDown()
13
14    router.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) {
15        // Verifica se o header X-Machine-Name foi adicionado corretamente
16        assert.Equal(t, []string{"my-computer"}, req.Header["X-Machine-Name"])
17    })
18
19    // Cria o slice de opções com a opção WithHostname
20    opts := []Option{WithHostname("my-computer")}
21
22    req, err := http.NewRequest(http.MethodGet, url, nil)
23    require.NoError(t, err)
24
25    // Cria o cliente com a URL do servidor de teste e as opções
26    c := NewClient("https://api.example.com", opts...)
27
28    resp, err := c.Do(t.Context(), req)
29    require.NoError(t, err)
30
31    defer resp.Body.Close()
32}
33
34// TestOption_WithSSLCertFile testa a opção WithSSLCertFile para o cliente http e valida se o certificado SSL foi aplicado corretamente.
35func TestOption_WithSSLCertFile(t *testing.T) {
36    url, router, tearDown := setupTestServer()
37    defer tearDown()
38
39    router.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) {
40        // Verifica se o certificado SSL foi aplicado corretamente
41        assert.NotNil(t, req.TLS)
42        assert.NotEmpty(t, req.TLS.PeerCertificates)
43    })
44
45    caCertFile := "testdata/ca_cert.pem"
46    withSSLCertFile, err := WithSSLCertFile(ctx, caCertFile)
47    require.NoError(t, err)
48
49    // Cria o slice de opções com a opção WithSSLCertFile
50    opts := []Option{withSSLCertFile}
51
52    req, err := http.NewRequest(http.MethodGet, url, nil)
53    require.NoError(t, err)
54
55    // Cria o cliente com a URL do servidor de teste e as opções
56    c := NewClient("https://api.example.com", opts...)
57
58    resp, err := c.Do(ctx, req)
59    require.NoError(t, err)
60
61    defer resp.Body.Close()
62}