-
-
Save mileer/1eba307f47b30cc4ccbe to your computer and use it in GitHub Desktop.
Revisions
-
zacstewart revised this gist
Dec 19, 2012 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -3,7 +3,7 @@ def create @friendship = Friendship.new(params[:friendship]) if @friendship.save Notifier.notify(notifiable: @friendship) redirect_to @friendship, notice: 'Your friend request has been sent' else render 'new' -
zacstewart revised this gist
Dec 10, 2012 . 1 changed file with 12 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -106,7 +106,7 @@ This effectively dismisses all the conditional logic previously handled by Occasionally, you need to create and persist more than one ActiveRecord model at once. For example creating a User and a new Blog for them upon signing up. The built-in solution for this is `accepts_nested_attributes_for`. This solution works, but _how_ it does has always seemed incredibly opaque to me. Furthermore, it isn't very flexible and becomes more confusing per each level of nested resources. Another tricky situation is when you want to create ActiveRecords in a way that @@ -132,6 +132,17 @@ Now you can create a simple ReblogsController to use it: <script src="https://gist.github.com/4210223.js?file=7-reposts_controller.rb"></script> As you can see, there are occasionally situations that can be addressed by leaning on some classical object-oriented patterns and RESTful practices beyond of Rails' core design. Remember: your database schema is not your application. That said, you don't want to use these techniques like gravy and make your app incomprehensible and err on the side of being too non-standard. Since applying these techniques I've noticed a marked decrease in those situations where I could build _something_ that got the job done but didn't feel right. I spend much less time deliberating over minutia and more time thinking about the larger goal. [1]: http://en.wikipedia.org/wiki/Don't_repeat_yourself [2]: http://books.google.com/books?id=XUaErakHsoAC&lpg=PA101&ots=5jgkGjqHrx&pg=PA101#v=onepage&q&f=false [3]: https://github.com/pluginaweek/state_machine -
zacstewart revised this gist
Dec 7, 2012 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -4,8 +4,8 @@ A while ago I hit a lull in my skill advancement as a Rails developer. I had long since learned to think of my resources as resources. I did my best to limit my controller actions the basic CRUD methods. I considered the "fat model, skinny controller" mantra to be sacrosanct. But I was still often finding myself going way out of my way to implement otherwise mundane features, for example sending mail after saving an object or adding a routine accept/reject to something. I realized that one simple assumption was holding me back: that there should be -
zacstewart revised this gist
Dec 6, 2012 . 1 changed file with 139 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,139 @@ # Breaking table–model–controller symmetry A while ago I hit a lull in my skill advancement as a Rails developer. I had long since learned to think of my resources as resources. I did my best to limit my controller actions the basic CRUD methods. I considered the "fat model, skinny controller" mantra to be sacrosanct. But I was still often finding myself going way out of my way to otherwise mundane features, for example sending mail after saving an object or adding a routine accept/reject to something. I realized that one simple assumption was holding me back: that there should be a one-to-one ratio of database tables, models and controllers. Discovering the power of additional controllers, decorators, service objects, form objects and other supplements to the standard models, view and controllers of Rails has been a boon to my productivity and I'd like to share a few patterns I've found useful. ## Extracting auxiliary objects Let's say you want to add a notification system to your application. You want users to have a notification area listing all their unread notifications and you want them to receive an email every time they get a new one. At first, it might be easy enough just to create the notification and then send mail from the controller, but as the events which cause a notification to be created grow, you find yourself writing that same few lines again and again. It's time to abstract that away. One way to do this would be to use an `after_create` hook on your Notification model to send mail. This would jive with the "fat model, skinny controller" principal, it even [DRY][1]s up your code, but that doesn't mean it's right. Using ActiveRecord callbacks to manipulate external objects can obfuscate intent and make testing difficult. If you abuse them, you can turn your application into confusing callback spaghetti. A clearer way to abstract this feature is to create a supporting object, i.e. a Notifier: <script src="https://gist.github.com/4210223.js?file=1-notifier.rb"></script> Here's how you could use it from a controller: <script src="https://gist.github.com/4210223.js?file=2-friendships_controller.rb"></script> An excellent post on this topic that I've drawn on heavily is [7 Patterns to Refactor Fat ActiveRecord Models][5] by Bryan Helmkamp. As a side note, if you're curious as to where to organize these bits, you can't really hurt yourself by creating another directory under _app/_. I often have directories like _decorators_ and _services_ there. ## Exposing additional resources Sometimes you want to perform a simple operation on a resource–for example, administrator approval of a blog comment. In terms of your model, all you want to do is flip an "approved" boolean on the comment. It can be temping to just add another action to your controller, route a POST request to it and call it a day. The authors of _RESTful Web Services_ refer to this as [overloaded POST][1]. You're essential trying to augment HTTP with a new method. In some cases, this may be appropriate but more often it's indicative of poor resource design. Another solution that a more REST-minded developer might come to is to utilize the HTTP PUT (or more correctly, PATCH) method and the `update` controller action. Rails makes this pretty easy by letting you include form parameters and specify the method of a hyperlink using `link_to`. This too is less than ideal, though. For one, you're going to end up with a lot of conditional logic within the `update` method. If you're using something like the [state_machine][3] gem you can end up with some funky looking code like this: <script src="https://gist.github.com/4210223.js?file=3-comments_controller.rb"></script> While seeming more "RESTful" at first, this may actually be worse than overloaded POST. You could call this overloaded PUT and while POST is allowed to be kind of a wildcard, PUT is expected to be idempotent: whether you do it once or a million times, the outcome should be the same. Approving a comment a million times is bordering on nonsensical. Willem van Bergen studied this pain point in his post [RESTful thinking considered harmful][4] and concluded that the best solution for these kind of transactional update operations is overloaded POST. He made the excellent observations that not all updates are equal, REST does not equal CRUD, and updating a resource does not always correspond to the UPDATE operation in a database–all concepts that Rails literature tends to conflate. However, I feel it's too early to throw in the towel and declare REST inadequate your problem space. A more appropriate solution is just to expose another resource, another _noun_. You don't have to _approve_ a comment, you can create an _approval_ for it. An approval doesn't have to map directly to a database table of approvals to be a valid resource, either. You can add a couple subordinate resources, "acceptance" and "rejection," to your comments resources in _routes.rb_: <script src="https://gist.github.com/4210223.js?file=4-routes.rb"></script> And add two controllers like this one: <script src="https://gist.github.com/4210223.js?file=5-acceptances_controller.rb"></script> This effectively dismisses all the conditional logic previously handled by `update`, or a least relegates it to a matter of routing. ## Objects for complex forms Occasionally, you need to create and persist more than one ActiveRecord model at once. For example creating a User and a new Blog for them upon signing up. The built-in solution for this is `accepts_nested_attributes_for`. This solution works, but it _how_ it does has always seemed incredibly opaque to me. Furthermore, it isn't very flexible and becomes more confusing per each level of nested resources. Another tricky situation is when you want to create ActiveRecords in a way that diverges from the typical CRUD actions. For example if you have a blog application similar to Tumblr that lets you _reblog_ a blog post. A reblog is essentially a duplicate of the original, potentially with some modification or addition to its attributes, for example adding a citation of original author and keeping a pointer back to the original. You could use the patterns explored in the previous section to expose a reblog resource for each blog, but in this case you'd probably end up with a pretty complicated ReblogsController. A simpler solution that can address both of these situations is to roll your own form object. Basically, all you need is something that quacks like an ActiveRecord model and can turn the parameters you provide it into the models that you need. To achieve that, you just need to mixin a few parts of ActiveModel. I also like to use a gem called Virtus to provide ActiveRecord-like attributes so that you can easily instantiate an object using the params attribute. <script src="https://gist.github.com/4210223.js?file=6-reblog.rb"></script> Now you can create a simple ReblogsController to use it: <script src="https://gist.github.com/4210223.js?file=7-reposts_controller.rb"></script> [1]: http://en.wikipedia.org/wiki/Don't_repeat_yourself [2]: http://books.google.com/books?id=XUaErakHsoAC&lpg=PA101&ots=5jgkGjqHrx&pg=PA101#v=onepage&q&f=false [3]: https://github.com/pluginaweek/state_machine [4]: http://www.shopify.com/technology/5898287-restful-thinking-considered-harmful [5]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 14 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ class RepostsController < ApplicationController def create @post = Post.find(params[:reblog][:post_id]) @reblog = Reblog.new(params[:reblog]) if @reblog.save flash[:success] = t('reblogs.rebloged_successfully') else flash[:error] = t('reblogs.reblog_failed') end redirect_to posts_path end end -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 43 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,43 @@ class Reblog extend ActiveModel::Naming extend CarrierWave::Mount include ActiveModel::Conversion include ActiveModel::Validations include Virtus attribute :user_id, Integer attribute :post_id, Integer validates :user_id, :post_id, presence: true def save if valid? persist! true else false end end def persisted? false end private def persist! @original = Post.find(post_id) @repost = Post.new( user_id: user_id, body: reblogged_body(@original), original_id: @original.id) @repost.save end def reblogged_body(original) <<-EOS #{original.body} via #{original.user.name} EOS end end -
zacstewart renamed this gist
Dec 5, 2012 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ class Notifier def initialize(notification) @notification = notification end -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,6 +2,7 @@ resources :posts do resources :comments do resource :acceptance, only: :create resource :rejection, only: :create end end end -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 13 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,13 @@ class AcceptancesController < ApplicationController def create @comment = Comment.find(params[:comment_id]) if @comment.accept flash[:success] = t('comments.accepted_successfully') else flash[:error] = t('comments.accept_failed') end redirect_to @comment.post end end -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 0 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,7 +2,6 @@ resources :posts do resources :comments do resource :acceptance, only: :create end end end -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 8 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,8 @@ Contrived::Application.routes.draw do resources :posts do resources :comments do resource :acceptance, only: :create resource :rejection, only: :create end end end -
zacstewart revised this gist
Dec 5, 2012 . 2 changed files with 0 additions and 30 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,18 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,12 +0,0 @@ -
zacstewart revised this gist
Dec 5, 2012 . 1 changed file with 20 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,20 @@ def update @comment = Comment.find(params[:id]) case params[:comment][:state] when 'accepted' if @comment.accept flash[:success] = t('comments.accepted_successfully') else flash[:error] = t('comments.accept_failed') end when 'rejected' if @comment.reject flash[:success] = t('comments.rejected_successfully') else flash[:error] = t('comments.reject_failed') end end redirect_to @comment.post end -
zacstewart revised this gist
Dec 5, 2012 . 2 changed files with 30 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,18 @@ class NotificationDeliverer def initialize(notification) @notification = notification end def self.notify(params) new(Notification.new(params)).save end def save @notification.save && deliver_email end private def deliver_email NotificationMailer.notification(@notification).deliver end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ class FriendshipController < ApplicationController def create @friendship = Friendship.new(params[:friendship]) if @friendship.save NotificationDeliverer.notify(notifiable: @friendship) redirect_to @friendship, notice: 'Your friend request has been sent' else render 'new' end end end -
zacstewart revised this gist
Dec 5, 2012 . 2 changed files with 16 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -3,6 +3,10 @@ def initialize(notification) @notification = notification end def self.notify(params) new(Notification.new(params)).save end def save @notification.save && deliver_email end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ class FriendshipController < ApplicationController def create @friendship = Friendship.new(params[:friendship]) if @friendship.save NotificationDeliverer.notify(notifiable: @friendship) redirect_to @friendship, notice: 'Your friend request has been sent' else render 'new' end end end -
zacstewart created this gist
Dec 4, 2012 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ class NotificationDeliverer def initialize(notification) @notification = notification end def save @notification.save && deliver_email end private def deliver_email NotificationMailer.notification(@notification).deliver end end