There Be Dragons: Rails Callbacks and Suppression

Article summary

After a long hiatus from Rails, I found myself working in a Rails codebase this week. Here at Atomic, our recent focus has been on the wins provided by our starter kit. I still love Ruby and Rails, but after digging through a well-intentioned codebase, I was reminded how much I dislike Rails magic callbacks.

In a mini-fit of rage, I went hunting on the internet for people who agree with me…usually not a very helpful course of action. As part of my googling, I came across some recent developer videos by @DHH, the creator of Rails.

Example

 
module Recording::Mentions
  extend ActiveSupport::Concern

  included do
    after_commit :eavesdrop_for_mentions, ...
  end

  private
  def eavesdrop_for_mentions
    #PerformLater ...
  end

end


class MessageController

  def create
    @recording = @bucket.recordings.new ...

    respond_to do |format|
    #...

  end

end

I’ve pulled in a snippet from the video that DHH claims as a victory for Rails. He claims that the fact that the message controller (and any other controller that works with Recordings) doesn’t know about the eavesdropping or the mentions is a good thing. As a Rails expert, with total and intimate knowledge of the entire code base, DHH may be right.

In the video, DHH claims that side effects are nice, and having them keeps things out of your way while you’re working on the main flow. He also pokes fun at the functional programming ideas of reducing side effects and pushing them to the edges of your system.

Wrong. Side effects and magic like this are how good developers get lost in their own code base.

Code that’s explicit and easy to read and reason about will always win out in my book. It allows new people to ramp in with less headaches and keeps everyone from being surprised by crazy, seemingly unrelated things that pop up.

Explicit Example

 

class MessageController

  def create
    @recording = MessageService.user_creates_message bucket: @bucket, ..
    respond_to do |format|
    #...
  end

end

class MessageService

  def user_creates_message(bucket:,params:)
    recording = RecordingRepository.create params
    MentionService.scan_for_and_send_notifications recording: recording, ...
  end

end

You can see and follow what the code is doing here. It’s easier to test, easier to ramp into, and represents a similar amount of code. So the Rails magic is just there to make you feel like a wizard, but it actually hurts the quality of your app’s code.

The magic is so bad that other code in DHH’s example deals with it via the new suppress method. This leads to spooky action-at-a-distance problems, and it violates single responsibility.

I love Rails, but the recommended way to work with callbacks is a terrible set of practices for building a real app.