Software is constantly evolving, both in specification and implementation. Keeping an extensible and maintainable codebase is, therefore, crucial to deal with these changes quickly and easily. Ruby on Rails comes with a lot of good techniques and patterns out of the box that facilitates this. Nevertheless, some of these are commonly misused or overused and consequently, they end up negatively affecting the quality of our code.
In this article, I will discuss some of these techniques and their caveats, and patterns that can help you level up your Rails so that you don’t fall into these situations. I will also provide code examples and cases where each pattern may be useful.
In addition, we will investigate important design principles, how they are not respected when overusing patterns/techniques with Ruby on Rails, and how to mitigate the problems they cause to maintainability.
But first, let’s review some fundamental concepts we will focus on in this article.
SRP (Single Responsibility Principle)
This principle, introduced by Robert C. Martin, states that a class or module should only have one responsibility, which is defined as a single reason to change. This, in other words, means that we should put together the things that change due to the same reason and separate those that change for different reasons. The motivation of this principle is limiting the impact of changes by minimizing the modules or classes where these changes occur.
As our Rails application gets more features, models tend to grow larger and more complex, including responsibilities that should not belong to them, turning into “fat models”. Consequently, the code gets harder to understand and change, becoming a problem for maintainability.
Low coupling and high cohesion
Coupling is the degree of interdependence between modules. A high interdependency will mean that changes made to one module will have a higher probability of impacting the other modules, so our goal should be to have low coupling. Cohesion refers to how the elements of a module belong together. Cohesive modules are easier to maintain. As the elements within the module are directly related to the functionality that the module is meant to have, changes are then more localized.
We should “design components that are self-contained: independent, and with a single, well-defined purpose” . In order to reach this, we need to have low coupling and high cohesion. As you may have noticed, this is also part of what the SRP principle means to achieve.
“Don’t Repeat Yourself” is a well-known principle, particularly for Rails developers. It is often associated with “avoiding code duplication”, however, the concept is much bigger than this. The principle is about reducing knowledge duplication, which is achieved when every piece of knowledge has a single representation. By saying knowledge, we mean the business logic of our application, which is represented through algorithms.
This concept is important because sometimes, thinking our code is not DRY, we end up extracting similar behavior dangerously, while adding unnecessary coupling and complexity to our application.
It’s worth noting that code duplication doesn’t always mean violating DRY principle, however, this by no means implies that we should have redundant code.
Explicit over implicit
This is a principle that is often underrated. The key in explicitness is that it makes the code easier to understand, which makes it easier to modify. Being explicit means not hiding the behavior of our modules and their methods, in order to make it easy to know how our application really works. Explicitness also allows us to avoid mysterious side effects.
One of Ruby on Rails advantage compared to other frameworks is the agility with which features can be delivered, and it partly achieves this due to different patterns such as ActiveRecord, which provides many useful techniques for us to use, like scopes and callbacks. Rails encourage the use of these and as a consequence, they tend to be overused by developers, while there are actually better solutions in some cases.
Scopes allow us to encapsulate commonly used queries into single methods in our ActiveRecord models instead of having this query logic repeated. Scopes are easy to implement and also provide readability to our models. However, they tend to increase the size of models, as it is easy to pollute them with a lot of different scopes. This gets worse when some of these do complex queries, like joins with other tables which couples our model to the other models’ attributes, something we shouldn’t do. It looks like the responsibility of these queries could belong somewhere else, for which the Query Objects pattern will be useful.
These are Plain Old Ruby Objects (PORO) that include a model’s query logic. The goal of this pattern is to reduce the responsibility of a model by removing complex queries from it. There are two different versions for this pattern: one in which we create one object per model, and another where we create one per scope we want to extract. The second version will likely be the best choice if we have multiple complex queries.
Imagine we had an e-commerce application and we regularly send emails to users that haven’t visited us recently and users that recently bought products. We may have then a User model with some scopes, looking like the following:
The following is an example of this pattern applied to User model:
Then we can use it from somewhere else in our code, for example, in a simple task:
We generate a public method for each scope. The query object is initialized with an Active Record relation as an optional parameter, and if there isn’t any given as argument, it initializes with all records of the relation. This also allows composition with other query objects and scopes, similar to if we were using scopes.
As you may have noticed, the subscribed scope behavior is repeated inside the query object. Fortunately, we can encapsulate queries or part of complex ones into auto-descriptive methods. For this, we dynamically extend the relation with “private” scopes and then use them in our query object, using
ActiveRecord::QueryMethods.extending method in our initializer.
Active Record Callbacks
Callbacks are useful as we can place repetitive behavior in a single place when otherwise it would be repeated somewhere else. Also, as they belong to the model we keep the controllers clean. Additionally, we can easily extend our code as they aren’t hard to implement and may not need to modify much of the other code in the model.
Active Record Callbacks, however, come with some drawbacks:
- Additional responsibilities to the model, which goes against SRP. In case you don’t know what SRP (Single Responsibility Principle) is, it is a principle which states that a class or module should have only one responsibility, which means a single reason to change.
- Implicit behavior, which makes testing it in isolation impossible, as we need to call the corresponding callback method (for instance,
createmethod). Instantiating the model may also trigger a cascade of callbacks, making testing slower in general.
- Unwanted side effects, particularly when having a lot of callbacks. This is also a consequence of implicitness.
- Once we define a callback, every instance of the model is bound to it. If we want to exclude some of them, we shouldn’t use them, as conditional callbacks are a big code smell.
- They break the linear flow of our code, making testing, debugging, and refactoring more difficult. When we run into a problem at the controller level or we just want to change its behavior, we start from the controller and keep inspecting methods calls until we reach our model. With callbacks, we also have to know which of these are called, figure out their order, and check if they effectively need to be triggered for our instance as they may be conditional.
Using too many callbacks doesn’t seem a really good idea. Fortunately, the Decorator pattern can help us with this. We could also use other patterns such as the Form objects mentioned in the previous part of the article, or even Service Object which will be described later on in this article.
The decorator pattern is a design pattern used to extend the functionality of objects without modifying the behavior of other objects of the same class. This is achieved by wrapping an object with another one that has the desired functionality. We can use this pattern instead of callbacks, providing more explicitness and a better assignment of responsibilities. Additionally, it makes testing easier, as each functionality can be tested directly and without needing to trigger every other callback.
As an example, imagine we added multiple callbacks for when a user is created:
The following would be a simple Ruby approach to this pattern.
If we wanted to delegate all user methods so that we can do
@user.attribute, we can also use SimpleDelegator.
From Rails 5.1 onwards, however, we can use a helper called delegate_missing_to, which is recommended instead of the above as it is more explicit.
Regardless of how we implement the Decorator, the controller where the model is created should look like the following:
In case you have been wondering, the Presenter pattern can be considered a subpattern of the Decorator pattern, as it has the same intent but focuses on trying to keep the logic out of the view. This is implemented similarly to the examples mentioned, but there are also commonly used gems such as Drapper.
With nested attributes, we can save the attributes of a record through an associated record. This makes them useful as we can easily create multiple nested records and automatically handle any error.
One issue with nested associations is that we may need to add additional complexity to our controllers. The biggest problem with them, however, is that it restricts the parameters our controllers must receive, and so it unnecessarily couples the frontend to the database. It’s weird for the frontend to know how we designed our models, especially if it is a completely separate app, such as React, Ember, or Angular apps. We should have an interface for this in the backend, which we can achieve with the Form Objects pattern.
A Form Object takes care of the creation of multiple models, attributes mapping, and contextual validations. We can define one form object for each form in the frontend. They give more flexibility to the frontend as we can map fields in the forms to the attributes of the records in the database. In addition, the pattern helps to respect the Single Responsibility Principle by removing logic from our models or controllers.
Regarding validations, it’s important to make a clear distinction between data integrity validations and contextual validations. The former is tied to the constraints defined in our database schema and, as they are related to how a model is always persisted, they should stay in the model. The latter consists of validations that are important only in the context of a particular form flow, becoming part of the business logic defined for it, so they should go in the form object. This, for example, allows us to easily require fields in one form that are not necessary for others and ensure that some validations will still be applied for every record created, even if this doesn’t happen through a form.
Any changes needed to the forms in the frontend will also be much easier to implement as we should only need to modify the related form object, and any new form related to a model may simply require extending our code.
Continuing the example we used earlier, now the User model has a user_request which is responsible for knowing the filters the user saves when browsing the store.
Let’s assume that when the user signs up on our site, we also send a nested user_request with the filters. We receive these parameters in our controller and create both associated records in the database. In this case, the signup controller may look something like this:
This looks clean, user creation is simple because we have
accept_nested_attributes_for in its model. However, we are coupling our frontend to our database, while also forcing them to send the
user_request param with
_attributes. Also, if the user model and its associations get more complex we may need to include additional code in the controller and, as we know, controllers should be as thin as possible. As an alternative, we can create a Form object, which could look similar to the following:
As you may have noticed, we need to add more code because nested attributes did things for us that we now have to do manually. However, this code is encapsulated in its own object, so it still is preferable over having code in controllers or models. Basically, the form object must be initialized with the params we receive in our controller and we will map to our models attributes. We define then a save method which is in charge of triggering the model persistence logic.
Like I previously mentioned, contextual validations should go in the form of objects. Let’s consider that for this form
phone is a required attribute, while we have some other cases where it isn’t. We need to add a presence check to this particular case, and the best place for it is this form object. In our database, however, phones are unique to each user so we keep the uniqueness validation in the model.
All models can also be saved in a transaction, so it rollbacks in case any of the models created has an error. Additionally, we may add code that we want to execute after everything is created correctly, such as triggering a notification.
The controller which uses the form object doesn’t change much. Here is how the form object could be used:
In this case, particularly, the form has all of its attributes explicitly defined, so there is no need to add strong parameters in our controllers, as we can ensure that there won’t be any additional attributes when persisting our models.
Of course, this is a simple example, and the implementation will probably vary depending on the context where it’s used. Forms can easily get more complex, so in this case, it may be useful to combine this pattern with a Service Object, a pattern I will mention in the next part of this article.
I included them together because concerns are basically a form of multi-inheritance. Inheritance is useful, but it must be used properly. It must be thought top-down, as creating specialized versions of a parent class and not as a way of extracting duplicated code from two classes. Possible issues with poorly used inheritance are that we may lose explicitness and if we needed to add a third class we would likely need to make unnecessary changes to the hierarchy.
The same applies to concerns, but the consequences are even worse as models can have multiple concerns. Also, in these cases, finding where a method is defined can be tough.
There are also some common bad practices related to concerns that should be avoided. An example is extracting methods into a concern just to reduce the size of a class. We may be gaining readability from this but we lose explicitness and responsibilities are still in the class, only that they were hidden in another file. Another example is bidirectional dependencies in concerns, which is the same as assuming that a superclass knows the implementation details of its subclasses, something we should avoid. An even worse version of this last case is a dependency between concerns. A proper concern should then be free of these dependencies and have a single well-defined responsibility.
Rails encourage the use of concerns and there are lots of articles that promote them, but its incorrect use has led to even be considered an anti-pattern. Quoting Bryan Helmkamp, CEO at CodeClimate: “Any application with an app/concerns directory is concerning.”
Should we really use concerns then? Even when taking into account all the considerations mentioned, it is difficult to apply them properly, and using concerns usually means having multiple places where we hide the complexity of our models, making them more difficult to understand and maintain. There isn’t really a reason to use them when we have more explicit, representative, and scalable alternatives, including any of the patterns mentioned in the article. The choice between these depends on the context they are going to be used, but if it doesn’t seem like any applies, you can go for the Services/Service Objects pattern.
As a side note, we can use concerns to share behavior between controllers, and this may make more sense in some cases, however, remember that controllers shouldn’t be complex, and having many concerns may also be a sign that we have logic we could move somewhere else.
This pattern consists of encapsulating a particular logic in a service object. The key benefits of using services are that we can remove responsibilities from our models or controllers and make explicit where that functionality is, while also solving the issues with using concerns that we previously mentioned. We can use this pattern when we have to implement a functionality that is complex, involves multiple models, or needs to interact with an external service.
This an example of using a concern for encapsulating some functionality:
The same example, done with a Service would look like the following:
When each service represents only one operation, they are sometimes called Service Objects, becoming a more procedural approach to the pattern. There are many ways to implement these, some prefer to add only one
call the method, so that they can be invoked by
Services by themselves do not necessarily make our code cleaner. The pattern is not only about extracting a method from a model or controller into another component. We must make sure that it is designed correctly, taking into account the same principles we use for models. There are many good practices related to creating services that extend outside the scope of this article, such as internally handling exceptions, returning responses as attr_readers, and reducing the way they are called to make it even less verbose.
As a side note, know that services can be used together with other patterns we previously covered in order to achieve a better assignment of responsibilities.
Customer Validation in Models
Rails built-in validators are really useful for model validations. When validations get too complex, however, we need to implement validations ourselves. It’s really common to include these custom validations in our models as custom methods, believing they belong to the model as other simple validations. The problem with this is that it adds an additional complexity to the models that could easily be encapsulated in its own object. Even Rails guidelines recommend better ways to deal with these cases, using other tools included in the framework. I will mention two of these recommendations.
To create a custom validator, we must create a class that inherits from ActiveModel::Validator and implements the method
validate that receives the record as an argument and validates it. It is then used in our models with
What makes them a really good solution is the explicitness they give, as it is intuitive to find where custom validations are located.
Another way of extracting validations is through EachValidators. We need to create a class that inherits from ActiveModel::EachValidator and implements the method validate_each that receives as arguments the record, attribute, and value to validate, and performs the validations. They can be used from the model similarly to standard validators and can be combined with these as shown in the following example.
These validators are more reusable than the first because they also receive the attribute to validate. If we had this regex validator for an email and we had more than one model with attributes that are emails but have different names, we could easily use the same validator for each attribute.
In this article, I discussed some recurrent problems in Rails applications that the techniques provided by the framework introduced. Using these techniques isn’t bad per se, but we tend to add a lot of logic and complexity to our models when using them, which is contrary to the mentality we should adopt.
To avoid this, it is necessary that we justify our design decisions based on well-established principles and give thought to how these will impact our project in the long term. For this reason, we covered several patterns that can help us to make the appropriate choices for our applications and reviewed principles that are necessary in order to understand the problems these patterns solve.
The mentioned patterns, when correctly applied, can solve a lot of problems related to the maintainability of our application. Although, for small projects, it may not be worth over-engineering it by using all of the patterns. In saying that, some are really easy to apply and we should start using them as early as possible, especially for big projects, as later on changes become more expensive to make.
 The Pragmatic Programmer