headshot of Chris Tonkinson
Chris Tonkinson

tech {enthusiast, practitioner, leader, podcaster, mentor, entrepreneur}

HomeAboutTags



Refactored Podcast

I co-host Refactored, a casual, ongoing conversation between two technology leaders trying to suck a little less every day.

Career Schema Project

I founded the Career Schema Project with the mission of developing open standards and technology to reduce the hassle of online jobseeking.


RSS Subscribe

© 2021 Chris Tonkinson

DRYer strong_parameters in Rails


This post documents a very simple pattern for DRYing up strong_parameters knowledge to make your Rails controllers cleaner and virtually maintenance free (at least in the face of model attribute changes).

Starting point

A typical Rails app will have a model with some attributes.

class Window < ActiveRecord::Base
  # Window has attributes :manufacturer, :thickness, and :glass_type
end

Following the conventional pattern, the WindowsController will probably have a private window_params method to whitelist the attributes allowed by Rails (v4 and up) for mass assigmnent.

class WindowsController < ApplicationController

  private
    def window_params
      params.require(:window).permit(:manufacturer, :thickness, :glass_type)
    end

end

If later you add an attribute to Window, say, :num_panes, you’ll need to append it to your permitted attributes.

class WindowsController < ApplicationController

  private
    def window_params
      params.require(:window).permit(:manufacturer, :thickness, :glass_type, :num_panes)
    end

end

Adding complexity

There are two latent problems (not problems per se, but… perhaps inconveniences?) with this approach.

First, the diff for your commit will include a patch to app/controllers/windows_controller.rb when in reality it’s very likely you made no substantial changes to that file. Mechanically you DID alter the code there, but there was no logical modification (in the abstract). The point here is minor - perhaps even philosophical - but it makes for unnecessarily noisy commits, which makes for more difficult debugging later.

Second, suppose your app had a House concept.

class House < ActiveRecord::Base
  has_many :windows
  accepts_nested_attributes_for :windows
end

Which had the same whitelisting in its controller.

class HousesController < ApplicationController

  private
    def house_params
      params.require(:house).permit(
        :address, :color, :value,
        window_attributes: [:manufacturer, :thickness, :glass_type],
      )
    end

end

Oops! Not only are we duplicating knowledge (the list of window_attributes), but we already forgot to add a reference to :num_panes in HousesController#house_params. No bueno.

DRY out this knowledge

There’s a quick and easy solution, which is to declare a class method on each model returning an array of whitelisted attributes for that model.

class Window < ActiveRecord::Base
  # Window has attributes :manufacturer, :thickness, :glass_type, and :num_panes

  def self.window_params
    [:manufacturer, :thickness, :glass_type, :num_panes]
  end
end

Now our WindowsController can be cleaned up.

class WindowsController < ApplicationController

  private
    def window_params
      params.require(:window).permit(*Window.window_params)
    end

end

As can our HousesController.

class HousesController < ApplicationController

  private
    def house_params
      params.require(:house).permit(
        :address, :color, :value,
        window_attributes: Window.window_params,
      )
    end

end

And neither needs to be changed as we modify information in our Window model. If we apply the same technique to the attributes of House as well we can remove even more knowledge that doesn’t belong from that controller.

class HousesController < ApplicationController

  private
    def house_params
      params.require(:house).permit(
        *House.house_params,
        window_attributes: Window.window_params,
      )
    end

end

Adding [more] complexity

Suppose we now introduce a Vehicle class. We decide, in our infinite wisdom, that we’ll share code using a concern. We can reuse the same pattern to keep our controllers tight.

module Windowable
  extend ActiveSupport::Concern

  included do
    has_many :windows
    accepts_nested_attributes_for :windows

    def self.windowable_params
      { window_attributes: Window.window_params }
    end
  end
end

With all of our models using this pattern, our final HousesController is completely oblivious to the implementation details of our models.

class HousesController < ApplicationController

  private
    def house_params
      params.require(:house).permit(*House.house_params, House.windowable_params)
    end

end

And we can live happily ever after knowing that as we add, remove, and modify attributes on our models, we have a single source of truth for whitelisted attributes. AND this truth is grouped with all the other authoritative information about the state of the model; that is to say, it lives in the model itself. Win-win.

A word about purism

There is at least one possible argument against this approach — the pattern I’ve outlined does violate an academically strict understanding of minimal coupling. Rails 4 introduced strong_parameters with the express intent of moving parameter whitelisting behavior OUT of the model and INTO the controller. While this pattern doesn’t violate that intent it does keep the configuration of said whitelisting in the model. You might even call it an antipattern.

But no matter which way you slice it, either you’ll be duplicating knowledge or violating a high-brow interpretation of encapsulation. So you’ll have to decide which brand of Evil™ you prefer to drink.

Constructive feedback and/or discourse? Comment below or tweet me. Send all hatemail to /dev/null (if you don’t, I will).