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

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

F9c1a378a1e3926ea1a58cf724140000?s=128

Ivan Nemytchenko

June 04, 2018
Tweet

Transcript

  1. RAILS HURTS Ivan Nemytchenko @inemation railshurts.com LET’S FIX THAT!

  2. 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
  3. RAILSHURTS.COM/CURRENT_STATE

  4. RAILSHURTS.COM

  5. THE LIFECYCLE OF RAILS DEVELOPER

  6. PHASE 1 - RAILS WAY DISCIPLE RailsHurts.com/_lifecycle

  7. RailsHurts.com/_lifecycle

  8. RailsHurts.com/_lifecycle

  9. RailsHurts.com/_lifecycle

  10. - 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
  11. None
  12. None
  13. None
  14. THE RAILS WAY LET’S INVESTIGATE

  15. REQUIREMENTS

  16. 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
  17. REQUIREMENTS 1. Edit profile 2. Downcase username 3. Set default

    color 4. Moderation mode
  18. 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
  19. REQUIREMENTS 1. Add “create organization” checkbox

  20. 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
  21. 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
  22. FOUR DIFFERENT SCENARIOS 1. User signs up 2. User updates

    profile 3. Moderator creates user 4. Moderator updates user
  23. 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
  24. 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
  25. CODECLIMATE EXPERIMENT RailsHurts.com/_cce

  26. 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
  27. CODECLIMATE EXPERIMENT

  28. RailsHurts.com/_osm

  29. OUR DECISION MAKING PROCESS (picture idea by @ryanbigg)

  30. 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
  31. A CROWD OF LONER METHODS

  32. (picture by @benorama)

  33. TEXT

  34. TEXT

  35. TEXT

  36. TEXT

  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. LAYERED ARCHITECTURE RailsHurts.com/_mfs

  43. LAYERED ARCHITECTURE

  44. 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
  45. KNOWLEDGE DIRECTION

  46. KNOWLEDGE DIRECTION

  47. CALL SERVICE FROM A MODEL class User < ApplicationRecord has_many

    :trainings def create_training(params) CreateTrainingService.call(params, self) end end
  48. GEMS DO THAT TOO

  49. CURRENT USER IN MODELS

  50. MODULARITY MODULARITY IS THE LACK OF CIRCULAR DEPENDENCIES

  51. 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
  52. 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
  53. APPLICATION LOGIC VS BUSINESS LOGIC

  54. 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
  55. 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)
  56. 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
  57. 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
  58. DOMAIN MODELS LET’S TALK ABOUT

  59. None
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. LAYERED ARCHITECTURE

  68. 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
  69. TEXT

  70. TEXT

  71. 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
  72. None
  73. TEXT

  74. FINAL PART

  75. None
  76. TEXT

  77. TEXT

  78. TEXT

  79. 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
  80. TEXT

  81. RUBY GOTO STATEMENT

  82. Rails needs a stronger voice of the community!

  83. 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