Arquitetura hexagonal: Como implementar uma entity

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!


Se inscreva na nossa newsletter

* indicates required

Deixe uma resposta