Welcome! I am so glad you came here to read about design patterns. With the Ruby on Rails community having such an abundance of newer developers and newer self-taught developers I thought it was excellent time to share some real world usage of design patterns. I picked the ones that have helped me to save dozens of applications and simplified many issues.
Today, day 1 is going to be all about service objects. We will talk about these again as the series continues. You can expect to read about organizers, form objects, query objects, presenters, the adapter pattern, and factories. There are definitely more patterns out there to read about and people like Martin Fowler or Robert C. Martin, but these are a nice start. Additionally they can be used right away on most RoR codebases!
What I hope to help you to accomplish is to gain a rudimentary understanding of these patterns and some good ways to apply them in. Ways that bring up the health of your code instead of adding complexity. I suppose however than when prescribing tools one must also mention the caveats to maintain intellectual integrity. And so, one of the biggest caveats to design patterns is that they are applied improperly every day. They are just tools that solve known problems in programming, not silver bullets. If you think about them critically and use them judiciously however I am sure you will be just fine.
These are the droids you are looking for
Most education starts with a good definition, so let’s start there. As I understand it a Service Object implements the system’s or user’s interactions and usually involves more than one model. What does this mean? Well in the simplest forms it means that service objects are classes that achieve a discrete business or domain objective. Things like registering a user, syncing an object with an API, or maybe applying tags to an article based on some machine learning analysis. Lets take a piece at the first usage; implementing a user’s actions.
class RegisterUser def initialize(params) @params = params end def call(&block) @user = User.create!(@params) send_user_to_firebase(@user) send_user_intercom(@user) if block_given? block.call(@user) else true end end private def send_user_to_fire_base(user) # Sends the user to the Firebase service end def send_user_intercom(user) # Sends the user to the Intercom service end end
As you can see, registering a user can get pretty complicated. If you have tried putting all of this in a controller like I used to then you know how hard it is to test some of this stuff. You also may have found out that it is simply not very reusable in things like an API user registration controller or when setting up an admin. This solution allows us to encapsulate what it means to register a user in a clean and reusable way. For fun, you may have noticed the block being passed to the #call method. This is a little trick I user all the time to execute custom code after a service succeeds because it is NOT reusable. Let’s take a quick look at a controller’s use.
class UserController < ApplicationController def create service = RegisterUser.new(params[:user]) @user = nil service.call do |user| @user = user UserMailer.welcome_email(user).deliver_now! end if @user flash[:success] = "Thank you for signing up!" redirect_to user_email_confirmation_path(@user) else render :new end end end
As you can see from this example the block to send an email was utilized so we could set an instance variable for a particular use case and more specifically to send an email to the user. There are so many times that sending email based on one action is not reusable, so this is a spiffy little trick to help fix that particular issue.
The next and final application is using a service object to perform a system job or operation. Let’s look at a class as an example that is responsible for syncing a user with Quickbooks.
class SyncUserWithQuickbooks def initialize(user) @user = user end def call return if @user.last_updated_with_qb_at < quickbooks_user.last_change_at success = Quickbooks::Customers.update(quickbooks_user, @user.quickbooks_attributes) if success @user.last_updated_with_qb_at = Time.zone.now @user.save! true else false # let the client know the sync failed. end end private def quickbooks_user @quickbooks_user ||= Quickbooks::Customers.find(@user.quickbooks_id) end end
While both of these classes are fairly similar (you will note the same public interface of #initialize and #call), this one performs a job of the system while the former performs a user’s action. In my experience both of these are fairly reusable, however reuse is not necessarily the only reason to craft service classes. They come in handy to encapsulate a business or piece of domain logic that is in one isolated place that you can iterate on as time flies by. And you can best believe you will either be removing these classes, drastically modifying them, or even making them part of an Organizer (for discussion next time) in order to accomplish the same basic concept as your applications grow.
The dark side of the force is so seductive
And you thought you were only going to read one Star Wars related subtitle…Anyway, it is important to point out that these patterns all come with disclaimers. I would also argue that there are some good general rules on how and when to use them.
Manage Those Transactions
If your service objects persists more than 1 model, then please for the love of all that is holy wrap that stuff in a transaction. You may or may not care now, but I promise you will later. It will be on one of those dark nights where you are looking at the DB and notice lots of rogue records or maybe even when a user can’t register because their user was saved but the complimentary sync to Firebase failed. Transactions will go a long way to save you from producing bad data you have to cleanup later.
One Size Does NOT Fit all
If you find that your service object does a few things or even many or perhaps just calls other service objects that perform business logic, then you probably don’t need a service object. You are probably looking for an organizer, but we will talk about that next time. For now just hold your breath and try to wait patiently.
We Need An Injection… Stat
For your sanity and ease of testing, pass objects into your service classes instead of integers or strings for the sake of lookup. This way you are not prohibited from using duck-typed classes and your in tests you can easily mock those arguments. It will save you a load of trouble and boilerplate stubbing of ActiveRecord lookups.
You Must Unlearn What You Have Learned
When you jumped into Rails you learned all about MVC and everything fit in one of those layers. It was just too easy to throw domain logic on models via quick little methods, and it was so slick to write that 60 line search method in your controller. Some of these new patterns will make sense, and others will challenge your idea of what it means to develop a clean Rails app. You may find that you have a services directory in your application that becomes just as big as your models or controllers directory. This is ok. It means that you are trimming the fat from those controllers and models and instead are creating isolated, reusable, pieces of domain logic that are far easier to test and maintain.
That’s a Wrap
Thanks so much for hanging in there with me through my meandering. In my next article we will bite from the coding tree of
knowledge and pluck the ever-so-delicious organizer fruit from a low hanging branch. I won’t tease you now, but seriously they are awesome. See you soon!
PS: If you want to troll me or send me your thoughts on this article feel free to email me.