Skip to content

Instantly share code, notes, and snippets.

@mileer
Forked from zacstewart/0-post.md
Last active August 29, 2015 14:23
Show Gist options
  • Select an option

  • Save mileer/1eba307f47b30cc4ccbe to your computer and use it in GitHub Desktop.

Select an option

Save mileer/1eba307f47b30cc4ccbe to your computer and use it in GitHub Desktop.

Revisions

  1. @zacstewart zacstewart revised this gist Dec 19, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 2-friendships_controller.rb
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ def create
    @friendship = Friendship.new(params[:friendship])

    if @friendship.save
    NotificationDeliverer.notify(notifiable: @friendship)
    Notifier.notify(notifiable: @friendship)
    redirect_to @friendship, notice: 'Your friend request has been sent'
    else
    render 'new'
  2. @zacstewart zacstewart revised this gist Dec 10, 2012. 1 changed file with 12 additions and 1 deletion.
    13 changes: 12 additions & 1 deletion 0-post.md
    Original 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 it _how_ it does has always seemed incredibly opaque to me. Furthermore,
    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
  3. @zacstewart zacstewart revised this gist Dec 7, 2012. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions 0-post.md
    Original 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 otherwise mundane features, for
    example sending mail after saving an object or adding a routine accept/reject
    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
  4. @zacstewart zacstewart revised this gist Dec 6, 2012. 1 changed file with 139 additions and 0 deletions.
    139 changes: 139 additions & 0 deletions 0-post.md
    Original 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/
  5. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 14 additions and 0 deletions.
    14 changes: 14 additions & 0 deletions 7-reposts_controller.rb
    Original 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
  6. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 43 additions and 0 deletions.
    43 changes: 43 additions & 0 deletions 6-reblog.rb
    Original 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
  7. @zacstewart zacstewart renamed this gist Dec 5, 2012. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  8. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 1-notification_deliverer.rb
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    class NotificationDeliverer
    class Notifier
    def initialize(notification)
    @notification = notification
    end
  9. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion 4-routes.rb
    Original 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
    end
  10. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 13 additions and 0 deletions.
    13 changes: 13 additions & 0 deletions 5-acceptances_controller.rb
    Original 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
  11. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion 4-routes.rb
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,6 @@
    resources :posts do
    resources :comments do
    resource :acceptance, only: :create
    resource :rejection, only: :create
    end
    end
    end
  12. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions 4-routes.rb
    Original 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
  13. @zacstewart zacstewart revised this gist Dec 5, 2012. 2 changed files with 0 additions and 30 deletions.
    18 changes: 0 additions & 18 deletions #1 notification_deliverer.rb
    Original file line number Diff line number Diff line change
    @@ -1,18 +0,0 @@
    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
    12 changes: 0 additions & 12 deletions #2 friendships_controller.rb
    Original file line number Diff line number Diff line change
    @@ -1,12 +0,0 @@
    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
  14. @zacstewart zacstewart revised this gist Dec 5, 2012. 1 changed file with 20 additions and 0 deletions.
    20 changes: 20 additions & 0 deletions 3-comments_controller.rb
    Original 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
  15. @zacstewart zacstewart revised this gist Dec 5, 2012. 2 changed files with 30 additions and 0 deletions.
    18 changes: 18 additions & 0 deletions 1-notification_deliverer.rb
    Original 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
    12 changes: 12 additions & 0 deletions 2-friendships_controller.rb
    Original 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
  16. @zacstewart zacstewart revised this gist Dec 5, 2012. 2 changed files with 16 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions notification_deliverer.rb → #1 notification_deliverer.rb
    Original 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
    12 changes: 12 additions & 0 deletions #2 friendships_controller.rb
    Original 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
  17. @zacstewart zacstewart created this gist Dec 4, 2012.
    14 changes: 14 additions & 0 deletions notification_deliverer.rb
    Original 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