Os benefícios de usar Form Objects em seus projetos

rodrigonbarreto_86

Rodrigo Barreto

Posted on January 6, 2024

Os benefícios de usar Form Objects em seus projetos

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

# ruby 3.2.2
# rails 7
rails new event_reservation_system --api
Enter fullscreen mode Exit fullscreen mode

Passo 2: Navegar para o Diretório do Projeto

cd event_reservation_system
rails db:create
Enter fullscreen mode Exit fullscreen mode

Passo 3: Gerar o Modelo Event

rails generate model Event title:string description:text event_type:string number_of_people:integer special_requests:text
Enter fullscreen mode Exit fullscreen mode

Passo 4: Executar as Migrações

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

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:

rails generate controller api/v1/events
Enter fullscreen mode Exit fullscreen mode

Passo 6: Definir Rotas

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :events
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode
# frozen_string_literal: true

module Events
  class EventBusinessForm < Events::EventFormBase
    validates :number_of_people, numericality: { greater_than: 5 }
  end
end
Enter fullscreen mode Exit fullscreen mode
# frozen_string_literal: true

module Events
  class EventBirthdayForm < Events::EventFormBase
    validates :number_of_people, numericality: { greater_than: 10 }
  end
end
Enter fullscreen mode Exit fullscreen mode

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)

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

Enter fullscreen mode Exit fullscreen mode

E o controller:

Image description

Lembre-se que este controller deveria ser mais skinny, o foco aqui é falar do form object

# 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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

**Implementação no EventFormBase

# 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
Enter fullscreen mode Exit fullscreen mode

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

Image description

Caso com validation

Image description
Image description

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

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!

💖 💪 🙅 🚩
rodrigonbarreto_86
Rodrigo Barreto

Posted on January 6, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related