Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Stop being Rails developer

Stop being Rails developer

F9c1a378a1e3926ea1a58cf724140000?s=128

Ivan Nemytchenko

March 18, 2015
Tweet

Transcript

  1. WHY LIFE WITH RUBY ON RAILS BECOMES SO HARD AFTER

    FEW MONTHS OF DEVELOPMENT? IVAN NEMYTCHENKO
  2. IVAN NEMYTCHENKO > 14 years in IT > cofounded 2

    IT-companies > occasional speaker > coorganized 2 HappyDev conferences > worked in a startup > worked as project-manager > working with Rails since 2006 > author of RailsHurts.com > internship for ruby junior developers: SkillGrid > twitter: @inem
  3. WHAT IS WRONG WITH RAILS?

  4. SAME PATTERN

  5. None
  6. None
  7. None
  8. None
  9. RAILSHURTS.COM/MESS

  10. RAILS-WAY IS NOT ENOUGH

  11. RAILS WORLD is so cool that we don't even have

    whole class of problems millions of java programmers struggring every day
  12. THESE THINGS MIGHT HELP YOU > SOLID principles > Design

    Patterns > Refactoring techniques > Architecture types > Code smells identification > Best practices of testing
  13. PRINCIPLES. MISUNDERSTOOD. APPLIED.

  14. DRY «DON'T REPEAT YOURSELF» RAILSHURTS.COM/_DRY

  15. JUST AVOID DUPLICATION, RIGHT?

  16. None
  17. EVERYTHING HAS ITS PRICE

  18. PRICE: MORE RELATIONS

  19. «Duplication is far cheaper that wrong abstraction» — Sandi Metz

  20. KISS «KEEP IT SIMPLE, STUPID» RAILSHURTS.COM/_KISS

  21. RAILS IS SIMPLE, RIGHT?

  22. ACTIVERECORD class User < ActiveRecord::Base end

  23. ACTIVERECORD

  24. ACTIVERECORD > input data coercion > setting default values >

    input data validation > interaction with the database > handling nested structures > callbacks (before_save, etc...)
  25. - WHY SHOULD I CARE?

  26. Polymorphic STI model which belongs to another polymorphic model through

    third model, which also has some valuable JSON data stored in Postgres using hstore.
  27. WHAT ARE YOU GONNA DO? > reorganize associations > become

    Rails core contributor
  28. IT IS GOING TO BE PAINFUL

  29. PAINFUL BECAUSE OF THE COMPLEXITY

  30. RAILS IS NOT SIMPLE. IT IS CONVENIENT.

  31. FAT MODEL, SKINNY CONTROLLER RAILSHURTS.COM/_SKINNY

  32. RIGHT PROBLEM IDENTIFIED

  33. BUT IT IS ONLY PART OF THE PROBLEM

  34. YOU SHOULD HAVE NO FAT CLASSES AT ALL

  35. *HINT: PROPER SOLUTION REQUIRES THINKING OUT OF MVC BOX

  36. RAILS IS NOT YOUR APPLICATION RAILSHURTS.COM/_APP

  37. It just doesn't make sense. Here's my Rails application. Here's

    app folder. What's wrong?
  38. None
  39. None
  40. RAILSHURTS.COM/_CLEAN

  41. HEXXAGONAL ARCHITECTURE RAILSHURTS.COM/_HEXX

  42. YAGNI «YOU AREN'T GONNA NEED IT» RAILSHURTS.COM/_YAGNI

  43. WHAT IF YOUR APP DOESN'T NEED PERSISTANCE YET?

  44. WHAT IF YOUR APP CORE DOESN'T NEED WEB FRAMEWORK YET?

  45. «Question everything generally thought to be obvious» — Dieter Rams

  46. THREE RULES FOR DEVELOPERS WHO ARE NOT SURE OF ONE'S

    STRENGTH > Do not apply DRY principle too early > Remember about Single Responsibility Principle > Learn how to apply Design Patterns
  47. None
  48. LET'S GET OUR HANDS DIRTY!

  49. FEATURE #1: USERS REGISTRATION

  50. FEATURE #1: USERS REGISTRATION

  51. SINGLE TABLE INHERITANCE !   CONDITIONAL VALIDATIONS !

  52. BOTH STI AND CONDITIONAL VALIDATIONS ARE JUST WORKAROUNDS THE PROBLEM

    IS DEEPER!
  53. SINGE RESPONSIBILITY PRINCIPLE: A CLASS SHOULD HAVE ONLY ONE REASON

    TO CHANGE
  54. OUR MODEL KNOWS ABOUT HOW.. > admin form is validated

    > org_user form is validated > guest_user form is validated > user data is saved
  55. FORM OBJECT !   

  56. COOKING FORM OBJECT (STEP 1) class Person attr_accessor :first_name, :last_name

    def initialize(first_name, last_name) @first_name, @last_name = first_name, last_name end end
  57. COOKING FORM OBJECT (STEP 2) class Person attr_accessor :first_name, :last_name

    def initialize(first_name, last_name) @first_name, @last_name = first_name, last_name end include ActiveModel::Validations validates_presence_of :first_name, :last_name end
  58. COOKING FORM OBJECT (STEP 3) class Person include Virtus.model attribute

    :first_name, String attribute :last_name, String include ActiveModel::Validations validates_presence_of :first_name, :last_name end
  59. FORM OBJECT class OrgUserInput include Virtus.model include ActiveModel::Validations attribute :login,

    String attribute :password, String attribute :password_confirmation, String attribute :organization_id, Integer validates_presence_of :login, :password, :password_confirmation validates_numericality_of :organization_id end
  60. USING FORM OBJECT def create input = OrgUserInput.new(params) if input.valid?

    @user = User.create(input.to_hash) else #... end end
  61. FOUR SIMPLE OBJECTS INSTEAD OF ONE COMPLEX

  62. FEATURE #2: BONUSCODE REDEEM

  63. def redeem unless bonuscode = Bonuscode.find_by_hash(params[:code]) render json: {error: 'Bonuscode

    not found'}, status: 404 and return end if bonuscode.used? render json: {error: 'Bonuscode is already used'}, status: 404 and return end unless recipient = User.find_by_id(params[:receptor_id]) render json: {error: 'Recipient not found'}, status: 404 and return end ActiveRecord::Base.transaction do amount = bonuscode.mark_as_used!(params[:receptor_id]) recipient.increase_balance!(amount) if recipient.save && bonuscode.save render json: {balance: recipient.balance}, status: 200 else render json: {error: 'Error during transaction'}, status: 500 end end end
  64. None
  65. None
  66. SINGLE RESPONSIBILITY PRINCIPLE

  67. None
  68. bonuscode.redeem_by(user) OR user.redeem_bonus(code) ?

  69. SERVICE OBJECT! (USE CASE, INTERACTOR)   

  70. COOKING SERVICE OBJECT (STEP 1) class RedeemBonuscode def redeem unless

    bonuscode = Bonuscode.find_by_hash(params[:code]) render json: {error: 'Bonuscode not found'}, status: 404 and return end if bonuscode.used? render json: {error: 'Bonuscode is already used'}, status: 404 and return end unless recipient = User.find_by_id(params[:receptor_id]) render json: {error: 'Recipient not found'}, status: 404 and return end ActiveRecord::Base.transaction do amount = bonuscode.mark_as_used!(params[:receptor_id]) recipient.increase_balance!(amount) if recipient.save && bonuscode.save render json: {balance: recipient.balance}, status: 200 else render json: {error: 'Error during transaction'}, status: 500 end end end end
  71. COOKING SERVICE OBJECT (STEP 2) class RedeemBonuscode def run!(params) unless

    bonuscode = Bonuscode.find_by_hash(params[:code]) raise BonuscodeNotFound.new end if bonuscode.used? raise BonuscodeIsAlreadyUsed.new end unless recipient = User.find_by_id(params[:receptor_id]) raise RecipientNotFound.new end ActiveRecord::Base.transaction do amount = bonuscode.mark_as_used!(params[:receptor_id]) recipient.increase_balance!(amount) recipient.save! && bonuscode.save! end recipient.balance end end         
  72. COOKING SERVICE OBJECT (STEP 3) def redeem use_case = RedeemBonuscode.new

    begin recipient_balance = use_case.run!(params) rescue BonuscodeNotFound, BonuscodeIsAlreadyUsed, RecipientNotFound => ex render json: {error: ex.message}, status: 404 and return rescue TransactionError => ex render json: {error: ex.message}, status: 500 and return end render json: {balance: recipient_balance} end
  73. BUSINESS LOGIC / CONTROLLER SEPARATION

  74. 7 Patterns to Refactor Fat ActiveRecord Models railshurts.com/_7ways * *

    * Arkency railshurts.com/_arkency * * * Adam Hawkins railshurts.com/_hawkins