Mateus Nava

Mateus Nava

April 29, 2022

Why do I use Service Object?

By Xavi Cabrera (unsplash.com)
By Xavi Cabrera (unsplash.com)
Service Object is about a class (PORO - Pure Old Ruby Object) with just only one public method, normally this method is called call or perform, for example, imagine that when the user completes the registration on your app, you need to create a subscription, this subscription has to send an email and create an invoice:

class CreateSubscription
  attr_reader :user

  def initialize(user:)
    @user = user
  end

  def call
    send_email

    create_invoice
  end

  private

  def send_email
     SendEmail.new(subject: 'Subscription', content: '..',  to: '...').call
  end

  def create_invoice
     CreateInvoice.new(....).call
  end
end

The first thing I really like about this type of construction is the expressiveness of the code, because of the private methods, the public method is very easy to read and understand. The second thing is that service objects are small classes with a single responsibility, and this is forced by the specific context in the class name (CreateSubscription, for example). You MUST have only one subject and because of that, you can't write code about other things.

I love constructing small pieces of code that you can easily combine to get a major feature (lego stuff) I always remember UNIX commands, UNIX commands are small and you can combine all with pipe (|). This is a powerful and elegant solution because you can have a LOT of possibilities. For example, you can read a file with cat and filter results with grep:
cat file.rb | grep
Or you can play a song in the remote machine with cat, mplayer, ssh:
cat song | ssh user@host "mplayer -cache 8192 – "

The other very good point is that it is very easy to write tests for these classes. You can do a unit test to make sure the service object does the job well, and in other parts of the software just check if the service is called. with correct arguments, using mock and stub. For example:
RSpec.describe CreateSubscription, type: :service do
  described 'Subscribing user' do
    context 'When user is valid' do
      ...
    end

    context 'When user is NOT valid' do
      ...
    end
    ...
  end
end

And in other parts of the software, like in a model, you can only check like this:
RSpec.describe User, type: :model do
  describe 'Saving user' do
    context 'When user is a new user' do
      let(:create_subscription_instace) { instance_double(CreateSubscription, call: nil) }

      before do
        allow(CreateSubscription).to receive(:new).with(user: subject).and_return(create_subscription_instace)
      end

      it 'subscribes' do
        subject.save

        expect(create_subscription_instace).to have_received(:call)
      end
    end
  end
end

That's all :)