April 29, 2022
Why do I use Service Object?
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 :)