Keep your ActiveRecord models manageable the Rails way

Keep your ActiveRecord models manageable the Rails way

Rails is awesome! It makes very easy and fun to start new projects. However, as your application grows, you will eventually need to come off the Rails. Otherwise your codebase will become completely unmanageable. Everyone knows that.

You'll need presenters and a service layer, including role and use-case objects. DCI will be great too or, alternatively, you can go Hexagonal. After all, the web is just a delivery mechanism, the database is a mere persistence strategy and, of course, “Rails is a detail”.

But… Wait a minute! Is all that really true? Does the Golden Path no longer work as your application becomes larger? How is it, then, that Rails claims to be “optimised for sustainable productivity”?

Criticising Rails is so last year! This time we'll revisit the patterns and conventions that Rails encourages and push them to the limit to see what happens. We'll seek examples of large Rails applications keeping their models manageable without derailing. We'll also discuss the trade-offs of running on and off the Rails. And, maybe, we'll finally learn how to stop worrying and love the Rails way!

8a8d65bb7cec6df5ee7383532145b400?s=128

Luismi Cavallé

June 07, 2013
Tweet

Transcript

  1. ,FFQ:PVS "DUJWF3FDPSE .PEFMT.BOBHFBCMF 5IF3BJMT8BZ CZ-VJTNJ$BWBMMÉ

  2. require_dependency,'topic_view' require_dependency,'rate_limiter' require_dependency,'text_sentinel' require_dependency,'text_cleaner' class,Topic,<,ActiveRecord::Base ,,include,ActionView::Helpers ,,include,RateLimiter::OnCreateRecord ,,def,self.max_sort_order ,,,,2**31,7,1 https://github.com/discourse/discourse/blob/master/app/models/topic.rb

  3. require_dependency,'slug' require_dependency,'avatar_lookup' require_dependency,'topic_view' require_dependency,'rate_limiter' require_dependency,'text_sentinel' require_dependency,'text_cleaner' class,Topic,<,ActiveRecord::Base ,,include,ActionView::Helpers ,,include,RateLimiter::OnCreateRecord ,,def,self.max_sort_order

    ,,,,2**31,7,1 ,,end ,,def,self.featured_users_count ,,,,4 ,,end ,,versioned,if:,:new_version_required? ,,acts_as_paranoid ,,after_recover,:update_flagged_posts_count ,,after_destroy,:update_flagged_posts_count ,,rate_limit,:default_rate_limiter ,,rate_limit,:limit_topics_per_day https://github.com/discourse/discourse/blob/master/app/models/topic.rb
  4. https://github.com/discourse/discourse/blob/master/app/models/topic.rb -0$ BOEUIBU`TOPUUPPNVDI

  5. #JH.PEFMT

  6. 'BU.PEFMT

  7. (JHBOUJD .PEFMT

  8. 8IZBSFUIFZCBE

  9. 431

  10. -PX$PIFTJPO

  11. 5JHIU$PVQMJOH

  12. #FDBVTF {{your_favourite_OO_guru}} TBZTTP

  13. 8IZBSFUIFZCBE JOQSBDUJDBMUFSNT  QMFBTF

  14. !!class!Article!<!ActiveRecord::Base !!!!attr_accessor!:moderation_option ! !!!!attr_accessible!:text,!:reply_to_i ! !!!!belongs_to!:topic !!!!belongs_to!:user,!!!!!:class_name! !!!!belongs_to!:reply_to,!:class_name!

  15. !!!belongs_to!:user,!!!!!:class_name!=>!Fore !!!belongs_to!:reply_to,!:class_name!=>!"Pos !!!has_many!:replies,!:class_name!!=>!"Post" !!!!!!!!!!!!!!!!!!!!!!:foreign_key!=>!"reply !!!!!!!!!!!!!!!!!!!!!!:dependent!!!=>!:nulli !!! !!!has_many!:taggings !!!has_many!:tags,!:through!=>!:taggings !!!validates!:text,!:presence!=>!true !!!delegate!:forum,!:to!=>!:topic

    !!!after_create!:set_topic_last_post_at !!!after_create!:skip_pending_review
  16. !!!!end !!!!def!pending_review !!!!!!where!:state!=>!'pending_review' !!!!end !!!!def!spam !!!!!!where!:state!=>!'spam' !!!!end !!!! !!!!def!tagged_with(tag) !!!!!!joins(:taggings!=>!:tags).where(:tags!=>!{!:name!=>!tag!})

    !!!!end !!!!def!visible !!!!!!joins(:topic).where(:forem_topics!=>!{!:hidden!=>!false!}) !!!!end !!end !! !!def!tag_names=(tag_list) !!!!assign_tag_list!tag_list !!end !!private
  17. !!!!!end !!!end !!! !!!def!tag_names=(tag_list) !!!!!assign_tag_list!tag_list !!!end !!!private !!!def!subscribe_replier !!!!!if!topic!&&!user

  18. ! !!!!def!blacklist_user !!!!!!user.update_attribute(:forem_state,!"spam")!if!user !!!!end !!!! !!!!def!assign_tag_list(tag_list) !!!!!!tag_names!=!tag_list.gsub(/\s+/,!"").split(",") !!!!!!existing!=!self.tags.map!{|t|!t.name!} !!!!!!(existing!@!tag_names).each!do!|name| !!!!!!!!self.tags.delete!Tag.find_by_name(name)

    !!!!!!end !!!!!!tag_names.each!do!|name| !!!!!!!!self.tags!<<!Tag.find_or_create_by_name(name) !!!!!!end! !!!!end !!end
  19. !!class!Article!<!ActiveRecord::Base !!!!attr_accessor!:moderation_option ! !!!!attr_accessible!:text,!:reply_to_id,!:tag_names ! !!!!belongs_to!:topic !!!!belongs_to!:user,!!!!!:class_name!=>!Forem.user_class.to_s !!!!belongs_to!:reply_to,!:class_name!=>!"Post" ! !!!!has_many!:replies,!:class_name!!=>!"Post",

    !!!!!!!!!!!!!!!!!!!!!!!:foreign_key!=>!"reply_to_id", !!!!!!!!!!!!!!!!!!!!!!!:dependent!!!=>!:nullify !!!! !!!!has_many!:taggings !!!!has_many!:tags,!:through!=>!:taggings ! !!!!validates!:text,!:presence!=>!true ! !!!!delegate!:forum,!:to!=>!:topic ! !!!!after_create!:set_topic_last_post_at !!!!after_create!:skip_pending_review !!!!after_save!:email_topic_subscribers ! !!!!class!<<!self !!!!!!def!by_created_at !!!!!!!!order!:created_at !!!!!!end ! !!!!!!def!pending_review !!!!!!!!where!:state!=>!'pending_review' !!!!!!end ! !!!!!!def!spam !!!!!!!!where!:state!=>!'spam' !!!!!!end !!!!!! !!!!!!def!tagged_with(tag) !!!!!!!!joins(:taggings!=>!:tags).where(:tags!=>!{!:name!=>!tag!}) !!!!!!end ! !!!!!!def!visible !!!!!!!!joins(:topic).where(:forem_topics!=>!{!:hidden!=>!false!}) !!!!!!end !!!!end !!!! !!!!def!tag_names=(tag_list) !!!!!!assign_tag_list!tag_list !!!!end ! !!!!private ! !!!!def!subscribe_replier !!!!!!if!topic!&&!user !!!!!!!!topic.subscribe_user(user.id) !!!!!!end !!!!end ! !!!!def!set_topic_last_post_at !!!!!!topic.update_attribute(:last_post_at,!created_at) !!!!end ! !!!!def!blacklist_user !!!!!!user.update_attribute(:forem_state,!"spam")!if!user !!!!end !!!! !!!!def!assign_tag_list(tag_list) !!!!!!tag_names!=!tag_list.gsub(/\s+/,!"").split(",") !!!!!!existing!=!self.tags.map!{|t|!t.name!} !!!!!!(existing!@!tag_names).each!do!|name| !!!!!!!!self.tags.delete!Tag.find_by_name(name) !!!!!!end !!!!!!tag_names.each!do!|name| !!!!!!!!self.tags!<<!Tag.find_or_create_by_name(name) !!!!!!end! !!!!end !!end
  20. before_validation after_validation before_save before_create after_create after_save after_commit

  21. 8IBUDBOXFEP BCPVUJU

  22. None
  23. 0QUJPO

  24. 3BJMT "DUJWF3FDPSE BOEUIFJSDPOWFOUJPOT BSFSVCCJTI

  25. #FDBVTF {{your_favourite_OO_guru}} TBZTTP

  26. None
  27. :PVOFFE B4FSWJDF-BZFS

  28. :PVOFFE %$*

  29. :PVOFFE )FYBHPOBM

  30. 0QUJPO

  31. I8 <38 Rails

  32. $PODFSOT

  33. 8IBU 8IZ )PX

  34. .JYJOT

  35. module!DogFort !!def!call_dog !!!!puts!"this!is!dog!" !!end end class!Dog !!include!DogFort end http://schneems.com/post/21380060358/concerned-about-code-reuse

  36. ActiveSupport::Concern

  37. module!M !!extend!ActiveSupport::Concern !!!! !!included!do !!!!scope!:disabled,!where(:disabled!=>!true) !!end !! !!module!ClassMethods !!!!#"class"methods !!end

    !!#"instance"methods end http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
  38. class!Article!<!ActiveRecord::Base !!include!Taggable

  39. module!Taggable !!extend!ActiveSupport::Concern !!included!do !!!!has_many!:taggings !!!!has_many!:tags,!through:!:taggings !!!!scope!:tagged_with,!B>(tag_name)!do !!!!!!joins(:tags).where(tags:!{!name:!tag_name!})! !!!!end !!end !!def!tag_list

    !!!!tags.pluck(:name).join(',!') !!end !!def!tag_list=(tag_list) !!!!assign_tag_list!tag_list !!end !!private !!def!assign_tag_list(tag_list) !!!!tag_names!=!tag_list.split(',').map(&:strip).uniq !!!!self.tags!=!tag_names.map!do!|tag_name|! !!!!!!Tag.where(name:!tag_name).first_or_initialize !!!!end !!end end
  40. 8IBU 8IZ )PX

  41. 5IFZIFMQUPLFFQ DPIFTJPOIJHI XIFOVTFEQSPQFSMZ

  42. module!Taggable !!extend!ActiveSupport::Concern !!included!do !!!!has_many!:taggings !!!!has_many!:tags,!through:!:taggings !!!!scope!:tagged_with,!B>(tag_name)!do !!!!!!joins(:tags).where(tags:!{!name:!tag_name!})! !!!!end !!end !!def!tag_list

    !!!!tags.pluck(:name).join(',!') !!end !!def!tag_list=(tag_list) !!!!assign_tag_list!tag_list !!end !!private !!def!assign_tag_list(tag_list) !!!!tag_names!=!tag_list.split(',').map(&:strip).uniq !!!!self.tags!=!tag_names.map!do!|tag_name|! !!!!!!Tag.where(name:!tag_name).first_or_initialize !!!!end !!end end
  43. %3:OFTT

  44. *OUSPEVDFMFTT JOEJSFDUJPOBOE DFSFNPOZUIBO PUIFSBCTUSBDUJPOT DPNQPTJUJPO EFDPSBUJPO

  45. l0⒏DJBMTPMVUJPOzUP UIF#JH.PEFMTJTTVF JO3BJMT

  46. https://github.com/rails/rails/commit/f6bbc3f582bfc16da3acc152c702b04102fcab81

  47. app/controllers/concerns app/models/concerns

  48. 8IBU 8IZ )PX

  49. 8IBU 8IZOPU )PX

  50. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

  51. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

  52. https://twitter.com/hakunin/statuses/113364211727482880

  53. Ruby Science The reference for writing fantastic Rails applications. https://learn.thoughtbot.com/products/13-ruby-science

  54. .JYJO *OIFSJUBODF JT B DPNNPO NFUIPE PG SFVTF JO PCKFDUPSJFOUFE

    TPGUXBSF 3VCZ TVQQPSUT TJOHMF JOIFSJUBODF VTJOH TVCDMBTTFT BOE NVMUJQMF JOIFSJUBODF VTJOH NJYJOT .JYJOT DBO CF VTFE UP QBDLBHF DPNNPO IFMQFST PS QSPWJEF B DPNNPO QVCMJD JOUFSGBDF )PXFWFS NJYJOT IBWF TPNF ESBXCBDLT t 5IFZ VTF UIF TBNF OBNFTQBDF BT DMBTTFT UIFZSF NJYFE JOUP XIJDI DBO DBVTF OBNJOH DPOnJDUT t "MUIPVHI UIFZ IBWF BDDFTT UP JOTUBODF WBSJBCMFT GSPN DMBTTFT UIFZSF NJYFE JOUP NJYJOT DBOU FBTJMZ BDDFQU JOJUJBMJ[FS BSHVNFOUT TP UIFZ DBOU IBWF UIFJS PXO TUBUF t 5IFZ JOnBUF UIF OVNCFS PG NFUIPET BWBJMBCMF JO B DMBTT t 5IFZSF OPU FBTZ UP BEE BOE SFNPWF BU SVOUJNF https://learn.thoughtbot.com/products/13-ruby-science https://learn.thoughtbot.com/products/13-ruby-science
  55. *OIFSJUBODF JT B DPNNPO NFUIPE PG SFVTF JO PCKFDUPSJFOUFE TPGUXBSF

    3VCZ TVQQPSUT TJOHMF JOIFSJUBODF VTJOH TVCDMBTTFT BOE NVMUJQMF JOIFSJUBODF VTJOH NJYJOT .JYJOT DBO CF VTFE UP QBDLBHF DPNNPO IFMQFST PS QSPWJEF B DPNNPO QVCMJD JOUFSGBDF )PXFWFS NJYJOT IBWF TPNF ESBXCBDLT t 5IFZ VTF UIF TBNF OBNFTQBDF BT DMBTTFT UIFZSF NJYFE JOUP XIJDI DBO DBVTF OBNJOH DPOnJDUT t "MUIPVHI UIFZ IBWF BDDFTT UP JOTUBODF WBSJBCMFT GSPN DMBTTFT UIFZSF NJYFE JOUP NJYJOT DBOU FBTJMZ BDDFQU JOJUJBMJ[FS BSHVNFOUT TP UIFZ DBOU IBWF UIFJS PXO TUBUF t 5IFZ JOnBUF UIF OVNCFS PG NFUIPET BWBJMBCMF JO B DMBTT t 5IFZSF OPU FBTZ UP BEE BOE SFNPWF BU SVOUJNF t 5IFZSF EJʺDVMU UP UFTU JO JTPMBUJPO TJODF UIFZ DBOU CF JOTUBOUJBUFE 4ZNQUPNT https://learn.thoughtbot.com/products/13-ruby-science https://learn.thoughtbot.com/products/13-ruby-science
  56. TVQQPSUT TJOHMF JOIFSJUBODF VTJOH TVCDMBTTFT BOE NVMUJQMF JOIFSJUBODF VTJOH NJYJOT

    .JYJOT DBO CF VTFE UP QBDLBHF DPNNPO IFMQFST PS QSPWJEF B DPNNPO QVCMJD JOUFSGBDF )PXFWFS NJYJOT IBWF TPNF ESBXCBDLT t 5IFZ VTF UIF TBNF OBNFTQBDF BT DMBTTFT UIFZSF NJYFE JOUP XIJDI DBO DBVTF OBNJOH DPOnJDUT t "MUIPVHI UIFZ IBWF BDDFTT UP JOTUBODF WBSJBCMFT GSPN DMBTTFT UIFZSF NJYFE JOUP NJYJOT DBOU FBTJMZ BDDFQU JOJUJBMJ[FS BSHVNFOUT TP UIFZ DBOU IBWF UIFJS PXO TUBUF t 5IFZ JOnBUF UIF OVNCFS PG NFUIPET BWBJMBCMF JO B DMBTT t 5IFZSF OPU FBTZ UP BEE BOE SFNPWF BU SVOUJNF t 5IFZSF EJʺDVMU UP UFTU JO JTPMBUJPO TJODF UIFZ DBOU CF JOTUBOUJBUFE 4ZNQUPNT t .FUIPET JO NJYJOT UIBU BDDFQU UIF TBNF QBSBNFUFST PWFS BOE PWFS https://learn.thoughtbot.com/products/13-ruby-science https://learn.thoughtbot.com/products/13-ruby-science
  57. http://blog.8thlight.com/eric-meyer/2012/11/16/composition-over-mixins.html

  58. http://blog.8thlight.com/eric-meyer/2012/11/16/composition-over-mixins.html

  59. http://blog.8thlight.com/eric-meyer/2012/11/16/composition-over-mixins.html

  60. http://blog.8thlight.com/eric-meyer/2012/11/16/composition-over-mixins.html

  61. %PO`UpYQSPCMFN KVTUNPWFJU .BOZNFUIPETSFTQPOTJCJMJUJFT /BNJOHDPOqJDUT 4IBSFETUBUF /PFYQMJDJUCPVOEBSJFT

  62. http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns

  63. http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns

  64. +BWB 3VCZ /VNCFSPGNFUIPETJO4USJOHDMBTT

  65. article.method(:tag_names).source_location #"=>"["./app/models/concerns/taggable.rb","23]

  66. &OPVHISPQFUP IBOHPVSTFMWFT

  67. 6TFWT"CVTF

  68. 8IBU 8IZ )PX

  69. $PNNPOQVSQPTF /PUCBTJDOBUVSFPGNPEFM $SPTTDVUUJOH %PNBJODPODFQU

  70. #"app/models/article.rb class!Article!<!ActiveRecord::Base !!include!Accessors !!include!Validations !!include!Associations !!include!Scopes !!#"... end

  71. #"app/models/article.rb class!Article!<!ActiveRecord::Base !!include!Taggable !!include!Searchable !!include!Movable !!include!Visible !!include!Trashable !!#"... end

  72. .PEFMT

  73. .PEFM

  74. .PEFM .PEFM .PEFM

  75. has_one

  76. 4DIFEVMJOH

  77. create_table!"schedulings"!do!|t| !!#"... !!t.integer!!"workflow_offset" !!t.string!!!"workflow_asset_url" !!t.text!!!!!"workflow_details" !!t.boolean!!"workflow_sent" !!t.string!!!"workflow_web_url" !!t.string!!!"workflow_template_url"

  78. class!Scheduling!<!ActiveRecord::Base !!#"... !!include!Workflow

  79. class!SchedulingsController!<!ApplicationController

  80. class!SchedulingsController!<!ApplicationController class!WorkflowsController!<!ApplicationController

  81. 4DIFEVMJOH

  82. 4DIFEVMJOH 8PSLqPX

  83. class!Workflow!<!ActiveRecord::Base !!#"... !!belongs_to!:scheduling

  84. class!Scheduling!<!ActiveRecord::Base !!#"... !!has_one!:workflow

  85. .BEFFYQMJDJUBO JNQMJDJUFOUJUZPGUIF EPNBJO

  86. 3FGBDUPSUPXBSETB EFFQFSJOTJHIU

  87. .PEFMTBSFTNBMMFS

  88. /POQFSTJTUFE .PEFMT

  89. ActiveModel

  90. 'PSNPCKFDUT

  91. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

  92. 1SPKFDUT $PNQBOZ 6TFST "1*5PLFOT

  93. 1SPKFDUT $PNQBOZ 6TFST 4JHOVQ "1*5PLFOT

  94. class!Signup !!extend!ActiveModel::N !!include!ActiveModel:: !!include!ActiveModel:: !!attr_accessor!:name http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

  95. History History class!Signup !!extend!ActiveModel::Naming !!include!ActiveModel::Conversion !!include!ActiveModel::Validations !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true

    http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  96. class!Signup !!extend!ActiveModel::Naming !!include!ActiveModel::Conversion !!include!ActiveModel::Validations !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!#"Virtual"models"are"never"themselves"

    !!def!persisted? http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  97. !!include!ActiveModel::Validations !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!#"Virtual"models"are"never"themselves"p !!def!persisted? !!!!false !!end

    !!def!save http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  98. !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!#"Virtual"models"are"never"themselves"persisted !!def!persisted? !!!!false !!end !!def!save

    !!!!if!valid? !!!!!!persist! !!!!!!true !!!!else !!!!!!false http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  99. !!def!persisted? !!!!false !!end !!def!save !!!!if!valid? !!!!!!persist! !!!!!!true !!!!else !!!!!!false !!!!end

    !!end private !!def!persist! http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  100. !!!!!!true !!!!else !!!!!!false !!!!end !!end private !!def!persist! !!!!@company!=!Company.create!(name:!company_name) !!!!@user!=!@company.users.create!(name:!name,!email:!email) !!end

    end class!SignupsController!<!ApplicationController !!def!create !!!!@signup!=!Signup.new(params[:signup]) !!!!if!@signup.save !!!!!!redirect_to!dashboard_path !!!!else !!!!!!render!"new" http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  101. class!Signup !!extend!ActiveModel::Naming !!include!ActiveModel::Conversion !!include!ActiveModel::Validations !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!#"Virtual"models"are"never"themselves"persisted

    !!def!persisted? !!!!false !!end !!def!save !!!!if!valid? !!!!!!persist! !!!!!!true !!!!else !!!!!!false !!!!end !!end private !!def!persist! !!!!@company!=!Company.create!(name:!company_name) !!!!@user!=!@company.users.create!(name:!name,!email:!email) !!end end class!SignupsController!<!ApplicationController !!def!create http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  102. class!SignupsController!<!ApplicationController !!def!create !!!!@signup!=!Signup.new(params[:signup]) !!!!if!@signup.save !!!!!!redirect_to!dashboard_path !!!!else !!!!!!render!"new" !!!!end !!end end

    http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  103. class!SimpleModel !!extend!ActiveModel::Naming !!include!ActiveModel::Conversion !!include!ActiveModel::Validations !!#"Virtual"models"are"never"themselves"persisted !!def!persisted? !!!!false !!end !!def!save !!!!if!valid?

    !!!!!!persist! !!!!!!true !!!!else !!!!!!false !!!!end !!end end
  104. class!Signup!<!SimpleModel !!attr_accessor!:name, !!88888888888888:company_name, !!88888888888888:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!def!persist! !!!!@company!=!Company.create!(name:!company_name) !!!!@user!=!@company.users.create!(name:!name,!email:!email) !!end

    end
  105. $POUSPMMFST

  106. :FT DPOUSPMMFST

  107. http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model

  108. 4LJOOJFTUQPTTJCMF DPOUSPMMFST

  109. class!CommentsController!<!ApplicationController !!#... !!def!create !!!!@comment!=!@post.comments.build(post_params) !!!!if!@comment.save !!!!!!redirect_to!@comment !!!!else !!!!!!render!action:!'new' !!!!end !!end

  110. https://gist.github.com/2838490#gistcomment-356060

  111. class!CommentsController!<!ApplicationController !!#... !! !!def!create !!!!@comment!=!@post.comments.create!(post_params) !!!! !!!!Notifications.new_comment(@comment).deliver !!!! !!!!TwitterPoster.new(current_user,!@comment.body).post !!!!FacebookPoster.new(current_user,!@comment.body).post

    !!end
  112. http://david.heinemeierhansson.com/2012/emails-are-views.html

  113. https://twitter.com/dhh/status/283832623023927296

  114. *U`TPLUPVTF DPOUSPMMFSTGPS PSDIFTUSBUJPO

  115. $POUSPMMFSTVTFEBT TFSWJDFPCKFDUT DPOUFYUT VTFDBTFT  TFSWJDFMBZFS

  116. "WPJEDBMMCBDLT

  117. 1SPQFSMZFODBQTVMBUF FYUFSOBMJOUFSGBDFT BTNBJMFSTBSF .PWFUIFMPHJDPVUPGUIF DPOUSPMMFSXIFOVTFEJO NVMUJQMFQMBDFT

  118. *OTVNNBSZ

  119. &YUSBDUUPDPODFSOT %JTDPWFSJNQMJDJUNPEFMT $PPSEJOBUFGSPNDPOUSPMMFST &ODBQTVMBUFFYUFSOBMJOUFSGBDFT

  120. 5IBOLT -VJTNJ$BWBMMÉ UXJUUFSDPNDBWBMMF HJUIVCDPNDBWBMMF QJOCPBSEJOVDBWBMMFUCJHNPEFMT  5FSJNB LBTJI ந"#