Sumário 🔗
- Valores pré-definidos
- Parâmetros de Configuração
- Options Pattern - Declaração
- Options Pattern - Implementação
- Options Pattern - Controle de Erros
- Na Prática
- 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}