Os benefícios de usar Form Objects em seus projetos
Tags: ruby, rails
Published: 2024-01-06 11:03:54

Na construção de aplicações web com Ruby on Rails, lidar com a complexidade dos formulários e dos dados recebidos pode rapidamente se tornar um desafio. Frequentemente, a tentação é incluir lógica de negócios diretamente nos modelos, o que pode levar a um código denso e de difícil manutenção. A solução? Form Objects.

Essas estruturas simples, mas poderosas, oferecem uma maneira elegante de manipular e validar formulários e parâmetros de acordo com as regras de negócios, mantendo essa complexidade afastada dos modelos. Neste post, vamos mergulhar na utilização de Form Objects para criar uma camada clara e eficiente de manipulação de dados, garantindo que apenas os dados corretamente validados e formatados cheguem aos nossos modelos.

Para isso vamos criar um projeto de reserva de eventos.

## Contexto

Nosso sistema de reserva de eventos precisa lidar com dois tipos de eventos: aniversários (**`birthday`**) e eventos corporativos (**`business`**). Cada tipo de evento tem suas próprias regras de validação que são um pouco diferentes:

- **Eventos de Aniversário (`birthday`)**: O número de pessoas deve ser maior que 10.
- **Eventos Corporativos (`business`)**: O número de pessoas deve ser maior que 5.

Essas regras são simples, mas ilustram bem como as necessidades de validação podem variar significativamente com base no contexto do evento.



## Criando o projeto

**Passo 1: Criar um Novo Projeto Rails**

```bash
# ruby 3.2.2
# rails 7
rails new event_reservation_system --api
```
**Passo 2: Navegar para o Diretório do Projeto**

```bash
cd event_reservation_system
rails db:create
```
**Passo 3: Gerar o Modelo Event**
```bash
rails generate model Event title:string description:text event_type:string number_of_people:integer special_requests:text
```
**Passo 4: Executar as Migrações**

```bash
rails db:migrate
```

**Passo 5: Gerar o Controller de Events**
Agora, crie um controller para os eventos. Este controller será usado para lidar com as requisições HTTP para criar, ler, atualizar e deletar eventos:

```bash
rails generate controller api/v1/events
```

**Passo 6: Definir Rotas**
```ruby
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :events
    end
  end
end
```

No coração do nosso projeto `event_reservation_system`, estamos explorando uma abordagem elegante e eficiente para lidar com validações condicionais em Ruby on Rails usando Form Objects. Especificamente, vamos demonstrar como diferentes tipos de eventos podem ter diferentes regras de validação, e como os Form Objects tornam esse processo mais gerenciável e limpo.

## Uso de Form Objects

Poderíamos implementar essas validações diretamente nos modelos, mas isso rapidamente tornaria os modelos complexos e difíceis de manter, especialmente à medida que as regras de negócios se tornam mais complexas. Em vez disso, vamos usar Form Objects.

Com os Form Objects, podemos encapsular a lógica de validação específica de cada tipo de evento em um objeto separado. Isso não apenas mantém nosso modelo **`Event`** limpo e focado, mas também nos permite gerenciar a complexidade de maneira mais eficiente.

## Demonstração no Código

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tc5rkf4573ikccycggq0.png)

```ruby
# frozen_string_literal: true

module Events
  class EventFormBase
    include ActiveModel::Model

    attr_accessor :title, :description, :event_type, :number_of_people, :special_requests

    validates :title, :event_type, :number_of_people, presence: true

    def attributes
      {
        title:,
        description:,
        event_type:,
        number_of_people:,
        special_requests:
      }
    end
  end
end
```


```ruby
# frozen_string_literal: true

module Events
  class EventBusinessForm < Events::EventFormBase
    validates :number_of_people, numericality: { greater_than: 5 }
  end
end
```

```ruby
# frozen_string_literal: true

module Events
  class EventBirthdayForm < Events::EventFormBase
    validates :number_of_people, numericality: { greater_than: 10 }
  end
end
```

Neste exemplo, desenvolvemos um Form Object base e, a partir dele, criamos dois Form Objects especializados: um para eventos do tipo 'birthday' (aniversário) e outro para 'business' (negócios).

O nosso modelo é configurado como exemplo abaixo, e é importante lembrar que ainda é possível incluir validações no próprio modelo. No entanto, estas tendem a se concentrar mais em aspectos relacionados à integridade dos dados no banco de dados, em vez de regras de negócios específicas. (Neste exemplo nāo adicionei nenhuma validação)

