Blog

Keeping your Classes Small and Maintainable with Service Objects

Placeholder Avatar
Sameera Gayan
November 19, 2015

Keeping your Classes Small and Maintainable with Service Objects

If you are familiar with Rails, you know that it has a predefined directory structure. Rails was one of the early adaptors of the MVC (Model, View, Controller) pattern. In fact that is one of the key strengths of the framework; it is easy to learn since everything has its own place. This is all well and good if your Rails app is relatively a small one - but when your app starts growing with features and functionality, soon you will find some code snippets that don’t seem to fit into the standard Rails directory structure. This is when these methods tend to get pushed to the ActiveRecord models. However, not all of these methods directly relate to a model; often these methods contain some validations required by the business/client.

In such scenarios, implementing the logic via service objects or services would be a good idea.

Simply put, a “service object” is a Ruby class that contains some of the application’s business logic without pushing it to the ActiveRecord layer. Often, a Service is a PORO (Plain Old Ruby Object).

Mmm.. that’s not very helpful - Let’s try to explain with an example

Let’s say you are developing a ticket booking system in Rails. So your client has the following requirement:

“After a user purchases a ticket he/she should receive the ticket by email.” Then you’ll say, “Okay sure, that’s not a problem, let’s send an email after the ticket has been purchased”, and you will start with adding the following code into your controller:

(Please note that I’m assuming that all the controllers/models and methods are already implemented at this stage)

```ruby # app/controllers/tickets_controller.rb #create

if @ticket.save Email.send_email_to_user(current_user.email, @ticket) # code else # code end ```

So, you get it working and the client is happy, and all is well. But then a few days later the client adds another requirement:

“We should send the user not only an email, but also an SMS if we have their mobile number.” Ahh… now you have two main options for tackling this problem: One option would be to add a condition in the tickets_controller itself and call the SMS sending function. But waitno!!! That will make your controller fat, which is against the Rails philosophy of skinny controllers. The second option would be to create a separate method and move the email and SMS sending into that. So you create a method in the User model and move the code there.

```ruby # app/models/user.rb class User < ActiveRecord::Base # code

# this method is called from the controller
def send_notifications(ticket)
  Email.send_email_to_user(self.email, ticket)
  Sms.send(self.mobile, ticket) if self.mobile
end   end ```

You’ve saved the day! Now, whenever the client needs to add a new notification channel, all you have to do is add it to the send_notifications method. However, as these notifications and business logics are added over time, you will notice that:

1) Even though the notifications are sending to a user, it’s not core functionality of the User class. 2) Adding more and more business logic inside the User model makes it too bulky, and will eventually make the code too hard to maintain. 3) Testing the model/method without stabbing/mocking the other models becomes too difficult.

Lets try to understand these points:

(1) If you look closely at the send_notifications method, sending a notification is actually a supporting function for the User entity, as opposed to a core one. Core functions would be for example, adding a new user or deleting an existing user. But where else can we implement this method? With the current Rails stack the User model seems to be the only logical place.

(2) When the client wants to add to the business logic, our User model ends up getting more and more functionality and soon becomes too big and hard to maintain. Think again about the following client requirement: > “We need to send the User an SMS if they have enabled the option send me the ticket link via SMS.” In your application’s business context, sending a notification after validating all these business rules should fall under the notification domain, not the user domain.

(3) When you writing unit tests for the User model, you now have to think about resulting method calls to these other classes, like Email. This becomes more obvious if you have used callbacks (like after_save), because you will have to stub the method of the Email class to test the User model.

Too much required data setup for a unit test starts to indicate a code smell. In this case it’s because our User model is doing things not directly related to a user’s domain. When you start running into these dilemmas, it’s usually an indication that you should rethink your code structure. In this particular case we can move the notification logic to a Service Object.

Ok… Let’s build a ‘service object’

Even though Rails comes with a default directory structure, nothing prevents you from adding new directories. So first let’s add a directory called services under app. This will be the container for our service classes - just like Rails has a Model directory for ActiveRecord models.

At this point, you can decide how you would like to organise your directory/file structure. Some may prefer to have subfolders for each service type and some may just add all the services under the services folder. It’s really up to you. Once you have created the folder let’s add our notification service - I’m going to call it ‘UserNotificationService’; so create a file called user_notification_service.rb under the services folder.

Service Folder structure Now let’s add our new folder to load path, so that Rails knows about it when the app starts:

ruby # config/application.rb module <Rails Application Name> class Application < Rails::Application # code config.autoload_paths << Rails.root.join('services') # code end end

Now we need to write the code. As I mentioned before, services are normally _PORO_s, it’s totally up to you to decide how to implement them. The following is one way of doing it (please note that exception handling is not included for the sake of simplicity):

ruby # Sending notifications to users after a ticket has been purchased class UserNotificationService def initialize(user) @user = user end # send notifications def notify(ticket) Email.send_email_to_user(@user.email, ticket) Sms.send(@user.mobile, ticket) if @user.mobile end end

As you can see, we can now add more and more business rules to our notification feature without polluting the User ActiveRecord model. This also gives you a clearer separation between your business logic and ORM layer (ie, ActiveRecord). The service is now called like the following:

```ruby # app/controllers/tickets_controller.rb #create

if @ticket.save UserNotificationService.new(current_user).notify(@ticket) # code else # code end ```

“That’s all good” you might say, but how do you test them?

Testing services is not hard at all. I’m using RSpec for my example, but you can use any Rails testing framework that you prefer.

In testing the service object, we only test logic defined in the class itself, and method calls out to other classes; not how other classes implement their own logic. For example, we only test to see if the UserNotificationService calls send_email_to_user in the Email class. The actual implementation of send_email_to_user should be tested in in the spec/test of the Email class.

To do this, create a directory called services inside the spec directory. Then create a file called user_notification_service_spec.rb. Service spec structure We can now spec out the logic of the service object like so:

ruby require 'rails_helper' describe UserNotificationService do let(:user) { FactoryGirl.create(:user, mobile: mobile) } let(:ticket) { FactoryGirl.create(:ticket) } subject(:notification) do UserNotificationService.new(user).notify(ticket) end context 'when the user does not have a mobile number' do let(:mobile) { nil } it 'send an email' do expect(Email).to receive(:send_email_to_user) notification end it 'does not send an SMS' do expect(Sms).to_not receive(:send) notification end end end

When to Use ‘service objects’

So, when exactly do you use service objects? The truth is that there is no hard and fast rule. Normally the services are better for mid to large systems; those with a decent amount of logic beyond the standard CRUD to create, update, delete and view resources. 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 re-think and see if it should go to a service instead.

The Conclusion…

Phew Implementing a service is not too hard, right? Service objects are a great way to organise your code and business logic. They will make your code more readable, maintainable and testable. So basically, from now on, try to use services objects whenever applicable, remembering to keep your services clean and manageable. Happy coding!