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

Stop being Rails developer

Stop being Rails developer

Ivan Nemytchenko

March 18, 2015
Tweet

More Decks by Ivan Nemytchenko

Other Decks in Programming

Transcript

  1. WHY LIFE WITH RUBY ON RAILS BECOMES
    SO HARD AFTER FEW MONTHS OF DEVELOPMENT?
    IVAN NEMYTCHENKO

    View Slide

  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

    View Slide

  3. WHAT IS WRONG
    WITH RAILS?

    View Slide

  4. SAME PATTERN

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. RAILSHURTS.COM/MESS

    View Slide

  10. RAILS-WAY IS NOT
    ENOUGH

    View Slide

  11. RAILS WORLD is so cool
    that we don't even have whole class of problems
    millions of java programmers
    struggring every day

    View Slide

  12. THESE THINGS MIGHT HELP YOU
    > SOLID principles
    > Design Patterns
    > Refactoring techniques
    > Architecture types
    > Code smells identification
    > Best practices of testing

    View Slide

  13. PRINCIPLES.
    MISUNDERSTOOD.
    APPLIED.

    View Slide

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

    View Slide

  15. JUST AVOID DUPLICATION, RIGHT?

    View Slide

  16. View Slide

  17. EVERYTHING
    HAS ITS PRICE

    View Slide

  18. PRICE: MORE RELATIONS

    View Slide

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

    View Slide

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

    View Slide

  21. RAILS IS SIMPLE, RIGHT?

    View Slide

  22. ACTIVERECORD
    class User < ActiveRecord::Base
    end

    View Slide

  23. ACTIVERECORD

    View Slide

  24. ACTIVERECORD
    > input data coercion
    > setting default values
    > input data validation
    > interaction with the database
    > handling nested structures
    > callbacks (before_save, etc...)

    View Slide

  25. - WHY SHOULD I CARE?

    View Slide

  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.

    View Slide

  27. WHAT ARE YOU GONNA DO?
    > reorganize associations
    > become Rails core contributor

    View Slide

  28. IT IS GOING TO
    BE PAINFUL

    View Slide

  29. PAINFUL BECAUSE OF THE
    COMPLEXITY

    View Slide

  30. RAILS IS NOT SIMPLE. IT IS CONVENIENT.

    View Slide

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

    View Slide

  32. RIGHT PROBLEM IDENTIFIED

    View Slide

  33. BUT IT IS ONLY PART OF THE PROBLEM

    View Slide

  34. YOU SHOULD HAVE NO FAT CLASSES AT ALL

    View Slide

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

    View Slide

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

    View Slide

  37. It just doesn't make sense.
    Here's my Rails application.
    Here's app folder. What's wrong?

    View Slide

  38. View Slide

  39. View Slide

  40. RAILSHURTS.COM/_CLEAN

    View Slide

  41. HEXXAGONAL ARCHITECTURE
    RAILSHURTS.COM/_HEXX

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  47. View Slide

  48. LET'S GET OUR HANDS DIRTY!

    View Slide

  49. FEATURE #1: USERS REGISTRATION

    View Slide

  50. FEATURE #1: USERS REGISTRATION

    View Slide

  51. SINGLE TABLE
    INHERITANCE !

    CONDITIONAL
    VALIDATIONS !

    View Slide

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

    View Slide

  53. SINGE RESPONSIBILITY PRINCIPLE:
    A CLASS SHOULD HAVE
    ONLY ONE REASON TO
    CHANGE

    View Slide

  54. OUR MODEL KNOWS ABOUT HOW..
    > admin form is validated
    > org_user form is validated
    > guest_user form is validated
    > user data is saved

    View Slide

  55. FORM OBJECT !
      

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  60. USING FORM OBJECT
    def create
    input = OrgUserInput.new(params)
    if input.valid?
    @user = User.create(input.to_hash)
    else
    #...
    end
    end

    View Slide

  61. FOUR SIMPLE OBJECTS
    INSTEAD OF ONE COMPLEX

    View Slide

  62. FEATURE #2: BONUSCODE REDEEM

    View Slide

  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

    View Slide

  64. View Slide

  65. View Slide

  66. SINGLE
    RESPONSIBILITY
    PRINCIPLE

    View Slide

  67. View Slide

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

    View Slide

  69. SERVICE OBJECT!
    (USE CASE, INTERACTOR)
      

    View Slide

  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

    View Slide

  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
      
      
      

    View Slide

  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

    View Slide

  73. BUSINESS LOGIC /
    CONTROLLER SEPARATION

    View Slide

  74. 7 Patterns to Refactor Fat ActiveRecord Models
    railshurts.com/_7ways
    * * *
    Arkency
    railshurts.com/_arkency
    * * *
    Adam Hawkins
    railshurts.com/_hawkins

    View Slide