There is a long going discussion on fat-this and skinny-that when building software systems. Obviously, you want to avoid fat classes all over, but where do you put the code then?
Thankfully, Ruby on Rails comes with a clear structure for your source code. Except where it does not. When models and controllers are on a diet, helpers are for views only or categorically evil and the lib folder is abandoned, there is not much left.
In this two-part blog post, we explore the layers provided by Rails and look out for new places to divide our code further.
When structuring code, the single responsibility principle is your best friend. If a method is not related to the other ones of a class, it should probably be put somewhere else, i.e. in an own class.
In order to decide well, we have to know what the actual responsibility of a class is. This will be the focus of the first part of this post. In the second part, we will learn where to put all that code that should no be in models or controllers.
Models are for persistence
Who is responsible for authentication? The User. Who calculates all kinds of discounts? The Product. Who fetches the latest posts from Twitter? The Account. In Rails-land, the first place that comes to mind for any kind of logic are the model classes. This is how Fat Models™ and shortly thereafter their justified criticism came into existence. In the backlash, even callbacks and validations have been declared evil by some. The model is clearly not the place to put above functionality. So is there actually anything left that we should put into a model class?
Essentially, models are used for persisting data. They reside in a single class per database table. Any interactions with the corresponding persistence table are well-placed in the model class. But as soon as there are multiple models involved (e.g. logical children like comments of a blog post), a separate place should be considered.
The critical question to ask if code should go into a model class or not is
„Does it have to do with persistence?“
Even if a method has to do with persistence, you still can put it in an own place. Use own classes or Concerns to extract logic common to several models or multiple methods sharing the same responsibility.
Lock down your data with validations
A critical aspect of persisting data is that it is valid and consistent. This is especially important if you are developing business applications with strict expectations of their data quality. Furthermore, a value’s constraints give you an important contract you may rely on. Consider the following controller action, where we load a post based on its slug, presumably passed as a URL path parameter:
# GET /posts/:slug
@post = Post.find_by_slug(params[:slug])
If a post without a slug would be possible, we could not access it by a URL. If the slug is not unique, the returned post would be undetermined. If it contained special characters, the URL would be invalid.
Therefore, validations that verify the consistency of data must be in the last point before persisting: the model. Only there it is guaranteed that the required checks are performed in all situations. In our projects, the first thing we do when adding a new model is to clarify the hard constraints for its attributes and write them down as validations in the model class.
… and callbacks
The persistence-only rule goes for callbacks as well. They can be very practical to normalize data, pre-calculate stored values for an optimized retrieval or prevent the destruction of certain entities. When model callbacks are used in order to keep the persisted data consistent, there is nothing against them.
However, you should be very cautious about calling third-party services (e.g. sending emails, posting updates to an API) in callbacks. This code not only has nothing to do with persistence, but may also create undesired side-effects that may interfere with the original operation. Does a save request really have to fail just because that other service is down?
Controllers process requests. They accept a set of parameters to load or manipulate data and return a friendly response. Handling sessions, various response formats and possible failures already make quite a few demands. That’s why we should not load even more code onto controller classes.
Besides extracting request-independent code, another opportunity for skinning controllers is the avoidance of duplication. In a majority of Rails projects, there is a fair amount of controllers that handle CRUD operations for a given model, with no or only minor differences. This is so common that Rails even ships with a generator for this kind of controller since its earliest days. The generator is fine for a start, but soon a lot of duplicated code will have been generated.
Thanks to the powerful features of Ruby, it is quite simple to abstract the common code into a superclass and have the actual model controller only make the necessary fine-tuning. Almost all of our Rails projects contain such a CrudController and even a ListController (for read-only models). The specific model controllers inherit from this class and are mostly empty: ultra-skinny controllers!
Of course, there are various Gems that tackle this abstraction. For admin-only frontends there is the great ActiveAdmin, or for more generic uses the (deprecated) Inherited Resources. For the full amount of flexibility and a maximum of DRYness we use dry_crud (shameless self-plug). This gem also makes use of the little known Rails feature of inheritable templates, which brings DRY to the view layer. By only inheriting from CrudController and by defining a corresponding route, the fully functional CRUD views and operations in your application’s style are available. Skinny views!
To be continued
So we have skinny models and skinny controllers (and even skinny views ;), but still no place to put all that extracted code. In the next part, we will reveal this missing puzzle piece.