You have a form who’s fields don’t match up to a model. What do you do?
Ask any web developer, and they will confidently defend one of three patterns: fat controllers, callbacks, or form objects. It’s an on-going, heated, debate.
I would like to propose a fourth pattern, form extensions, which I will argue is often the most pragmatic approach. It is probably not a new idea, but it certainly doesn’t get the lip-service it should. It deserves a name, and a first-class position in a web-developer’s toolbelt.
Let’s keep this discussion grounded by considering an example problem. Suppose we have a registration form that includes a user’s emails address, password, and their account name. When the form is submitted, we want to create a user record, as well as an account for that user. This is a very common problem, and one that I have seen solved in many different ways.
First, let’s look at the common patterns, then I’ll present the form extension pattern. I will use Rails for my examples, but the discussion should translate easily to any MVC-based web framework.
1. Fat controllers
This is certainly the simplest solution. The controller is meant to be the glue of your application, so let it “glue” together your user and account creation. In Rails this looks something like:
class UserController < ApplicationController def create user = User.build(params[:user]) account = Account.build(params[:account]) if user.save && account.save # handle success else # handle failure end end end
- It’s obvious.
- It’s a pain to re-use (read: ugly tests!).
- It doesn’t play nice with other patterns (e.g. you can’t use Devise anymore).
This is the Rails way. You want an account created when you create a user? Throw a
before_create callback on that sucker and you’re done.
class User < ActiveRecord::Base # ... belongs_to :account # Create a virtual address for the extra form field. attr_accessor :account_name # Validate account attributes when a user is created # (Or, you can transfer any errors over manually) validates_presence_of :account_name, on: :create before_create :create_initial_account private def create_initial_account create_account(name: @account_name) end end
- It’s easy to write.
- It’s simple.
- It’s very re-usable.
- Coupling. Your user object is now in charge of three concerns: registration, user functionality, and account functionality. This makes for slow, over-complicated tests, and hard-to-reason-about code.
3. Form objects
All the cool kids are doing it this way. You have a registration form, so why not make a “registration” a first-class object in your system and let that object handle all the registration-related business logic?
class Registration include ActiveModel::Model attr_accessor( :account_name, :email, :password ) validates :account_name, presence: true validates :email, presence: true, email: true # ... def submit if valid? User.create!(email: @email, password: @password) User.create_account(name: @account_name) end end end
- It follows the single-responsibility principal.
- It’s re-usable.
- It’s easy to test.
- It scales well when complexity is added in the future.
- Complexity. It’s an extra layer that could be overkill for you. You just duplicated your validation logic. Did you test it? Are you delegating the attributes properly? Better test all of them.
4. Form extensions
Let me preface by saying that I don’t think any of the above patterns are wrong. Each has its uses, and none is perfect. Form extensions, I think, strike a nice balance between the simplicity of callbacks and the re-usability and scalability of form objects.
The form extensions advocate argues that registration is mostly about creating a user. Sure, we’d like to create an account (and possibly notify some external services, etc), but that feels like an aside. The user object is almost good enough as it is. Do we really need an extra layer of indirection just to add a tiny bit of behaviour? Ideally we’d like the simplicity of callbacks without the coupling. In comes the form extensions pattern, which gives us a clean and transparent way to “opt-in” to the callback functionality only when it’s needed, and to not have to think about it when it’s not. In general, you create an extension of your primary model, giving the extension the extra functionality needed for the given form. In this example, we would create a
User that knows about registration.
class RegistratingUser < User # You still need this attribute for the form. attr_accessor :account_name # Only need to add account-specific validations validates_presence_of :account_name before_create :create_initial_account private def create_initial_account create_account(name: @account_name) end end
Now you can freely use your
User model without worrying about any details of registration, and you can opt-in to the registration functionality by using a
RegistratingUser model instead. The result is simpler than a form object, because your user already has most of the functionality you need. You don’t need to manually delegate that behaviour. A form extension is almost as simple as a callback, and almost as scalable as a form object. I believe it strikes the perfect balance in many cases.
- It’s simple.
- It’s re-usable.
- It’s easy to test.
- There is less to test than a form object, if we trust inheritance.
It won’t handle future complexity as well as form objects. It uses inheritance instead of composition, so you are leaking user-related concerns into your registration concern. If the registration logic will be complex, I would recommend a form object instead.