$30 off During Our Annual Pro Sale. View Details »

Rails hurts, because we're using it wrong. Let'...

Rails hurts, because we're using it wrong. Let's fix that!

Ivan Nemytchenko

June 04, 2018
Tweet

More Decks by Ivan Nemytchenko

Other Decks in Programming

Transcript

  1. ABOUT ME - first Rails project in 2006 - freelancing,

    agencies, conferences - former GitLab developer advocate - teaching junior developers at goodprogrammer.ru - online internships for rubyists, Skillgrid virtual agency - live in Serbia
  2. - 6 years old Rails project - 250 models ~

    100 associations in User model - user base: 165000 leaners - 1-2 active developers KIRILL MOKEVNIN CTO of hexlet.io
  3. IMPLEMENTATION class Person < ApplicationRecord validates_presence_of :username, :email, :address validates_confirmation_of

    :email validates_acceptance_of :terms_of_service geocoded_by :address before_save :geocode end
  4. IMPLEMENTATION class Person < ApplicationRecord attr_accessor :moderation_mode validates_presence_of :username, :email,

    :address validates_confirmation_of :email, on: :create validates_acceptance_of :terms_of_service, on: :create validates_presence_of :profession, :workplace if: :in_moderation_mode? geocoded_by :address before_save :geocode, on: :create before_validation :strip_and_downcase_username, on: :create before_validation :set_default_color_theme, on: :create def in_moderation_mode? !!moderation_mode end def strip_and_downcase_username; end def set_default_color_theme; end end
  5. IMPLEMENTATION class Person < ApplicationRecord attr_accessor :create_an_organization attr_accessor :moderation_mode belongs_to

    :organization #... after_save :create_default_organization if: :create_organization? def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end def in_moderation_mode? !!moderation_mode end def create_organization? !!create_an_organization end def strip_and_downcase_username; end def set_default_color_theme; end end
  6. class Person < ApplicationRecord attr_accessor :create_an_organization attr_accessor :moderation_mode belongs_to :organization

    validates_presence_of :username, :email, :address validates_confirmation_of :email, on: :create validates_acceptance_of :terms_of_service, on: :create validates_presence_of :profession, :workplace if: :in_moderation_mode? geocoded_by :address before_save :geocode, on: :create before_validation :strip_and_downcase_username, on: :create before_validation :set_default_color_theme, on: :create after_save :create_default_organization if: :create_organization? def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end def in_moderation_mode? !!moderation_mode end def create_organization? !!create_an_organization end def strip_and_downcase_username; end def set_default_color_theme; end end
  7. FOUR DIFFERENT SCENARIOS 1. User signs up 2. User updates

    profile 3. Moderator creates user 4. Moderator updates user
  8. FOUR DIFFERENT SCENARIOS strip_and_downcase_username x x set_default_color_theme x validates_presence_of(:username, :email,

    :address) x x x x validates_confirmation_of(:email) x x validates_acceptance_of(terms_of_service) x x validates_presence_of(:profession, :workspace) x x geocode x x save x x x x create_organization x x Create Update Moderation Create Update
  9. class Person < ApplicationRecord attr_accessor :create_an_organization attr_accessor :moderation_mode belongs_to :organization

    validates_presence_of :username, :email, :address validates_confirmation_of :email, on: :create validates_acceptance_of :terms_of_service, on: :create validates_presence_of :profession, :workplace if: :in_moderation_mode? geocoded_by :address before_save :geocode, on: :create before_validation :strip_and_downcase_username, on: :create before_validation :set_default_color_theme, on: :create after_save :create_default_organization if: :create_organization? def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end def in_moderation_mode? !!moderation_mode end def create_organization? !!create_an_organization end def strip_and_downcase_username; end def set_default_color_theme; end end FOUR DIFFERENT SCENARIOS
  10. PERSON.SAVE IN PSEUDOCODE def save_object strip_and_downcase_username if create? set_default_color_theme if

    create? && !in_moderation_mode? validate(:email, :tos) if create? validate(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end
  11. A LONER METHOD IN PERSON MODEL class Person < ApplicationRecord

    after_save :create_default_organization if: :create_organization? def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end
  12. class CreditRequest def initialize @age = 0 @sex = 0

    @income = 0 @credit_history = 0 @requested_amount = 0 @sum = 0 end def calculate #... end def result #... end end SIMPLE TASK
  13. class CreditRequest def initialize @age = 0 @sex = 0

    @income = 0 @credit_history = 0 @requested_amount = 0 @sum = 0 end def calculate #... end def result #... end end def calculate(age, sex, income, credit_history, requested_amount) sum = 0 if(age >= 21 && age <= 40) sum += 10 elsif(age > 40) sum += 20 end if(sex == "w") sum += 10 end if(income >= 20001 && income <= 40000) sum += 10 elsif(income > 40000) sum += 20 end if(credit_history == "y") sum += 20 end if(requested_amount < 20000) sum += 20 elsif(requested_amount > 20000 && requested_amount <= 40000) sum += 10 end @sum = sum end SIMPLE TASK
  14. class CreditRequest def initialize #... end def calculate(age, sex, income,

    credit_history, requested_amount) #… end def result if(@sum >= 50) puts "Credit request approved" else puts "Credit request declined" end end end SIMPLE TASK
  15. class CreditRequest def initialize #... end def calculate(age, sex, income,

    credit_history, requested_amount) #… end def result if(@sum >= 50) STDOUT.puts "Credit request approved" else STDOUT.puts "Credit request declined" end end end SIMPLE TASK
  16. SIMPLE TASK class CreditRequest def initialize #... end def calculate(age,

    sex, income, credit_history, requested_amount) #… end def result if(@sum >= 50) STDOUT.puts "Credit request approved" else STDOUT.puts "Credit request declined" end end end
  17. Service encapsulates single process of our business. They take all

    collaborators (database, logging, external adapters like Facebook, user parameters) and performs a given process. SERVICES
  18. CALL SERVICE FROM A MODEL class User < ApplicationRecord has_many

    :trainings def create_training(params) CreateTrainingService.call(params, self) end end
  19. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end
  20. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end APPLICATION-LOGIC VALIDATIONS
  21. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end INTERACTION WITH EXTERNAL SERVICE
  22. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end COMPLEX INTERACTION WITH OTHER MODEL(S)
  23. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end DATA FILTERING
  24. class Person < ApplicationRecord def save_object strip_and_downcase_username if create? set_default_color_theme

    if create? && !in_moderation_mode? validates_presence_of(:username, :email, :address) validates_confirmation_of(:email) if create? validates_acceptance_of(terms_of_service) if create? validates_presence_of(:profession, :workspace) if in_moderation_mode? geocode if create? save create_organization if create? && create_organization? end def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end end SETTING DEFAULT VALUE
  25. NOT A DOMAIN MODEL class Person < ApplicationRecord attr_accessor :create_an_organization

    attr_accessor :moderation_mode belongs_to :organization validates_presence_of :username, :email, :address validates_confirmation_of :email, on: :create validates_acceptance_of :terms_of_service, on: :create validates_presence_of :profession, :workplace if: :in_moderation_mode? geocoded_by :address before_save :geocode, on: :create before_validation :strip_and_downcase_username, on: :create before_validation :set_default_color_theme, on: :create after_save :create_default_organization if: :create_organization? def create_default_organization Organization.create(address: address, title: "#{username}'s organization", person: self) end def in_moderation_mode? !!moderation_mode end def create_organization? !!create_an_organization end def strip_and_downcase_username; end def set_default_color_theme; end end
  26. 1. EXTRACT SERVICE class Person < ApplicationRecord DEFAULT_COLOR = "#fcaa84"

    belongs_to :organization validates_presence_of :username, :email, :address end class PersonService def create(params) lat, lng = Geocoder.coordinates(params[:address]) username = params[:username].strip.downcase color = params[:color] || Person::DEFAULT_COLOR prepared_params = params.merge(lat: lat, lng: lng, color: color, username: username) person = Person.create(prepared_params) end end
  27. 2. EXTRACT FORM class PersonRegistrationForm < Person include ApplicationPhorm validates_confirmation_of

    :email validates_acceptance_of :terms_of_service end class PeopleController < ApplicationController def create p = PersonRegistrationForm.new(person_params) if p.valid? PersonService.create(person_params) redirect_to p else render :new end end end
  28. 3. HANDLE “CREATE ORGANIZATION” SCENARIO class PersonService def create(params) lat,

    long = Geocoder.coordinates(params[:address]) username = params[:username].strip.downcase color = params[:color] || Person::DEFAULT_COLOR prepared_params = params.merge(lat: lat, lng: long, color: color, username: username) person = Person.create(prepared_params).save! OrganizationMutator.create_deafult(person) if params[:create_an_organization] end end class OrganizationMutator def create_default(person) Organization.create(address: person.address, title: "#{person.username}'s organization", person: person) end end
  29. 3. INTRODUCE PERSON MUTATOR class PersonMutator def create(params) username =

    params[:username].strip.downcase color = params[:color] || Person::DEFAULT_COLOR prepared_params = params.merge(color: color, username: username) person = Person.create(prepared_params).save! person end end class PersonService def create(params) lat, lng = Geocoder.coordinates(params[:address]) person = PersonMutator.create(params.merge(lat: lat, lng: lng)).save! OrganizationMutator.create(person) if params[:create_an_organization] end end
  30. 4. UPDATE IS TRIVIAL class PeopleController < ApplicationController def update

    p = Person.find(params[:id]) if p.valid? p.update(person_params) redirect_to p else render :new end end end
  31. 5. MODERATION class ModerationPersonRegistrationForm < Person include ApplicationPhorm validates_confirmation_of :email

    validates_acceptance_of :terms_of_service validates_presence_of :profession, :workplace end def create p = ModerationPersonRegistrationForm.new(person_params) if p.valid? PersonService.create(person_params) redirect_to p else render :new end end def update p = ModerationPersonRegistrationForm.find(params[:id]) if p.valid? p.update(person_params) redirect_to p else render :new end end
  32. Models treat them as domain models, they contain relations and

    business rules Mutators to handle creation/editing/deletion logic (so that we always operate with objects in correct state) Services to handle business operations logic and interaction with external services Form objects application logic, use-case validation specific rules, defaults and filters THE PURPOSE OF LAYERS
  33. Lazy man CQRS queres - scopes, commands - mutators Flexibility

    in controllers use models, mutators or services Services and mutators are just functions! NICE THINGS ABOUT THIS ARCHITECTURE
  34. DHH: ``I’ve described this discovery of Ruby in the past

    as finding a magical glove that just fit my brain perfectly. Better than I had ever imagined any glove could ever fit`` rubyonrails.org/doctrine
  35. MAKE RAILS GREAT AGAIN! railshurts.com/book @railshurts, @inemation - GitLab stickers

    - GitLab power banks (show me your code) - I’ll be in Kiev until June 7