Post on 16-Apr-2017
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