I recently watched a presentation by Bryan Helmkamp titled Refactoring Fat Models with Patterns. Bryan based his talk on his blog 7 Patterns to Refactor Fat ActiveRecord Models, in which he describes seven patterns used to simplify models and adhere to the Single Responsibility Principle. I highly recommend studying both these resources.
From the patterns Bryan described, the Form Object pattern struck a chord as it seemed to be an elegant solution for a problem I have developed multiple implementations for but never felt completely satisfied with the result. I refer to User Registration and the lesser issue of User Authentication.
Does User Registration Logic Belong in a Model?
IMHO, no because registration/signup is a one-off event for a
User yet code responsible for this remains in the
User class and must be accounted for whenever a
User object is instantiated during testing.
This becomes even more apparent when additional validation could be required during registration that rely on remote services (i.e. lookup the user’s IP against a spammer blacklist). Adding
this logic to the User model (be it in a method or ActiveRecord callback) adds external dependencies to the
User class which again must be accounted for during testing.
Typically, user registration involves the following steps:
- Validate correctness of username and password with checks that restrict lengths, formats and uniqueness of each
- add virtual properties to a class (i.e. password)
- add methods to generate both a salt and an encrypted password
A sample implementation based on
authenticated_system, which expects the
User class to contain the following implementation:
Although convenient, the above just made our tests more complex as there are now additional validations that must be accounted for when writing
Additionally, the registration logic is tightly coupled to the
User class and cannot be easily re-used.
Is There a Better Way?
Bryan describes a Form Object as:
When multiple ActiveRecord models might be updated by a single form submission, a Form Object can encapsulate the aggregation.
Using a Form Object would mean that:
- The User registration process is encapsulated by a single and truly re-usable class
- All user registration and authentication code no longer needs to reside in the
Usermodel having less impact on testing
We’ll be using the following User Registration story:
- a user can register using a username and password
- the username must be unique
- username and password must be valid
- store an encrypted version of the salted password
- check the user’s IP against stopforumspam.com to determine if this is a known bot or spammer
UserRegistrator is a Ruby Class that implements all our
User story requirements.
include ActiveModel::Validationswhich provides the
errorsarray, which a Controller can use to report any registration specific errors
- Password encryption, salting and related virtual properties are all kept out of the
Userclass as are the
SessionsController then becomes:
The opinions expressed here represent my own and may or may not have any basis in reality or truth. These opinions are completely my own and not those of my friends, colleagues, acquaintances, pets, employers, etc...