Specialist: A pattern for carving up fat ActiveRecord models
All roads to a well-factored Rails application lead from fat controllers through fat models. Where to head next is less well mapped. Many patterns move non-model code into separate layers. When you’ve gone down that path as far as you can, your models can still be weighed down with code that would belong in a model — if there weren’t so much of it. Specialist is a pattern that makes it surprisingly easy to slice up your fat models into lean modules.
Patterns for layering in Rails applications
Naive developers tend to put all Ruby code that Rails doesn’t force them to put elsewhere into views or controllers. One improvement is to move display-related Ruby code from views to Rails helpers, decorators or serializers — which is after all what they’re for. Another is to move model-related Ruby code from controllers to models. Now all our controller actions are one or a few lines but our models are enormous. What’s more, modularity actually suffers: all of the pieces of code related to a given model that used to be in the controllers that need them are now all in that same fat model.
A good article by Bryan Helmkamp reviewed some options for splitting up fat models. Simply extracting modules or Rails model concerns is at best an intermediate step; you get small files but, since all of a model’s concerns are included in the same model class, no actual modularity. For example, if a User
model has one concern that lists users for a leaderboard and another that lists users for an administrative UI, and you name the concerns UserLeaderboardSupport
and UserAdminIndexSupport
and give them each a different ClassMethods
named all_with_associations
, the second one to be included in User
will overwrite the first.
Several patterns discussed in that article separate code not related to core model concerns or queries into different layers, such as value objects, form objects, service objects (that encapsulate business logic independently of persistence), and services (that encapsulate external services; every application I’ve worked with since before Ruby has had this layer). These are all well understood and work well. However, after applying any of these patterns that apply, a typical Rails model will still bulge with ActiveRecord code.
Now, how to filet these musclebound models? I usually find single-query objects to be too fine grained. Most queries are used only in one place, so I don’t want the overhead of an entire class for every one. The unit of modularity I care about is usually the code that supports a controller or controller action, because those are what come and go as features come and go. Finder objects (classes with collections of queries for models of given types) can be helpful, but they require a lot of delegation and don’t address model instance methods. Decorators (perhaps the same decorators that hold view code, or perhaps a layer of model-focused decorators below the view decorators; remember you can nest ’em) are definitely helpful, but don’t address non-instance query methods. I’ve also found that putting some of the model code that supports a given controller or action in a finder and some in a decorator actually splits up responsibilities, which is as bad as conflating them. For example, a query might need to set an extra attribute on its results which a decorator then uses in a later query. Another example is queries which use includes
to make later uses of instances’ association methods efficient. What I often want, then, is a single entity which adds both class and instance methods to another class.
Specialist: Finder object and decorator in one
While refactoring some fat models recently I wanted to extract controller-specific code from each model into just such an entity, an enhanced version of the model with new class and instance methods. The question was how to make each new entity a stand-in for its original model class, i.e. to allow clients and new methods to use all of the model class’s class and instance methods. I started by delegating class methods to the model class and instance methods to a model instance, but decorating the instances everywhere was a pain and there were some strange subtleties in delegating from one class to another and I finally realized I was reinventing subclasses to make it all work. So I just used subclasses.
Subclassing seems a little strange, since modern OO and especially Ruby practice favors objects that are less strongly coupled to one another than subclasses are to their superclasses. But it’s exactly what’s wanted here: an object which behaves like an object of another type in every respect, relies heavily on that other type’s behavior, and adds behavior of its own.
This kind of subclassing needs a name — “model subclass” is awkward and too general — if only to name the directory where the subclasses live in a Rails app. Each subclass specializes in the queries and calculations needed by a controller or a controller action, so I call them ‘specialists’.
A lone Specialist
Specializing a model is trivial as long as you don’t need to return Specialist instances from other models’ association methods. Just write a subclass and add methods:
class LeaderboardUser < User def self.leaders order("score desc").includes(:category_scores) end def highest_scoring_category category_scores.to_a.max_by &:score end end
ActiveRecord automatically does what we want here: query methods called on a subclass of an ActiveRecord model return instances of the subclass, so LeaderboardUser.leaders
returns LeaderboardUser
instances. Specialists are very easy to implement; just write code as you would in an ordinary ActiveRecord model. You don’t need to manually delegate or wrap anything.
Specialists are also very easy to use: use them just like ActiveRecord models. In a controller action, we can just call LeaderboardUser.leaders
and hand the result off to a view or decorator or serializer which can use the LeaderboardUser
-specific methods as if all that functionality was part of User.
Being a new class, the specialist needs its own model spec or test and, if you’re using factory_girl or something similar, its own factory. These aren’t just boilerplate, but part of the point of the pattern: the new behavior should only be available where it’s specifically needed, so you need to ask for it by name in tests and factories too. However, Specialist makes integration testing even more necessary than it already was: it’s easy to forget to return a Specialist where one is needed, while returning one from a test double so that unit tests of clients pass and the error goes undetected. Thorough integration testing will prevent this kind of integration error.
Graphs of Specialists
When more than one model contributes to the same feature, you may want a Specialist of each model for the feature. That then means getting all of the Specialists’ association methods to return other Specialists. Of course you need to tell ActiveRecord the Specialist class to return, and it guesses the wrong foreign key, so you need to tell it the right one.
class LeaderboardUser < User has_many :category_scores, inverse_of: :user, class_name: 'LeaderboardCategoryScore', foreign_key: 'user_id' end class LeaderboardCategoryScore < CategoryScore belongs_to: :user, inverse_of: :category_scores, class_name: 'LeaderboardUser', foreign_key: 'user_id' end
Redefining associations like this does get a bit repetitive when there are many Specialists and/or many associations, but it’s probably no worse boilerplate than is needed for other model-splitting patterns. Reducing the boilerplate with class macros wouldn’t be out of the question, although it would take some clever design since not every association is necessarily to a Specialist. Thus far I’ve just redefined the associations as shown here.
If you use factory_girl, you also need to tell your Specialist factories to construct the right classes for associated objects.
FactoryGirl.define do factory :user do # define scalar attributes factory :leaderboard_user class: LeaderboardUser end factory :category_score do # define scalar attributes user factory :leaderboard_category_score class: LeaderboardCategoryScore do association :user, factory: :leaderboard_user end end end
With many Specialists of a given model factory code does get repetitive, but since you can write any Ruby you like in a factory definition file it is very easy to DRY up.
Sharing code among Specialists
Part of the point of the Specialist pattern is that most model code is used in only one place in higher layers, so it should only be available where it’s needed. Some model code is used by multiple clients, however. If each of those clients uses a Specialist, just extract a module or concern with the shared functionality and include it in each of the Specialists that needs it. Since Specialists are by definition correctly scoped to their clients, there is no concern about overexposing code as there is when including modules or concerns in the base model. Specialists and ordinary models can include the same module too.
When Specialist is appropriate
The Specialist pattern is most helpful when
- models are fat even after code not related to persistence or core business logic has been moved to other layers
- most model functionality is used only in one or a few places in higher layers
- model functionality is in both class and instance methods, especially when they work together
More of those things are true, and they’re more true, in larger applications and those which render their own views. Rails applications which leave rendering to client-side applications or otherwise wholly or mostly serve APIs may have less code that needs to be split into Specialists, or may follow some other organization imposed by an API framework. Nonetheless, Specialist is so easy to implement and use that I encourage everyone to look for places where it applies in any application.
Leave a Reply