Build Sleek Rails Components With Plain Old Ruby Objects
Your website is gaining traction, and you are growing rapidly. Ruby/Rails is your programming language of choice. Your team is bigger and you’ve given up on “fat models, skinny controllers” as a design style for your Rails apps. However, you still don’t want to abandon using Rails? No problem.
In this article, Toptal Software Engineer Eqbal Quran explains how you can decouple and isolate your Rails components using nothing Plain Old Ruby Objects. Ruby objects and abstractions can decouple concerns, simplify testing, and help you produce clean, maintainable code.
Your website is gaining traction, and you are growing rapidly. Ruby/Rails is your programming language of choice. Your team is bigger and you’ve given up on “fat models, skinny controllers” as a design style for your Rails apps. However, you still don’t want to abandon using Rails? No problem.
In this article, Toptal Software Engineer Eqbal Quran explains how you can decouple and isolate your Rails components using nothing Plain Old Ruby Objects. Ruby objects and abstractions can decouple concerns, simplify testing, and help you produce clean, maintainable code.
Eqbal is a senior full-stack developer with more than a decade of experience working in web and mobile development.
Expertise
PREVIOUSLY AT
Your website is gaining traction, and you are growing rapidly. Ruby/Rails is your programming language of choice. Your team is bigger and you’ve given up on “fat models, skinny controllers” as a design style for your Rails apps. However, you still don’t want to abandon using Rails.
No problem. Today, we’ll discuss how to use OOP’s best practices to make your code cleaner, more isolated, and more decoupled.
Is Your App Worth Refactoring?
Let’s start by looking at how you should decide if your app is a good candidate for refactoring.
Here is a list of metrics and questions I usually ask myself to determine whether or not my code needs refactoring.
- Slow unit tests. PORO unit tests usually run fast with well-isolated code, so slow running tests can often be an indicator of a bad design and overly-coupled responsibilities.
- FAT models or controllers. A model or controller with more than 200 lines of code (LOC) is generally a good candidate for refactoring.
- Excessively large code base. If you have ERB/HTML/HAML with more than 30,000 LOC or Ruby source code (without GEMs ) with more than 50,000 LOC, there’s a good chance you should refactor.
Try using something like this to find out how many lines of Ruby source code you have:
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
This command will search through all the files with .rb extension (ruby files) in the /app folder, and print out the number of lines. Please note that this number is only approximate since comment lines will be included in these totals.
Another more precise and more informative option is to use the Rails rake task stats
which outputs a quick summary of lines of code, number of classes, number of methods, the ratio of methods to classes, and the ratio of lines of code per method:
bundle exec rake stats
+----------------------+-------+-----+-------+---------+-----+-------+
| Name | Lines | LOC | Class | Methods | M/C | LOC/M |
+----------------------+-------+-----+-------+---------+-----+-------+
| Controllers | 195 | 153 | 6 | 18 | 3 | 6 |
| Helpers | 14 | 13 | 0 | 2 | 0 | 4 |
| Models | 120 | 84 | 5 | 12 | 2 | 5 |
| Mailers | 0 | 0 | 0 | 0 | 0 | 0 |
| Javascripts | 45 | 12 | 0 | 3 | 0 | 2 |
| Libraries | 0 | 0 | 0 | 0 | 0 | 0 |
| Controller specs | 106 | 75 | 0 | 0 | 0 | 0 |
| Helper specs | 15 | 4 | 0 | 0 | 0 | 0 |
| Model specs | 238 | 182 | 0 | 0 | 0 | 0 |
| Request specs | 699 | 489 | 0 | 14 | 0 | 32 |
| Routing specs | 35 | 26 | 0 | 0 | 0 | 0 |
| View specs | 5 | 4 | 0 | 0 | 0 | 0 |
+----------------------+-------+-----+-------+---------+-----+-------+
| Total | 1472 |1042 | 11 | 49 | 4 | 19 |
+----------------------+-------+-----+-------+---------+-----+-------+
Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- Can I extract recurrent patterns in my codebase?
Decoupling in Action
Let’s start with a real-world example.
Pretend we want to write an application that tracks time for joggers. At the main page, the user can see the times they entered.
Each time entry has a date, distance, duration, and additional relevant “status” info (e.g. weather, type of terrain, etc.), and an average speed that can be calculated when needed.
We need a report page that displays the average speed and distance per week.
If the average speed for the entry is higher than the overall average speed, we’ll notify the user with an SMS (for this example we will be using Nexmo RESTful API to send the SMS).
The homepage will allow you to select the distance, date, and time spent jogging to create an entry similar to this:
We also have a statistics
page which is basically a weekly report that includes the average speed and distance covered per week.
- You can check out the online sample here.
The Code
The structure of the app
directory looks something like:
⇒ tree
.
├── assets
│ └── ...
├── controllers
│ ├── application_controller.rb
│ ├── entries_controller.rb
│ └── statistics_controller.rb
├── helpers
│ ├── application_helper.rb
│ ├── entries_helper.rb
│ └── statistics_helper.rb
├── mailers
├── models
│ ├── entry.rb
│ └── user.rb
└── views
├── devise
│ └── ...
├── entries
│ ├── _entry.html.erb
│ ├── _form.html.erb
│ └── index.html.erb
├── layouts
│ └── application.html.erb
└── statistics
└── index.html.erb
I won’t discuss the User
model as it’s nothing special since we are using it with Devise to implement authentication.
As for the Entry
model, it contains the business logic for our application.
Each Entry
belongs to a User
.
We validate the presence of distance
, time_period
, date_time
and status
attributes for each entry.
Every time we create an entry, we compare the average speed of the user with the average of all other users in the system, and notify the user by SMS using Nexmo(we won’t discuss how the Nexmo library is used, though I wanted to demonstrate a case in which we use an external library).
Notice, that the Entry
model contains more than the business logic alone. It also handles some validations and callbacks.
The entries_controller.rb
has the main CRUD actions (no update though). EntriesController#index
gets the entries for the current user and orders the records by date created, while EntriesController#create
creates a new entry. No need to discuss the obvious and the responsibilities of EntriesController#destroy
:
While statistics_controller.rb
is responsible for calculating the weekly report, StatisticsController#index
gets the entries for the logged in user and groups them by week, employing the #group_by
method contained in the Enumerable class in Rails. It then tries to decorate the results using some private methods.
We don’t discuss the views much here, as the source code is self-explanatory.
Below is the view for listing the entries for the logged-in user (index.html.erb
). This is the template that will be used to display the results of the index action (method) in the entries controller:
Note that we are using partials render @entries
, to pull the shared code out into a partial template _entry.html.erb
so we can keep our code DRY and reusable:
The same goes for the _form
partial. Instead of using the same code with (new and edit) actions, we create a reusable partial form:
As for the weekly report page view, statistics/index.html.erb
shows some statistics and reports the weekly performance of the user by grouping some entries :
And finally, the helper for entries, entries_helper.rb
, includes two helpers readable_time_period
and readable_speed
which should make the attributes more humanly readable:
Nothing fancy so far.
Most of you will argue refactoring this is against the KISS principle and will make the system more complicated.
So does this application really need refactoring?
Absolutely not, but we’ll consider it for demonstration purposes only.
After all, if you check out the previous section, and the characteristics that indicate an app needs refactoring, it becomes obvious that the app in our example is not a valid candidate for refactoring.
Life Cycle
So let’s start by explaining the Rails MVC pattern structure.
Usually, it starts by the browser making a request, such as https://www.toptal.com/jogging/show/1
.
The web server receives the request and uses routes
to find out which controller
to use.
The controllers do the work of parsing user requests, data submissions, cookies, sessions, etc., and then ask the model
to get the data.
The models
are Ruby classes that talk to the database, store and validate data, perform the business logic, and otherwise do the heavy lifting. Views are what the user sees: HTML, CSS, XML, Javascript, JSON.
If we want to show the sequence of a Rails request lifecycle, it would look something like this:
What I want to achieve is to add more abstraction using plain old ruby objects (POROs) and make the pattern something like the following for create/update
actions:
And something like the following for list/show
actions:
By adding POROs abstractions we will assure full separation between responsibilities SRP, something that Rails is not very good at.
Guidelines
To achieve the new design, I’ll use the guidelines listed below, but please note these are not rules you have to follow to the T. Think of them as flexible guidelines that make refactoring easier.
- ActiveRecord models can contain associations and constants, but nothing else. So that means no callbacks (use service objects and add the callbacks there) and no validations (use Form objects to include naming and validations for the model).
- Keep Controllers as thin layers and always call Service objects. Some of you would ask why use controllers at all since we want to keep calling service objects to contain the logic? Well, controllers are a good place to have the HTTP routing, parameters parsing, authentication, content negotiation, calling the right service or editor object, exception catching, response formatting, and returning the right HTTP status code.
- Services should call Query objects, and should not store state. Use instance methods, not class methods. There should be very few public methods in keeping with SRP.
- Queries should be done in query objects. Query object methods should return an object, a hash or an array, not an ActiveRecord association.
- Avoid using Helpers and use decorators instead. Why? A common pitfall with Rails helpers is that they can turn into a big pile of non-OO functions, all sharing a namespace and stepping on each other. But much worse is that there’s no great way to use any kind of polymorphism with Rails helpers — providing different implementations for different contexts or types, over-riding or sub-classing helpers. I think the Rails helper classes should generally be used for utility methods, not for specific use cases, such as formatting model attributes for any kind of presentation logic. Keep them light and breezy.
- Avoid using concerns and use Decorators/Delegators instead. Why? After all, concerns seem to be a core part of Rails and can DRY up code when shared among multiple models. Nonetheless, the main issue is that concerns don’t make the model object more cohesive. The code is just better organized. In other words, there’s no real change to the API of the model.
- Try to extract Value Objects from models to keep your code cleaner and to group related attributes.
- Always pass one instance variable per view.
Refactoring
Before we get started, I want to discuss one more thing. When you start the refactoring, usually you end up asking yourself: “Is that really good refactoring?”
If you feel you are making more separation or isolation between responsibilities (even if that means adding more code and new files), then this is usually a good thing. After all, decoupling an application is a very good practice and makes it easier for us to do proper unit testing.
I won’t discuss stuff, like moving logic from controllers to models, as I assume you are doing that already, and you are comfortable using Rails (usually Skinny Controller and FAT model).
For the sake of keeping this article tight, I won’t discuss testing here, but that doesn’t mean you shouldn’t test.
On the contrary, you should always start with a test to make sure things are ok before moving forward. This is a must, especially when refactoring.
Then we can implement changes and make sure the tests all pass for the relevant parts of the code.
Extracting Value Objects
First, what is a value object?
Martin Fowler explains:
Value Object is a small object, such as a money or date range object. Their key property is that they follow value semantics rather than reference semantics.
Sometimes you may encounter a situation where a concept deserves its own abstraction and whose equality isn’t based on value, but on the identity. Examples would include Ruby’s Date, URI, and Pathname. Extraction to a value object (or domain model) is a great convenience.
Why bother?
One of the biggest advantages of a Value object is the expressiveness that they help achieve in your code. Your code will tend to be far clearer, or at least it can be if you have good naming practices. Since the Value Object is an abstraction, it leads to cleaner code and fewer errors.
Another big win is immutability. The immutability of objects is very important. When we are storing certain sets of data, which could be used in a value object, I usually don’t want that data to be manipulated.
When is this useful?
There is no single, one-size-fits-all answer. Do what is best for you and what makes sense in any given situation.
Going beyond that, though, there are some guidelines I use to help me make that decision.
If you think of a group of methods is related, with Value objects they are more expressive. This expressiveness means that a Value object should represent a distinct set of data, which your average developer can deduce simply by looking at the name of the object.
How is this done?
Value objects should follow some basic rules:
- Value objects should have multiple attributes.
- Attributes should be immutable throughout the object’s life cycle.
- Equality is determined by the object’s attributes.
In our example, I’ll create an EntryStatus
value object to abstract Entry#status_weather
and Entry#status_landform
attributes to their own class, which looks something like this:
Note: This is just a Plain Old Ruby Object (PORO) that does not inherit from ActiveRecord::Base
. We have defined reader methods for our attributes and are assigning them upon initialization. We also used a comparable mixin to compare objects using (<=>) method.
We can modify Entry
model to use the value object we created:
We can also modify the EntryController#create
method to use the new value object accordingly:
Extract Service Objects
So what is a Service object?
A Service object’s job is to hold the code for a particular bit of business logic. Unlike the “fat model” style, where a small number of objects contain many, many methods for all necessary logic, using Service objects results in many classes, each of which serves a single purpose.
Why? What are the benefits?
- Decoupling. Service objects help you achieve more isolation between objects.
- Visibility. Service objects (if well-named) show what an application does. I can just glance over the services directory to see what capabilities an application provides.
- Clean-up models and controllers. Controllers turn the request (params, session, cookies) into arguments, pass them down to the service and redirect or render according to the service response. While models only deal with associations, and persistence. Extracting code from controllers/models to service objects would support SRP and make the code more decoupled. The responsibility of the model would then be only to deal with associations and saving/deleting records, while the service object would have a single responsibility (SRP). This leads to better design and better unit tests.
- DRY and Embrace change. I keep service objects as simple and small as I can. I compose service objects with other service objects, and I reuse them.
- Clean up and speed up your test suite. Services are easy and fast to test since they are small Ruby objects with one point of entry (the call method). Complex services are composed with other services, so you can split up your tests easily. Also, using service objects makes it easier to mock/stub related objects without needing to load the whole rails environment.
- Callable from anywhere. Service objects are likely to be called from controllers as well as other service objects, DelayedJob / Rescue / Sidekiq Jobs, Rake tasks, console, etc.
On the other hand, nothing is ever perfect. A disadvantage of Service objects is that they can be an overkill for a very simple action. In such cases, you may very well end up complicating, rather than simplifying, your code.
When should you extract service objects?
There is no hard and fast rule here either.
Normally, Service objects are better for mid to large systems; those with a decent amount of logic beyond the standard CRUD operations.
So whenever you think that a code snippet might not belong to the directory where you were going to add it, it’s probably a good idea to reconsider and see if it should go to a service object instead.
Here are some indicators of when to use Service objects:
- The action is complex.
- The action reaches across multiple models.
- The action interacts with an external service.
- The action is not a core concern of the underlying model.
- There are multiple ways of performing the action.
How should you design Service Objects?
Designing the class for a service object is relatively straightforward, since you need no special gems, don’t have to learn a new DSL, and can more or less rely on the software design skills you already possess.
I usually use the following guidelines and conventions to design the service object:
- Do not store state of the object.
- Use instance methods, not class methods.
- There should be very few public methods (preferably one to support SRP.
- Methods should return rich result objects and not booleans.
- Services go under the
app/services
directory. I encourage you to use subdirectories for business logic-heavy domains. For instance, the fileapp/services/report/generate_weekly.rb
will defineReport::GenerateWeekly
whileapp/services/report/publish_monthly.rb
will defineReport::PublishMonthly
. - Services start with a verb (and do not end with Service):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
. - Services respond to the call method. I found using another verb makes it a bit redundant: ApproveTransaction.approve() does not read well. Also, the call method is the de facto method for lambda, procs, and method objects.
If you look at StatisticsController#index
, you’ll notice a group of methods (weeks_to_date_from
, weeks_to_date_to
, avg_distance
, etc.) coupled to the controller. That’s not really good. Consider the ramifications if you want to generate the weekly report outside statistics_controller
.
In our case, let’s create Report::GenerateWeekly
and extract the report logic from StatisticsController
:
So StatisticsController#index
now looks cleaner:
By applying the Service object pattern we bundle code around a specific, complex action and promote the creation of smaller, clearer methods.
Homework: consider using Value object for the WeeklyReport
instead of Struct
.
Extract Query Objects from Controllers
What is a Query object?
A Query object is a PORO which represent a database query. It can be reused across different places in the application while at the same time hiding the query logic. It also provides a good isolated unit to test.
You should extract complex SQL/NoSQL queries into their own class.
Each Query object is responsible for returning a result set based on the criteria / business rules.
In this example, we don’t have any complex queries, so using Query object won’t be efficient. However, for demonstration purpose, let’s extract the query in Report::GenerateWeekly#call
and create generate_entries_query.rb
:
And in Report::GenerateWeekly#call
, let’s replace:
def call
@user.entries.group_by(&:week).map do |week, entries|
WeeklyReport.new(
...
)
end
end
with:
def call
weekly_grouped_entries = GroupEntriesQuery.new(@user).call
weekly_grouped_entries.map do |week, entries|
WeeklyReport.new(
...
)
end
end
The query object pattern helps keep your model logic strictly related to a class’ behavior, while also keeping your controllers skinny. Since they are nothing more than plain old Ruby classes, query objects don’t need to inherit from ActiveRecord::Base
, and should be responsible for nothing more than executing queries.
Extract Create Entry to a Service Object
Now, let’s extract the logic of creating a new entry to a new service object. Let’s use the convention and create CreateEntry
:
And now our EntriesController#create
is as follows:
def create
begin
CreateEntry.new(current_user, entry_params).call
flash[:notice] = 'Entry was successfully created.'
rescue Exception => e
flash[:error] = e.message
end
redirect_to root_path
end
Move Validations into a Form Object
Now, here things start to get more interesting.
Remember in our guidelines, we agreed we wanted models to contain associations and constants, but nothing else (no validations and no callbacks). So let’s start by removing callbacks, and use a Form object instead.
A Form object is a Plain Old Ruby Object (PORO). It takes over from the controller/service object wherever it needs to talk to the database.
Why use Form objects?
When looking to refactor your app, it’s always a good idea to keep the single responsibility principle (SRP) in mind.
SRP helps you make better design decisions around what a class should be responsible for.
Your database table model (an ActiveRecord model in the context of Rails), for example, represents a single database record in code, so there is no reason for it to be concerned with anything your user is doing.
This is where Form objects come in.
A Form object is responsible for representing a form in your application. So each input field can be treated as an attribute in the class. It can validate that those attributes meet some validation rules, and it can pass the “clean” data to where it needs to go (e.g., your database models or perhaps your search query builder).
When should you use a Form object?
- When you want to extract the validations from Rails models.
- When multiple models can be updated by a single form submission, you might want to create a Form object.
This enables you to put all the form logic (naming conventions, validations, and so on) into one place.
How do you create a Form object?
- Create a plain Ruby class.
- Include
ActiveModel::Model
(in Rails 3, you have to include Naming, Conversion, and Validations instead) - Start using your new form class as if it were a regular ActiveRecord model, the biggest difference being that you cannot persist the data stored in this object.
Please note that you can use the reform gem, but sticking with POROs we’ll create entry_form.rb
which looks like this:
And we will modify CreateEntry
to start using the Form object EntryForm
:
class CreateEntry
......
......
def call
@entry_form = ::EntryForm.new(@params)
if @entry_form.valid?
....
else
....
end
end
end
Note: Some of you would say that there’s no need to access the Form object from the Service object and that we can just call the Form object directly from the controller, which is a valid argument. However, I would prefer to have clear flow, and that’s why I always call the Form object from the Service object.
Move Callbacks to the Service Object
As we agreed earlier, we don’t want our models to contain validations and callbacks. We extracted the validations using Form objects. But we are still using some callbacks (after_create
in Entry
model compare_speed_and_notify_user
).
Why do we want to remove callbacks from models?
Rails developers usually start noticing callback pain during testing. If you’re not testing your ActiveRecord models, you’ll begin noticing pain later as your application grows and as more logic is required to call or avoid the callback.
after_*
callbacks are primarily used in relation to saving or persisting the object.
Once the object is saved, the purpose (i.e. responsibility) of the object has been fulfilled. So if we still see callbacks being invoked after the object has been saved, what we are likely seeing is callbacks reaching outside of the object’s area of responsibility, and that’s when we run into problems.
In our case, we are sending an SMS to the user after we save an entry, which is not really related to the domain of Entry.
A simple way to solve the problem is by moving the callback to the related service object. After all, sending an SMS for the end user is related to the CreateEntry
Service Object and not to the Entry model itself.
In doing so, we no longer have to stub out the compare_speed_and_notify_user
method in our tests. We’ve made it a simple matter to create an entry without requiring an SMS to be sent, and we’re following good Object Oriented design by making sure our classes have a single responsibility (SRP).
So now our CreateEntry
looks something like:
Use Decorators Instead of Helpers
While we can easily use Draper collection of view models and decorators, I’ll stick to POROs for the sake of this article, as I’ve been doing so far.
What I need is a class that will call methods on the decorated object.
I can use method_missing
to implement that, but I’ll use Ruby’s standard library SimpleDelegator
.
The following code shows how to use SimpleDelegator
to implement our base decorator:
% app/decorators/base_decorator.rb
require 'delegate'
class BaseDecorator < SimpleDelegator
def initialize(base, view_context)
super(base)
@object = base
@view_context = view_context
end
private
def self.decorates(name)
define_method(name) do
@object
end
end
def _h
@view_context
end
end
So why the _h
method?
This method acts as a proxy for view context. By default, the view context is an instance of a view class, the default view class being ActionView::Base
. You can access view helpers as follows:
_h.content_tag :div, 'my-div', class: 'my-class'
To make it more convenient, we add a decorate
method to ApplicationHelper
:
module ApplicationHelper
# .....
def decorate(object, klass = nil)
klass ||= "#{object.class}Decorator".constantize
decorator = klass.new(object, self)
yield decorator if block_given?
decorator
end
# .....
end
Now, we can move EntriesHelper
helpers to decorators:
# app/decorators/entry_decorator.rb
class EntryDecorator < BaseDecorator
decorates :entry
def readable_time_period
mins = entry.time_period
return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60
Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe
end
def readable_speed
"#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe
end
end
And we can use readable_time_period
and readable_speed
like so:
# app/views/entries/_entry.html.erb
- <td><%= readable_speed(entry) %> </td>
+ <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td>
+ <td><%= decorate(entry).readable_time_period %></td>
Structure After Refactoring
We ended up with more files, but that’s not necessarily a bad thing (and remember that, from the onset, we acknowledged that this example was for demonstrative purposes only and was not necessarily a good use case for refactoring):
app
├── assets
│ └── ...
├── controllers
│ ├── application_controller.rb
│ ├── entries_controller.rb
│ └── statistics_controller.rb
├── decorators
│ ├── base_decorator.rb
│ └── entry_decorator.rb
├── forms
│ └── entry_form.rb
├── helpers
│ └── application_helper.rb
├── mailers
├── models
│ ├── entry.rb
│ ├── entry_status.rb
│ └── user.rb
├── queries
│ └── group_entries_query.rb
├── services
│ ├── create_entry.rb
│ └── report
│ └── generate_weekly.rb
└── views
├── devise
│ └── ..
├── entries
│ ├── _entry.html.erb
│ ├── _form.html.erb
│ └── index.html.erb
├── layouts
│ └── application.html.erb
└── statistics
└── index.html.erb
Conclusion
Even though we focused on Rails in this blog post, RoR is not a dependency of the described service objects and other POROs. You can use this approach with any web framework, mobile, or console app.
By using MVC as the architecture of web apps, everything stays coupling and makes you go slower because most changes have an impact on other parts of the app. Also, it forces you to think where to put some business logic – should it go into the model, the controller, or the view?
By using simple POROs, we have moved business logic to models or services that don’t inherit from ActiveRecord
, which is already a big win, not to mention that we have a cleaner code, which supports SRP and faster unit tests.
Clean architecture aims to put the use cases in the center/top of your structure, so you can easily see what your app does. It also makes it easier to adopt changes since it is much more modular and isolated.
I hope I demonstrated how using Plain Old Ruby Objects and more abstractions decouples concerns, simplifies testing and helps produce clean, maintainable code.
Further Reading on the Toptal Blog:
Eqbal Quran
Amman, Amman Governorate, Jordan
Member since June 13, 2014
About the author
Eqbal is a senior full-stack developer with more than a decade of experience working in web and mobile development.
Expertise
PREVIOUSLY AT