Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

WHAT IS WRONG WITH RAILS?

Slide 4

Slide 4 text

SAME PATTERN

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

RAILSHURTS.COM/MESS

Slide 10

Slide 10 text

RAILS-WAY IS NOT ENOUGH

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

PRINCIPLES. MISUNDERSTOOD. APPLIED.

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

JUST AVOID DUPLICATION, RIGHT?

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

EVERYTHING HAS ITS PRICE

Slide 18

Slide 18 text

PRICE: MORE RELATIONS

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

RAILS IS SIMPLE, RIGHT?

Slide 22

Slide 22 text

ACTIVERECORD class User < ActiveRecord::Base end

Slide 23

Slide 23 text

ACTIVERECORD

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

- WHY SHOULD I CARE?

Slide 26

Slide 26 text

Polymorphic STI model which belongs to another polymorphic model through third model, which also has some valuable JSON data stored in Postgres using hstore.

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

IT IS GOING TO BE PAINFUL

Slide 29

Slide 29 text

PAINFUL BECAUSE OF THE COMPLEXITY

Slide 30

Slide 30 text

RAILS IS NOT SIMPLE. IT IS CONVENIENT.

Slide 31

Slide 31 text

FAT MODEL, SKINNY CONTROLLER RAILSHURTS.COM/_SKINNY

Slide 32

Slide 32 text

RIGHT PROBLEM IDENTIFIED

Slide 33

Slide 33 text

BUT IT IS ONLY PART OF THE PROBLEM

Slide 34

Slide 34 text

YOU SHOULD HAVE NO FAT CLASSES AT ALL

Slide 35

Slide 35 text

*HINT: PROPER SOLUTION REQUIRES THINKING OUT OF MVC BOX

Slide 36

Slide 36 text

RAILS IS NOT YOUR APPLICATION RAILSHURTS.COM/_APP

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

RAILSHURTS.COM/_CLEAN

Slide 41

Slide 41 text

HEXXAGONAL ARCHITECTURE RAILSHURTS.COM/_HEXX

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

WHAT IF YOUR APP DOESN'T NEED PERSISTANCE YET?

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

LET'S GET OUR HANDS DIRTY!

Slide 49

Slide 49 text

FEATURE #1: USERS REGISTRATION

Slide 50

Slide 50 text

FEATURE #1: USERS REGISTRATION

Slide 51

Slide 51 text

SINGLE TABLE INHERITANCE !   CONDITIONAL VALIDATIONS !

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

FORM OBJECT !   

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

FOUR SIMPLE OBJECTS INSTEAD OF ONE COMPLEX

Slide 62

Slide 62 text

FEATURE #2: BONUSCODE REDEEM

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

SINGLE RESPONSIBILITY PRINCIPLE

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

SERVICE OBJECT! (USE CASE, INTERACTOR)   

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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         

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

BUSINESS LOGIC / CONTROLLER SEPARATION

Slide 74

Slide 74 text

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