```ruby
class Event < ApplicationRecord
  enum event_type: { business: 'business', birthday: 'birthday' }
end

```


## E o controller:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/04of7s3368j0pl9ecotl.png)

**Lembre-se que este controller deveria ser mais `skinny`, o foco aqui é falar do form object**
```ruby
# frozen_string_literal: true


module Api
  module V1
    class EventsController < ApplicationController
      class InvalidEventTypeError < StandardError; end
      INVALID_EVENT_TYPE_MSG = 'Invalid event type'

      def create
        form = build_event_form(event_params)
        return render json: form.errors, status: :unprocessable_entity unless form.valid?

        create_event(form.attributes)
      rescue InvalidEventTypeError => e
        render json: { error: e.message }, status: :bad_request
      end

      private

      def create_event(attributes)
        event = Event.new(attributes)
        if event.save
          render json: event, status: :created
        else
          render json: event.errors, status: :unprocessable_entity
        end
      end

      def build_event_form(params)
        case params[:event_type]
        when Event.event_types[:birthday]
          Events::EventBirthdayForm.new(event_params)
        when Event.event_types[:business]
          Events::EventBusinessForm.new(event_params)
        else
          raise InvalidEventTypeError, INVALID_EVENT_TYPE_MSG
        end
      end

      def event_params
        params.require(:event).permit(:title, :description, :event_type, :number_of_people, :special_requests)
      end
    end
  end
end
```

Neste exemplo, no nosso controller, o método **`build_event_form`** desempenha um papel crucial. Você notará que, com base no valor de **`params[:event_type]`**, ele direciona para um Form Object diferente. Essa abordagem facilita a escalabilidade do nosso sistema para acomodar diferentes tipos de eventos e novas regras de validação com grande facilidade.

Uma observação importante: a estrutura atual deste método, utilizando um **`case`**, é bastante direta. No entanto, em um cenário mais complexo ou para uma maior escalabilidade, poderíamos considerar **implementar um padrão de design como o Factory**. Isso será explorado em detalhes em um futuro post.

No método create do nosso controller, temos uma lógica que primeiro constrói o Form Object usando build_event_form e verifica sua validade. Se o formulário for válido, prosseguimos com a criação do evento. É interessante notar que algumas pessoas preferem encapsular a criação do evento diretamente dentro do próprio Form Object. Essa é uma abordagem viável e pode oferecer uma maior encapsulação da lógica de negócios. Abaixo, você encontra um exemplo de como isso pode ser feito.

**Exemplo no Controller**
```ruby
def create
  form = build_event_form(event_params)
  return render json: form.errors, status: :unprocessable_entity unless form.valid?

  event = form.create
  render json: event, status: :created
rescue InvalidEventTypeError => e
  render json: { error: e.message }, status: :bad_request
end
```

**Implementação no EventFormBase

```ruby
# frozen_string_literal: true

module Events
  class EventFormBase
    include ActiveModel::Model

    attr_accessor :title, :description, :event_type, :number_of_people, :special_requests

    validates :title, :event_type, :number_of_people, presence: true

    def attributes
      {
        title:,
        description:,
        event_type:,
        number_of_people:,
        special_requests:
      }
    end

    def create
      Event.create!(attributes)
    end
  end
end
```

Nesta abordagem, o método create no EventFormBase é responsável por criar o evento, tornando o controller mais enxuto e delegando a responsabilidade de criação do evento para o Form Object.

Para facilitar o entendimento, incluí alguns exemplos do Postman/Insomnia. Nestes exemplos, você encontrará demonstrações das mensagens de validação e também dos casos de sucesso para cada tipo de solicitação. Isso deve ajudar a visualizar melhor como cada cenário é tratado e as respostas correspondentes.

**Birthday**

Url: http://localhost:3000/api/v1/events/

**Caso com sucesso**
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7s29ackg9j1j7rzhitj2.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgi5rwgo3mw6fxf6i5bo.png)

**Caso com validation**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4taxvbjsmomg4v9qbk5.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3jmymloyingndm7ocx65.png)

Espero que vocês tenham achado este mergulho nos Form Objects no Ruby on Rails útil. Se quiserem ver mais detalhes sobre o projeto event_reservation_system, incluindo a implementação completa e outros recursos interessantes, confiram o repositório no meu GitHub: [https://github.com/rodrigonbarreto/event_reservation_system](https://github.com/rodrigonbarreto/event_reservation_system)

Ainda temos muito a explorar neste tópico, então fiquem atentos para mais posts sobre Rails e outras técnicas e padrões de desenvolvimento. Até a próxima!