How to stop being Rails Developer

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

Transcript of How to stop being Rails Developer

  • WHY LIFE WITH RUBY ON RAILS BECOMESSO HARD AFTER FEW MONTHS OF DEVELOPMENT?

    IVAN NEMYTCHENKO

  • 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

  • WHAT IS WRONG WITH RAILS?

  • SAME PATTERN

  • RAILSHURTS.COM/MESS

  • RAILS-WAY IS NOT ENOUGH

  • RAILS WORLD is so coolthat we don't even have whole class of problems

    millions of java programmersstruggring every day

  • THESE THINGS MIGHT HELP YOU

    > SOLID principles> Design Patterns

    > Refactoring techniques> Architecture types

    > Code smells identification> Best practices of testing

  • PRINCIPLES.MISUNDERSTOOD.

    APPLIED.

  • DRYDON'T REPEAT YOURSELF

    RAILSHURTS.COM/_DRY

  • JUST AVOID DUPLICATION, RIGHT?

  • EVERYTHING HAS ITS PRICE

  • PRICE: MORE RELATIONS

  • Duplication is far cheaper that wrong abstraction

    Sandi Metz

  • KISSKEEP IT SIMPLE, STUPID

    RAILSHURTS.COM/_KISS

  • RAILS IS SIMPLE, RIGHT?

  • ACTIVERECORDclass User < ActiveRecord::Base

    end

  • ACTIVERECORD

  • ACTIVERECORD

    > input data coercion> setting default values> input data validation

    > interaction with the database> handling nested structures

    > callbacks (before_save, etc...)

  • - WHY SHOULD I CARE?

  • Polymorphic STI model which belongs to another polymorphic model through third model, which also has

    some valuable JSON data stored in Postgres using hstore.

  • WHAT ARE YOU GONNA DO?> reorganize associations

    > become Rails core contributor

  • IT IS GOING TO BE PAINFUL

  • PAINFUL BECAUSE OF THE COMPLEXITY

  • RAILS IS NOT SIMPLE. IT IS CONVENIENT.

  • FAT MODEL,SKINNY CONTROLLER

    RAILSHURTS.COM/_SKINNY

  • RIGHT PROBLEM IDENTIFIED

  • BUT IT IS ONLY PART OF THE PROBLEM

  • YOU SHOULD HAVE NO FAT CLASSES AT ALL

  • *HINT:

    PROPER SOLUTIONREQUIRES THINKINGOUT OF MVC BOX

  • RAILSIS NOT YOURAPPLICATION

    RAILSHURTS.COM/_APP

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

    Here's app folder. What's wrong?

  • RAILSHURTS.COM/_CLEAN

  • HEXXAGONAL ARCHITECTURE

    RAILSHURTS.COM/_HEXX

  • YAGNIYOU AREN'T GONNA NEED IT

    RAILSHURTS.COM/_YAGNI

  • WHAT IF YOUR APPDOESN'T NEED PERSISTANCE YET?

  • WHAT IF YOUR APP COREDOESN'T NEED WEB FRAMEWORK YET?

  • Question everything generally thought to be obvious

    Dieter Rams

  • THREE RULES FOR DEVELOPERSWHO 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

  • LET'S GET OUR HANDS DIRTY!

  • FEATURE #1: USERS REGISTRATION

  • FEATURE #1: USERS REGISTRATION

  • SINGLE TABLE INHERITANCE !

    CONDITIONAL VALIDATIONS !

  • BOTH STI AND CONDITIONAL VALIDATIONS ARE JUST WORKAROUNDS

    THE PROBLEM IS DEEPER!

  • SINGE RESPONSIBILITY PRINCIPLE:

    A CLASS SHOULD HAVE ONLY ONE REASON TO

    CHANGE

  • OUR MODEL KNOWS ABOUT HOW..

    > admin form is validated> org_user form is validated

    > guest_user form is validated> user data is saved

  • FORM OBJECT !

  • 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 endend

  • 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_nameend

  • 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

  • FORM OBJECTclass 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_idend

  • USING FORM OBJECTdef create input = OrgUserInput.new(params) if input.valid? @user = User.create(input.to_hash) else #... endend

  • FOUR SIMPLE OBJECTSINSTEAD OF ONE COMPLEX

  • FEATURE #2: BONUSCODE REDEEM

  • 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

  • SINGLERESPONSIBILITY

    PRINCIPLE

  • bonuscode.redeem_by(user)

    ORuser.redeem_bonus(code)

    ?

  • SERVICE OBJECT!(USE CASE, INTERACTOR)

  • 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

  • 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

  • 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

  • BUSINESS LOGIC /CONTROLLER SEPARATION

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

    * * *Arkency

    railshurts.com/_arkency* * *

    Adam Hawkins railshurts.com/_hawkins