Benito Serna Tips and tools for Ruby on Rails developers

Rails no es mi app…. pero ¿Que tanto debería delegar al modelo?

October 26, 2016

En esta idea de “Rails is not my app” es necesario definir cuales son las cosas que va hacer tu aplicación y cuales va a delegar.

Cuando usas Rails o algún otro framework similar, lo usas porque no quieres repetir lo que otros ya han hecho, para así desarrollar más rápido. Pero si estas tratando de separa la “lógica de tu aplicación” del framework no es tan fácil.

Desde mi punto de vista las cosas más grandes o importantes que nos da ActiveRecord es la validación de datos y la comunicación con la base de datos. ¿Como puedes hacer para seguir aprovechando esto?

¿Como aprovechar la comunicación a la base de datos que nos da ActiveRecord?

Lo que yo hago es tratar de considerar al modelo como si fuera mi fuente de almacenamiento de datos. Algo muy similar a lo que llaman “Repository Pattern”, pero siempre inyectando la dependencia.

Por ejemplo, en la siguiente función para exportar usuarios, tengo que pedir los últimos usuarios registrados. Para esto inyecto la clase User de Active Record que viene siendo mi “Users Repository” y para las pruebas creo un objeto muy simple con el que puedo definir la interfaz esperada.

class User < ActiveRecord::Base
  def self.last_registered(number)
    order(id: :desc).limit(number)
  end
end

class Admin::UsersExportsController < Admin::BaseController
  def create
    export = Users.export_users(User, params[:users_export], Date.current)
    # other stuff...
  end
end

module Users
  def self.export_users(store, params, current_date)
    # stuff..
    records = store.last_registered(form.number_of_users)
    # stuff..
  end
end

module Users
  RSpec.describe "Export users" do
    describe "generates a list of users" do
      it "with the given number of last registered users" do
        store = store_with (1..100).map { |id| user_with(id: id) }
        export = export_users(store, "number_of_users" => 10)
        expect(export.users_list.count).to eq 10
        expect(export.users_list.first.id).to eq 100
      end
    end

    def store_with(users)
      FakeUsersStore.new(users)
    end

    def user_with(attrs)
      FakeUser.new(attrs)
    end

    def export_users(store, params, current_date = Date.current)
      Users.export_users(store, params, current_date)
    end

    class FakeUsersStore
      def initialize(records)
        @records = records
      end

      def last_registered(number)
        records.reverse.first(number)
      end

      private

      attr_reader :records
    end
  end
  end

¿Como aprovechar las validaciones de ActiveRecord?

La mayoría de las validaciones de un modelo vienen definidas en el módulo ActiveModel::Model. Por lo que una forma de seguir utilizando estos métodos, es incluir ActiveModel como dependencia.

Por ejemplo, en la siguiente función ProjectUpdates.createprojectupdate incluyo ActiveModel::Model en ProjectUpdates::Form para que pueda hacer las validaciones por mi.

class Admin::ProjectUpdatesController < Admin::BaseController
  def create
    project = Project.find_by_slug!(params[:project_id])
    creation = ProjectUpdates.create_project_update(project, params[:project_update], ProjectUpdate)

    if creation.success?
      # render something
    else
      # render something
    end
  end
end

module ProjectUpdates
  def self.create_project_update(project, params, project_updates_store)
    CreateProjectUpdate.new(project, params, project_updates_store).call
  end

  class CreateProjectUpdate
    def initialize(project, params, project_updates_store)
      @project = project
      @form = Form.new(params)
      @project_updates_store = project_updates_store
    end

    def call
      if form.valid?
        record = project_updates_store.create(form.to_h.merge(project_id: project.id))
        SuccessResponse.new(record)
      else
        ErrorResponse.new(NewUpdatePage.new(project, form))
      end
    end

    private

    attr_reader :project, :form, :project_updates_store, :notifier
  end

  class Form
    include ActiveModel::Model

    ATTRIBUTES = [:title, :content, :date]
    attr_accessor *ATTRIBUTES
    validates_presence_of *ATTRIBUTES

    def to_h
      ATTRIBUTES.map { |field| [field, send(field)] }.to_h
    end
  end
end

module ProjectUpdates
  RSpec.describe "Create project updates" do
    describe "creates a record" do
      describe "(error)" do
        before do
          expect(project_updates_store).not_to receive(:create)
        end

        it "validates presence of title" do
          creation = create_project_update(project, params.merge("title" => nil))
          expect(creation).not_to be_success
          expect(creation.page.form.errors[:title]).to eq ["no puede estar en blanco"]
        end

        it "validates presence of content" do
          creation = create_project_update(project, params.merge("content" => nil))
          expect(creation).not_to be_success
          expect(creation.page.form.errors[:content]).to eq ["no puede estar en blanco"]
        end

        it "validates presence of date" do
          creation = create_project_update(project, params.merge("date" => nil))
          expect(creation).not_to be_success
          expect(creation.page.form.errors[:date]).to eq ["no puede estar en blanco"]
        end
      end
    end
  end
end

…Espero que estos dos ejemplos o técnicas te ayuden a visualizar un poco como puedes tratar de separar la lógica de tu aplicación de Rails o el framework que estas usando.

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.