Objectify Your Forms:
Beyond Basic User Input
DannyOlson
dbolson@gmail.com
Corporate Sponsorship
Sharethrough
In-feed advertisingexchange.
Background
Forms are common in web applications
Forms often end up savingdatato multiple tables
Rails gives us #accepts_ne...
tl;dr
We don'talways have to do things The Rails Way™
Ice Cream
Memes
Online Meme-Based Ice Cream
Ordering Service
Class Diagram
First Implementation
The Rails Way™
classIceCreamsController<ApplicationController
defnew
@ice_cream=IceCream.new
respond_with@ice_cream
end
defcreate
@ice_cr...
=form_forice_creamdo|f|
-#otherfieldsomitted
-ifice_cream.persisted?
=f.fields_for:memesdo|fields|
=fields.label:name
=fie...
classIceCream<ActiveRecord::Base
accepts_nested_attributes_for:memes,
reject_if:proc{|attr|
attr['name'].blank?||attr['rat...
Responsibilities
classIceCream<ActiveRecord::Base
reject_if:proc{|attr|
attr['name'].blank?||attr['rating'].blank?
},
allo...
Responsibilities
classIceCream<ActiveRecord::Base
before_save:set_price
private
defset_price
self.price=scoops*100
end
Responsibilities
classIceCream<ActiveRecord::Base
deftopping_ids=(toppings)
filtered_toppings=toppings.reject{|t|!Topping....
Responsibilities
classIceCream<ActiveRecord::Base
validates:flavor_id,presence:true
validates:serving_size_id,presence:tru...
Single Responsibility Principle
Every class should have one, and only one,
reason to change.
1. formatdata
2. save the dat...
Concerning...
Early on, SRPis easy to apply. ActiveRecord
classes handle persistence, associations and not
much else. Butbit-by-bit, the...
#accepts_nested_attributes_foris
used, in ActiveRecord classes, to reduce the
amountof code in Rails applications needed t...
New Feature Request
We need to base the price off of both
the memes and the amountof scoops.
From this:
#app/models/ice_cream.rb
classIceCream<ActiveRecord::Base
defset_price
self.price=scoops*100
end
#app/controlle...
To this:
#app/models/ice_cream.rb
classIceCream<ActiveRecord::Base
defratings_sum
memes.reduce(0){|sum,meme|sum+=meme.rati...
There's Another Way
Form Object
An objectthatencapsulates context-specific
logic for user input.
Ithas onlythe attributes displayed in the for...
Second Implementation
With a Form Object
classNewOrderForm
includeVirtus.model
attribute:flavor_id,Integer
attribute:serving_size_id,Integer
attribute:scoops,Integ...
classNewOrderForm
extendActiveModel::Naming
includeActiveModel::Conversion
includeActiveModel::Validations
validates:flavo...
classNewOrderForm
defsave
ifvalid?
@model=OrderCreating.call(attributes)
true
else
false
end
end
classOrderCreating
defsel...
classEditOrderForm
attribute:memes,Array[EditMemeForm]
classEditMeme
attribute:id,Integer
attribute:name,String
attribute:...
classOrdersController<ApplicationController
defnew
@order=NewOrderForm.new
respond_with@order
end
defcreate
@order=NewOrde...
-iforder.persisted?
-order.memes.each_with_indexdo|meme,index|
-ifmeme.id
=hidden_field_tag"order[memes][][id]",meme.id
=l...
classIceCream<ActiveRecord::Base
belongs_to:flavor
belongs_to:serving_size
has_and_belongs_to_many:toppings
has_many:memes...
New Feature Request Redux
We need to base the price off of the meme ratings
notjustthe amountof scoops.
From this:
classOrderCreating
defself.call(attributes)
IceCream.transactiondo
ice_cream=IceCream.create!(flavor_id:flavor_...
To this:
classOrderCreating
defself.call(attributes)
IceCream.transactiondo
ice_cream=IceCream.create!(flavor_id:flavor_id...
Benefits
Clearer domain model
Less magic
Simpler to test
Onlyneed to consider the specific context
Easier to maintain and ...
When to Use a Form Object
A Rule (if you want one):
Use a form object when persisting
multiple ActiveRecord models
What's Out There?
redtape gem
activeform-rails gem
Thank You
Links
dbolson@gmail.com
https://github.com/dbolson/form-object-presentation
https://github.com/solnic/virtus
http://blog.c...
Objectify Your Forms: Beyond Basic User Input
Upcoming SlideShare
Loading in …5
×

Objectify Your Forms: Beyond Basic User Input

1,420 views

Published on

wroc_love.rb 2014 talk on form objects.

Published in: Technology
3 Comments
0 Likes
Statistics
Notes
  • Be the first to like this

No Downloads
Views
Total views
1,420
On SlideShare
0
From Embeds
0
Number of Embeds
439
Actions
Shares
0
Downloads
9
Comments
3
Likes
0
Embeds 0
No embeds

No notes for slide

Objectify Your Forms: Beyond Basic User Input

  1. 1. Objectify Your Forms: Beyond Basic User Input DannyOlson dbolson@gmail.com
  2. 2. Corporate Sponsorship Sharethrough In-feed advertisingexchange.
  3. 3. Background Forms are common in web applications Forms often end up savingdatato multiple tables Rails gives us #accepts_nested_attributes_for Much magic and potentialconfusion
  4. 4. tl;dr We don'talways have to do things The Rails Way™
  5. 5. Ice Cream
  6. 6. Memes
  7. 7. Online Meme-Based Ice Cream Ordering Service
  8. 8. Class Diagram
  9. 9. First Implementation The Rails Way™
  10. 10. classIceCreamsController<ApplicationController defnew @ice_cream=IceCream.new respond_with@ice_cream end defcreate @ice_cream=IceCream.new(valid_params) if@ice_cream.save Meme.create_defaults(@ice_cream) redirect_toedit_ice_cream_path(@ice_cream), notice:'Icecreamwascreated.' else render:new end end
  11. 11. =form_forice_creamdo|f| -#otherfieldsomitted -ifice_cream.persisted? =f.fields_for:memesdo|fields| =fields.label:name =fields.text_field:name =fields.label:rating =fields.select:rating, options_for_select(10.downto(1),fields.object.rating) -iffields.object.persisted? =fields.label:_destroy,'Delete' =fields.check_box:_destroy =f.submit'MakeItSo(Delicious)'
  12. 12. classIceCream<ActiveRecord::Base accepts_nested_attributes_for:memes, reject_if:proc{|attr| attr['name'].blank?||attr['rating'].blank? }, allow_destroy:true validates:flavor_id,:serving_size_id,presence:true validates:scoops,presence:true,inclusion:{in:[1,2,3]} validate:more_scoops_than_toppings before_save:set_price deftopping_ids=(toppings) filtered_toppings=toppings.reject{|t|!Topping.exists?(t)} super(filtered_toppings) end private defmore_scoops_than_toppings ifscoops.to_i<toppings.size errors.add(:toppings,"can'tbemorethanscoops") end end defset_price self.price=scoops*100 end
  13. 13. Responsibilities classIceCream<ActiveRecord::Base reject_if:proc{|attr| attr['name'].blank?||attr['rating'].blank? }, allow_destroy:true
  14. 14. Responsibilities classIceCream<ActiveRecord::Base before_save:set_price private defset_price self.price=scoops*100 end
  15. 15. Responsibilities classIceCream<ActiveRecord::Base deftopping_ids=(toppings) filtered_toppings=toppings.reject{|t|!Topping.exists?(t)} super(filtered_toppings) end
  16. 16. Responsibilities classIceCream<ActiveRecord::Base validates:flavor_id,presence:true validates:serving_size_id,presence:true validates:scoops,presence:true,inclusion:{in:[1,2,3]} validate:more_scoops_than_toppings private defmore_scoops_than_toppings ifscoops.to_i<toppings.size errors.add(:toppings,"can'tbemorethanscoops") end end
  17. 17. Single Responsibility Principle Every class should have one, and only one, reason to change. 1. formatdata 2. save the data 3. check values of associated objects 4. validations
  18. 18. Concerning...
  19. 19. Early on, SRPis easy to apply. ActiveRecord classes handle persistence, associations and not much else. Butbit-by-bit, they grow. Objects thatare inherently responsible for persistence become the de facto owner of all business logic as well. And ayear or two later you have aUser class with over 500 lines of code, and hundreds of methods in it’s public interface. - 7 Patterns to Refactor FatActiveRecord Models
  20. 20. #accepts_nested_attributes_foris used, in ActiveRecord classes, to reduce the amountof code in Rails applications needed to create/update records across multiple tables with asingle HTTPPOST/PUT. As with many things Rails, this is convention-driven... While this sometimes results in less code, itoften results in brittle code. - #accepts_nested_attributes_for (Often) Considered Harmful
  21. 21. New Feature Request We need to base the price off of both the memes and the amountof scoops.
  22. 22. From this: #app/models/ice_cream.rb classIceCream<ActiveRecord::Base defset_price self.price=scoops*100 end #app/controllers/ice_creams_controller.rb classIceCreamsController<ApplicationController defcreate @ice_cream=IceCream.new(valid_params) if@ice_cream.save Meme.create_defaults(@ice_cream) redirect_toedit_ice_cream_path(@ice_cream), notice:'Icecreamwascreated.' else render:new end end
  23. 23. To this: #app/models/ice_cream.rb classIceCream<ActiveRecord::Base defratings_sum memes.reduce(0){|sum,meme|sum+=meme.rating} end defset_price unlessprice_changed? self.price=scoops*100 end end #app/controllers/ice_creams_controller.rb classIceCreamsController<ApplicationController defcreate @ice_cream=IceCream.new(valid_params) if@ice_cream.save Meme.create_defaults(@ice_cream) meme_ratings=@ice_cream.ratings_sum @ice_cream.update_attributes!({ price:@ice_cream.price+meme_ratings }) redirect_toedit_ice_cream_path(@ice_cream), notice:'Icecreamwascreated.' else render:new end end
  24. 24. There's Another Way
  25. 25. Form Object An objectthatencapsulates context-specific logic for user input. Ithas onlythe attributes displayed in the form Itsets up its own data Itvalidates thatdata Itdelegates to persistence butdoesn'tknow specifics
  26. 26. Second Implementation With a Form Object
  27. 27. classNewOrderForm includeVirtus.model attribute:flavor_id,Integer attribute:serving_size_id,Integer attribute:scoops,Integer attribute:topping_ids,Array[Integer] end
  28. 28. classNewOrderForm extendActiveModel::Naming includeActiveModel::Conversion includeActiveModel::Validations validates:flavor_id,:serving_size_id,presence:true validates:scoops,presence:true,inclusion:{in:[1,2,3]} validate:more_scoops_than_toppings private defmore_scoops_than_toppings ifscoops.to_i<topping_ids.delete_if{|attr|attr==''}.size errors.add(:topping_ids,"can'tbemorethanscoops") end end
  29. 29. classNewOrderForm defsave ifvalid? @model=OrderCreating.call(attributes) true else false end end classOrderCreating defself.call(attributes) IceCream.transactiondo ice_cream=IceCream.create!(flavor_id:flavor_id, serving_size_id:serving_size_id, topping_ids:topping_ids, scoops:scoops, price:scoops*100) Meme.create_defaults(ice_cream) ice_cream end end end
  30. 30. classEditOrderForm attribute:memes,Array[EditMemeForm] classEditMeme attribute:id,Integer attribute:name,String attribute:rating,Integer attribute:_destroy,Boolean,default:false validates:name,presence:true validates:rating, presence:true, inclusion:{in:1..10,message:'mustbebetween1and10'} end
  31. 31. classOrdersController<ApplicationController defnew @order=NewOrderForm.new respond_with@order end defcreate @order=NewOrderForm.new(valid_params) if@order.save redirect_toedit_order_path(@order), notice:'Yourorderwascreated.' else render:new end end end
  32. 32. -iforder.persisted? -order.memes.each_with_indexdo|meme,index| -ifmeme.id =hidden_field_tag"order[memes][][id]",meme.id =label_tag"order[memes][][name]",'Name' =text_field_tag"order[memes][][name]",meme.name =label_tag"order[memes][][rating]",'Rating' =select_tag"order[memes][][rating]",meme_rating_options -ifmeme.id =label_tag"memes_destroy_#{index}"do =check_box_tag"order[memes][][_destroy]" Delete
  33. 33. classIceCream<ActiveRecord::Base belongs_to:flavor belongs_to:serving_size has_and_belongs_to_many:toppings has_many:memes,dependent::destroy end
  34. 34. New Feature Request Redux We need to base the price off of the meme ratings notjustthe amountof scoops.
  35. 35. From this: classOrderCreating defself.call(attributes) IceCream.transactiondo ice_cream=IceCream.create!(flavor_id:flavor_id, serving_size_id:serving_size_id, topping_ids:topping_ids, scoops:scoops, price:scoops*100) Meme.create_defaults(ice_cream) ice_cream end end end
  36. 36. To this: classOrderCreating defself.call(attributes) IceCream.transactiondo ice_cream=IceCream.create!(flavor_id:flavor_id, serving_size_id:serving_size_id, topping_ids:topping_ids, scoops:scoops, price:scoops*100) Meme.create_defaults(ice_cream) IceCreamPriceUpdating.call(ice_cream) end end end classIceCreamPriceUpdating defself.call meme_ratings=ice_cream.memes.reduce(0){|sum,meme| sum+=meme.rating } ice_cream.update_attributes!(price:ice_cream.price+meme_ratings) ice_cream end
  37. 37. Benefits Clearer domain model Less magic Simpler to test Onlyneed to consider the specific context Easier to maintain and change
  38. 38. When to Use a Form Object A Rule (if you want one): Use a form object when persisting multiple ActiveRecord models
  39. 39. What's Out There? redtape gem activeform-rails gem
  40. 40. Thank You
  41. 41. Links dbolson@gmail.com https://github.com/dbolson/form-object-presentation https://github.com/solnic/virtus http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat- activerecord-models http://evan.tiggerpalace.com/articles/2012/11/07/accepts_nested_attributes_for- often-considered-harmful/ https://github.com/ClearFit/redtape https://github.com/GCorbel/activeform-rails

×