Invert Control

Ben and Joe follow up on a previous episode on extracting classes to demonstrate the classic followup punch to Extract Class: Invert Control. They show the previous example in the context of an integration-tested Rails application, and demonstrat...
This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/invert-control

Hands down, these are the best videos on the net right now for Rubyists and possibly coders in general. Huge huge huge thanks and mad props for Ben, Joe, and the team who make these.

keep em coming, we will keep watching and implementing!

3 Likes

I like this kind of idea to push dependencies up into one place. In this particular example, I think we could go further by asking a question why Sender has to know about the parser at all because what they need to know is only a collection of recipients to do their job, so I think it’s better Sender to accept recipients instead of parser:

class InvitationsController < ApplicationController
  def create
    recipients = Parser.new(params[:csv_file].read).recipients
    Sender.new(recipients, params[:message]).send
  end
end

class Sender
  def initialize(recipients, message)
    ...
  end
  ...
end

Before Sender has to know about parser on how to get recipients and use each recipient object in the collection, but by doing refactoring above we reduce responsibility of Sender to not even care how to get recipients from the parser at all.

4 Likes

I absolutely agree with your assessment here. Keep the chain as short as possible without exposing the left and right link where it doesn’t need to be.

Sender probably wasn’t the best name for this class. There might exist classes whose sole purpose is to wrap the interaction with the system:

class CommentOnIssue
  def run(issue_id, message)
    comment = Comment.new(issue_id: issue_id, message: message)
    comment.save

    comment.mentioned_user_names.each do |name|
      user = User.find_by_name name
      IssueMailer.mail_mentioned_in_comment(user, comment).deliver
    end
  end
end

vs

class CommentOnIssue < Struct.new(:database, :mailer)
  def run(issue_id, message)
    comment = Comment.new(issue_id: issue_id, message: message)
    database.create comment

    comment.mentioned_user_names.each do |name|
      user = database.find_user_by_name name
      mailer.mail_mentioned_in_comment(user, comment)
    end
  end
end

In the first example the abstract concept of the use case depends on the implementation details of how to persist things (using ActiveRecord) and how to mail (using ActionMailer). While the second example depends only on abstractions.

Thanks! It really means a lot to us, and you’ve just made my morning; happy friday!