The Conductor Pattern

  • Ruby

Objects on Rails

A movement has been growing over the last several months to make our Rails applications more object-oriented. One of the most popular recently has been Avdi Grimm's Objects on Rails. In it, he talks about an alternative to the Presenter pattern, which he calls Exhibits (and both of which are subsets of the Decorator pattern). I've been using another form of this, which may or may not be called a Conductor.

Most of these patterns come from the Java world, and Smalltalk before that. They have very specific rules about how they can be used (read-only, wrap methods on a single object, etc). In the Ruby world, however, its flexible object model and powerful metaprogramming features mean we can be a little more lazy about those rules. The Conductor pattern is an implementation of what Martin Fowler's PoEAA calls a Unit of Work. Specifically, it wraps one or more models, and can be bidirectional, so it can decorate attributes of the models as well as update them.

I gave a talk a couple years ago at Mountain.rb entitled Forms Don't Have to be this Complicated. In it, I spoke about how terrible Rails is at managing forms, particularly ones that deal with several related objects. I outlined a couple solutions, none of which were very good. I have recently been using this Conductor pattern for these complicated forms, and it seems to have alleviated most of the pain. The ability to wrap multiple models means it can handle the nested associations, and being bidirectional makes it perfect for forms.

In this post, I'm going to show you a couple uses of the Conductor pattern, as well as a library I wrote that implements it.

GoldPlating

In Ruby, writing an implementation of the Conductor library is trivial with the help of ActiveModel. The entire implementation is only 60 LOC. I haven't bothered making a gem of it yet, but here's a gist. Just toss it in your Rails lib/ dir and require it.

Here's how to use it:

class Registration
  include GoldPlating

  wrap Account, :name
  wrap User,    :email, :password

  validates_presence_of :account_name,
                        :user_email,
                        :user_password,
                        :message => "Required"

  validates_format_of :user_email,
                      :with => /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/,
                      :message => "Not an Email",
                      :if => lambda { user_email.present? }

  validates_length_of :user_password,
                      :minimum => 5,
                      :message => "Too short",
                      :if => lambda { user_email.present? }


  def save
    super &&
      Membership.create(account: account, user: user)
  end

end

Here, we have two models, Account and User. In this example app, a User can have access to several Accounts which might represent a business entity, similar to how Github Organizations work. When a user signs up, we want to automatically create but their User for login, and the initial Account they'll belong to. Because attribute names may overlap, GoldPlating prefixes them with the model name. Account#name becomes Registration#account_name.

Validations

I've also added some validations to the Registration class. Sometimes different forms dealing with the same models have different valid states for those models, which validation contexts attempt to solve. I think having validations on the wrapper object is a much more elegant solution, leaving the validations on the model objects to simply validate whats needed to stick the object in the database, or is required of the model for the business logic in the rest of the app.

It also lets you have separate human-readable error messages closer to the view, leaving your model validations simpler. For example, if I was requiring users to confirm their email or password, this would be an excellent place to put the validates_confirmation_of, leaving the model clear. It also makes it so that you don't have to set the _confirmation fields in your tests or factories when creating the objects.

The downside is that you have to duplicate some of your model validations in the conductor, like the validation on email above. I don't have an elegant solution for this yet.

Controller

When writing a registration form that needs to create both objects, the normal Rails method would be to separate out the params in the controller, and create one of each object then associate them. This gets tricky, however, when one or the other fails to validate. Also, it puts a lot of logic in the controller. Using this conductor however, our controller is simple:

class RegistrationController < ApplicationController
  skip_before_filter :require_login

  def new
    @registration = Registration.new
  end

  def create
    @registration = Registration.new(params[:registration])

    if @registration.valid?
      @registration.save
      auto_login @registration.user
      redirect_back_or_to root_path, :notice => "Registration Successful"
    else
      render :new
    end
  end
end

The form in the view is, too:

= form_for @registration, url: signup_path do |form|

  = form.text_field :account_name

  = form.email_field :user_email

  = form.password_field :user_password

The controller and view can interact with the Registration conductor like any other ActiveModel-compliant object. In fact, this is the entire implementation. GoldPlating handled the object initialization, and assignment of the params to the right objects. GoldPlating also handled saving the objects it wraps, all we had to do was implement a #save to create the Membership association record between the newly-created User and Account records.

So much easier

I've been using this Conductor pattern quite liberally throughout my recent projects. Having this wrapper object to contain all the business logic related to presenting multiple objects to a form, and consuming and validating that form's output, greatly simplifies my code. It also makes testing a breeze. I can test all the edge-case behaviour via tests that exercise the conductor, rather than full-stack tests through the full Rails request stack.

I've found that having a single conductor object for each form is the best way to go. It does lead to some code duplication, which might be alleviated some by including modules of shared methods, but overall the code in the conductor is simple and focused enough that the duplication has not (yet) been a major problem. It really helps to isolate the User-Interface of the forms from the data model, and associated business logic.

Some of the really useful places for conductors have been Registration, UserPreferences, PasswordReset, ManipulateAuthorization. You'll notice that all of these involve some sort of User model, but each in a different way. Its nice having all the logic for each process encapsulated in a single, distinct place.

Feel free to grab GoldPlating from that gist, or if there's enough interest, I can package it up as a gem.