Dando continuidade à nossa série de posts sobre arquitetura hexagonal, nesse post implementaremos a entity do package category, que faz parte do core da aplicação.
Se você ainda não leu, convido a ler o post onde definimos a organização das pastas e arquivos desse projeto utilizando arquitetura hexagonal.
Definindo Category
Sem mais delongas, a primeira coisa que vou fazer no arquivo entity.go, é definir a struct Category.
type Category struct { ID primitive.ObjectID `bson:"_id" json:"id"` Name string `bson:"name" json:"name"` Description string `bson:"description" json:"description"` CreatedAt time.Time `bson:"created_at" json:"created_at"` ModifiedAt time.Time `bson:"modified_at" json:"modified_at"` Active bool `bson:"active" json:"active"` }
Repare que, além de definir os campos da struct, também adicionei as tags bson e json. Essas tags serão utilizadas para serializar e desserializar essa struct nos formatos bson (mongo) e json.
Sei que alguns podem criticar minha abordagem e dizer que “o certo seria ter uma struct para cada camada”. No entanto, sendo arquitetura hexagonal, domain-driven design, solid, clean architecture e etc apenas teorias, cabe a nós entender seus fundamentos antes de aplicá-los.
Entendo que algumas linguagens não tem essa feature de tag, e por isso, faz todo sentido ter várias classes “iguais” em camadas diferentes, mas esse não é o caso do Go.
Em Go, ter várias structs para representar o mesmo tipo de dado, a meu ver, não faz muito sentido. Isso por que, tirando o fato de você aumentar o consumo de memória e processamento (afinal você precisa ficar “copiando” os dados de uma struct para a outra), você acaba dificultando futuras manutenções, já que você tem 3 structs para olhar ao invés de uma.
Lembre-se, menos código, menos chance de bug.
Ok, agora que expliquei um pouco da minha motivação para utilizar essa abordagem, vamos para o segundo passo, definir as variáveis de erro.
Variáveis de erro
Como o campo Name é o único obrigatório para a struct Category, precisamos criar somente uma variável de erro.
var ( ErrNameRequired = errors.New("name is required") )
Essa abordagem de criar as variáveis de erro como variável global do package, facilita na hora de escrever testes unitários, pois conseguimos testar exatamente o erro retornado e não só a mensagem do erro.
Métodos
Agora, implementaremos os cinco métodos da struct Category. Esses métodos devem fornecer meios para mudar o valor dos campos Name, Description e Active, assim como validar se a struct tem todos os campos obrigatórios preenchidos.
unc (c *Category) ChangeName(name string) error { if name == "" { return ErrNameRequired } c.Name = name c.ModifiedAt = time.Now() return nil } func (c *Category) ChangeDescription(description string) { c.Description = description c.ModifiedAt = time.Now() } func (c *Category) Enable() { c.Active = true c.ModifiedAt = time.Now() } func (c *Category) Disable() { c.Active = false c.ModifiedAt = time.Now() } func (c *Category) Validate() error { if c.Name == "" { return ErrNameRequired } return nil }
Repare que, exceto no método de validação, sempre que mudamos um valor da struct, também atualizamos o campo ModifiedAt.
Funções
Por fim, mas não menos importante, proveremos duas funções para criar a struct Category.
A primeira será uma função mais declarativa, onde precisamos passar os valores name e description para que ela nos retorne um ponteiro de Category.
func New(name, description string) (*Category, error) { now := time.Now() c := Category{ Name: name, Description: description, CreatedAt: now, ModifiedAt: now, Active: true, } err := c.Validate() if err != nil { return nil, err } return &c, nil }
Já a segunda será uma função “facilitadora”, onde só precisamos passar um parâmetro do tipo io.ReadCloser
– que é o tipo do atributo Body da struct Request do package net/http – que a função nos retornará um ponteiro de Category.
func Bind(body io.ReadCloser) (*Category, error) { var c Category err := json.NewDecoder(body).Decode(&c) if err != nil { return nil, err } now := time.Now() c.CreatedAt = now c.ModifiedAt = now err = c.Validate() if err != nil { return nil, err } return &c, nil }
Repare que em ambas as funções, afim de validar se todos os campos obrigatórios foram preenchidos corretamente, após o preenchimento dos valores da struct Category, o método Validate()
é executado.
Conclusão
Nesse post definimos a struct Category, implementamos quatro métodos para manipular os valores da struct, um método de validação e duas funções para inicialização da struct.
Com isso, fechamos a implementação básica da entity Category.
No próximo post, vamos começar a implementação dos ports.
Para não perder nada, assine agora a nossa newsletter. É 100% gratuita e sempre será!
Até a próxima!