DEV Community

Felipe
Felipe

Posted on • Edited on

Options Pattern em Go

Essa semana estive estudando alguns Padrões de Design em Go e achei bastante interessante a implementação do padrão Options. A ideia desse artigo é explicar a motivação na adoção desse design pattern e destrinchar sua implementação.

Rabiscando um personagem de RPG

Imagine que você, renomado consultor de engenharia de software, é responsável por entregar um protótipo de RPG para um cliente. Você decide começar desenvolvendo pelo personagem Warrior, e implementa dessa forma:

type Warrior struct {
    Attack   int
    Defense  int
    useSword bool
}

func NewWarrior(attack int, def int, useSwordbool) *Warrior {
    w := &Warrior{}
    w.Attack = attack
    w.Defense = def
    w.useSword= useSword

    return w
}
Enter fullscreen mode Exit fullscreen mode

Seu cliente não tem lá um padrão de qualidade muito alto e curtiu muito sua implementação. E ele sai criando vários e vários Warriors pela base de código dessa forma:

w := NewWarrior(10, 5, true)
Enter fullscreen mode Exit fullscreen mode

Beleza. Seu cliente lançou uma versão v0 do tão esperado RPG.

Alguns meses passam, e seu cliente quer mais umas “coisinhas simples” no código para lançar a versão v1 do game. Ele decidiu que seria ótimo se o personagem Warrior também tivesse outros atributos, como hasAxe e hasClub.

Prontamente você modifica a struct e a função construtora:

type Warrior struct {
    Attack   int
    Defense  int
    useSword bool
    useAxe   bool
    useClub  bool
}

func NewWarrior(attack int, def int, useSword bool, useAxe bool, useClub bool) *Warrior {
    w := &Warrior{}
    w.Attack = attack
    w.Defense = def
    w.useSword = useSword
    w.useAxe = useAxe
    w.useClub = useClub

    return w
}
Enter fullscreen mode Exit fullscreen mode

Agora, pra inicializar um novo Warrior na nova versão v1, seu cliente faz dessa forma:

w := NewWarrior(10, 5, true, false, false)
Enter fullscreen mode Exit fullscreen mode

Entretanto, essa implementação tem dois problemas:

  • Com o aumento da quantidade de atributos da struct Warrior, torna-se inviável usar a função NewWarrior e passar uma quantidade tão grande de argumentos.

  • Você introduz uma breaking change entre as versões v0 e v1. Seu cliente terá de refatorar toda a base de código para atualizar a assinatura e declaração da função NewWarrior com os novos atributos que a struct Warrior recebeu.

Talvez você já tenha pensado em uma solução: ao invés de passar os valores dos atributos de Warrior como argumento da função construtora, passar uma struct com os atributos, dessa forma:

type Warrior struct {
    attributes Attributes
}

type Attributes struct {
    attack   int
    defense  int
    hasSword bool
}

func NewWarrior(attributes Attributes) *Warrior {
    w := &Warrior{
        attributes: attributes,
    }

    return w
}

func main() {
    w := NewWarrior(
        Attributes{
            10,
            5,
            true,
        },
    )

    fmt.Println(w)
}

Enter fullscreen mode Exit fullscreen mode

Por mais que essa solução resolva o primeiro problema, o segundo ainda será uma dor de cabeça para seu cliente.

Usando o padrão Options

Como fazer um bom código significa escrever código extensível/com fácil manutenção, vamos explorar uma outra abordagem utilizando o padrão Options.

Voltando na implementação da primeira versão v0, vamos criar um novo objeto Warrior dessa forma:

type Warrior struct {
    attack   int
    defense  int
    useSword bool
}

func NewWarrior(options ...func(*Warrior)) *Warrior {
    w := &Warrior{}

    for _, option := range options {
        option(w)
    }

    return w
}
Enter fullscreen mode Exit fullscreen mode

WHAT?!

Eu confesso que de cara não é a função mais fácil de ser entendida, mas você vai ver que daqui alguns minutos tudo fará sentido. Segue o jogo!

A nova função NewWarrior recebe como argumento uma função com assinatura func(*Warrior). Ou seja, recebe como argumento uma outra função cujo argumento é um ponteiro para a struct Warrior, ou melhor, um ponteiro para um objeto de Warrior. Os três pontinhos ali (ou reticências, pra soar mais bonito) indicam que NewWarrior é uma função variádica, ou seja, recebe qualquer número de argumentos. Se eu quiser passar 1, 2, ou 100 funções como argumento de Warrior, a função vai executar normalmente.

O loop for na função faz algo bastante simples: ele executa cada função, passada como argumento em NewWarrior, colocando o objeto w como argumento. Ou seja, a função NewWarrior nada mais é que um loop que vai executar várias funções colocando o objeto w como argumento. No fim, só retorna esse objeto.

Beleza, mas que tipo de função a gente vai passar como argumento de NewWarrior?

Se liga na implementação dessa funçãozinha aqui, que vai setar o valor do ataque do nosso Warrior:

func WithAttack(attack int) func(*Warrior) {
    return func(w *Warrior) {
        w.attack = attack
    }
}
Enter fullscreen mode Exit fullscreen mode

A função WithAttack recebe o valor do ataque desejado e retorna uma nova função func(*Warrior). Logo em seguida, já implementamos essa função de retorno, que é uma função também simples: ela funciona de forma similar a um setter, caso você já tenha estudado um pouquinho de Orientação a Objetos antes.

Agora veja como o seu cliente vai inicializar um novo objeto:


package main

import "fmt"

type Warrior struct {
    attack   int
    defense  int
    useSword bool
}

func NewWarrior(options ...func(*Warrior)) *Warrior {
    w := &Warrior{}

    for _, option := range options {
        option(w)
    }

    return w
}

func WithAttack(attack int) func(*Warrior) {
    return func(w *Warrior) {
        w.attack = attack
    }
}

func WithDefense(def int) func(*Warrior) {
    return func(w *Warrior) {
        w.defense = def
    }
}

func UseSword(useSword bool) func(*Warrior) {
    return func(w *Warrior) {
        w.useSword = useSword
    }
}

func main() {
    w := NewWarrior(
        WithAttack(10),
        WithDefense(5),
        UseSword(true),
    )

    fmt.Println(w)
}

Enter fullscreen mode Exit fullscreen mode

Na nossa nova função construtora de Warrior, vamos passar funções que vão setar os atributos do objetos, ao invés de passar os valores diretamente. Lembre-se que NewWarrior irá apenas executar, em loop, todas as funções passadas a ele usando um objeto do tipo *Warrior como argumento.

Dessa forma, se eu lançar uma nova versão da minha aplicação com novos atributos (como useAxe e useClub), o código não vai quebrar por conta dos objetos inicializados sem esses atributos. Afinal, como NewWarrior é só um loop que executa funções, se eu não passar setters de useAxe e useClub, a função NewWarrior só não vai executar estes setters e o objeto criado terá o valor default para esses atributos.

Esse Design Pattern é bastante útil no design de structs relacionados a configuração de um objeto, como no caso de um server http. Como desafio, recomendo colocar mais alguns atributos na struct Warrior pra você ver como é fácil estender essa struct sem introduzir um breaking change na sua aplicação =)

Referências:
https://golang.cafe/blog/golang-functional-options-pattern.html
https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md

Top comments (0)