how to stop being rails developer

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

Upload: ivan-nemytchenko

Post on 16-Apr-2017

228 views

Category:

Internet


5 download

TRANSCRIPT

  • 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