¿Tu proyecto tiene mas de 500 gemas? ¿Tienes pesadillas con las cascadas de callbacks de ActiveRecord? ¿Tus Fat Models luchan por mantenerse con vida? ¿Recibes con miedo cada nueva versión de Rails?
Si te sientes identificado al leer estas lineas quizás te interese lo que tengo que contar. Rails es un framework estupendo para empezar un proyecto y entregar un MVP rápidamente pero a partir de ese momento degrada prácticamente con la misma facilidad.
En esta charla repasaremos una serie de técnicas y consejos "desde la trinchera" para poder mantener y hacer crecer proyectos Rails.
6. MADRID · NOV 18-19 · 2016
Hay una nueva versión de Rails
7. MADRID · NOV 18-19 · 2016
Sistema conectado Vs modular
To Design or Not To Design? A Third Good Question
8. MADRID · NOV 18-19 · 2016
Sistema conectado Vs modular
● Rails no es mi aplicación.
● Mi aplicación es mi lógica de negocio.
● Mi aplicación debe ser independiente.
● Idependiente del contexto.
● Independiente de como se accede a ella.
● Independiente de las herramientas que se usan para contruirla.
9. MADRID · NOV 18-19 · 2016
Problemas: Gems
● Gemas que añaden poco valor y hay que mantener.
● Gemas que se acoplan demasiado a ActiveRecord.
● Gemas con toneladas de dependencias.
● Gemas sin mantenimiento.
$ bundle list | wc -l
341
10. MADRID · NOV 18-19 · 2016
Problemas: God objects
● User.rb
● LOC > 1000
● Derivado de la politica “Fat
models”
11. MADRID · NOV 18-19 · 2016
Problemas: ActiveRecord
●
Acoplamiento entre persistencia y lógica de negocio.
●
Demasiadas responsabilidades en una sola clase.
●
Callbacks.
irb> User.new.methods.count
=> 2307
12. MADRID · NOV 18-19 · 2016
Problemas: Rails way
●
Fat controllers.
●
Fat Models.
●
Concerns.
●
Imposibilita hacer Unit testing.
13. MADRID · NOV 18-19 · 2016
Refactor: Gem diet
●
Gemas que aporten valor real.
●
Gemas mantenidas.
●
Gemas para conectar con servicios externos (S3, Mailchimp, etc.).
●
Bundle groups.
14. MADRID · NOV 18-19 · 2016
Refactor: Form objects
●
Contienen validación de datos.
●
Un formulario por cada caso de uso.
●
Un formulario puede contener datos de varios modelos.
15. MADRID · NOV 18-19 · 2016
Refactor: Form objects
# app/forms/users/create_form.rb
module Users
class RegisterForm
include ActiveModel::Model
include Virtus.model
attribute :nickname, String
attribute :email, String
attribute :password, String
validates :nickname, presence: true
validates :email, presence: true, format: RFC_2822
validates :password, presence: true, length: { within: 8..50 }
def persisted?
false
end
end
end
16. MADRID · NOV 18-19 · 2016
Refactor: Service objects
●
Un servicio por cada caso de uso.
●
Inyección de dependencias.
●
Interfaz común (.execute)
●
Contienen:
– Lógica de negocio.
– Llamadas a servicios externos.
– Llamadas a servicios internos.
●
Los servicios delegan la persistencia a ActiveRecord.
17. MADRID · NOV 18-19 · 2016
Refactor: Service Object
# app/services/users/create_form.rb
module Users
class RegisterService
class RegisterError < StandardError; end
def initialize(form)
@attrs = form.data
end
def execute
user = User.new(@attrs)
raise RegisterError, user.errors unless user.save
welcome_email
add_to_newsletter if user.newsletter_subscription?
suggest_followings
# ...
end
end
end
18. MADRID · NOV 18-19 · 2016
Refactor: Model
# app/models/user.rb
class User < ActiveRecord::Base
has_many :items
has_many :roles
def newsletter_subscription?
# ...
end
end
19. MADRID · NOV 18-19 · 2016
Refactor: Controller (1)
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
authorize User
if register_form.valid?
user = register_form.execute
render user, status: :created
else
render register_form, status: :unprocessable_entity
end
end
def update
authorize user
if profile_update_form.valid?
updated_user = profile_update_service.execute
render updated_user, status: :ok
else
render profile_update_form, status: :unprocessable_entity
end
end
end
20. MADRID · NOV 18-19 · 2016
Refactor: Controller (2)
# app/controllers/users_controller.rb
class UsersController < ApplicationController
private
def user
@user ||= policy_scope(User).find_by!(nickname: params[:nickname])
end
def register_form
@register_form ||= Users::RegisterForm.new(params)
end
def user_create_service
@register_service ||= Users::RegisterService.new(register_form)
end
end
21. MADRID · NOV 18-19 · 2016
Refactor: Controller (3)
# app/controllers/users_controller.rb
class UsersController < ApplicationController
private
def profile_update_form
Users::ProfilepdateForm.new(params).tap do |form|
form.resource = user
end
end
def profile_update_service
@profile_update_service ||= Users::ProfilepdateService.new(
user,
profile_update_form
)
end
end
22. MADRID · NOV 18-19 · 2016
Refactor: Unit testing
●
Service/Form objects faciles de testear al no estar acoplados a Rails.
●
Muy rápidos.
●
Tests unitarios.
●
Mocks de la persistencia.
23. MADRID · NOV 18-19 · 2016
Unit testing: Form Object
# spec/unit/forms/users/create_form_spec.rb
describe Users::RegisterForm type: [:form, :unit] do
let(:form) { described_class.new(nickname: 'codemotion', # ...) }
describe '.valid?' do
context 'when nickname is not valid' do
it 'should return false if it is too short' do
form.nickname 'xx'
expect(form).not_to be_valid
expect(form.errors).to include(:nickname)
end
# ...
end
# ...
end
end
24. MADRID · NOV 18-19 · 2016
Unit testing: Controller
# spec/unit/controllers/users_controller_spec.rb
describe UsersController, type: [:controller, :unit] do
describe 'create an user' do
subject { process :create, method: :post, params: params }
before(:each) do
expect_any_instance_of(described_class).to receive(:register_service) { register_service_double }
expect_any_instance_of(described_class).to receive(:register_form) { register_form_double }
end
it 'return created with valid data' do
expect_any_instance_of(described_class).to receive(:authorize).with(User)
expect(subject).to have_http_status(:created)
end
end
end
25. MADRID · NOV 18-19 · 2016
Integration testing: Service Object
# spec/integration/services/users/create_service_spec.rb
describe Users::RegisterService type: [:service, :integration] do
let(:user_attributes) { attributes_for :user }
subject { described_class.new(Users::RegisterForm.new(user_attributes)) }
describe '.execute' do
it 'should create a new user' do
new_user = subject.execute
expect(new_user).to be_an_instance_of(User)
end
end
end
26. MADRID · NOV 18-19 · 2016
Refactor
●
¿Son necesarias todas estas clases y divisiones?
●
Hay que ser pragmatico ante todo.