Calling Services Object without instance in Ruby

Thiago Lima
4 min readAug 11, 2023

--

When we call a public method of a class in Ruby, we usually need to instantiate it first using the constructor method .new. For instance, if we need to call the methods .price and .type of the Product class, we would do it like this:

product = Product.new(params)
puts product.price
puts product.type

From the product instance, I can access all its public methods.

When we create a Service Object class, we are essentially creating a worker, an entity that will perform an action. It’s natural to call this class as if it were someone who will do something. For example: BookDestroyer, PluginConstructor, Finalizator.

Our Working Service
Our Working Service

The service contains an algorithm with a series of executions that deliver a task. Let’s consider the example of ProductCreator.

ProductCreator.new(params).call

Notice that we instantiate the class and immediately call its only public method .call. It's common to call the unique method of a Service class like this. It's not obligatory, but a convention practiced by developers. This is because the service is meant to perform a single action, which is to execute what the class proposes. Therefore, it would be advantageous if we didn't have to instantiate the class, in order to save space.

ProductCreator.call(params)

Let’s admit it, it’s simpler, easier to read, and smoother. You might be thinking, “But to do this, we just need to create a class method, so we don’t need instances.” You’re right to think that way, but let’s create something more elaborate. If we were to create only a class method, we wouldn’t be leveraging the full potential of the object-oriented paradigm. It would be like creating only a module and placing a call function in it – that's not our intention.

Modeling a Service

class NameCreator
def initialize(nome)
@nome = nome
end

def call
puts "Nome: #{@nome}"
end
end

NameCreator.new('Thiago').call

Here, we have a class that takes a name and prints a string containing the name passed as an argument. If you execute this code in the IRB, it will return:

Nome: Thiago
=> nil

Direct Invocation

Let’s add a class method called call to our class, and inside it, we'll instantiate the class itself.

class NameCreator
def self.call(nome)
new(nome).call
end

def initialize(nome)
@nome = nome
end

def call
puts "Nome: #{@nome}"
end
end

NameCreator.call('Thiago')

Notice that we haven’t stopped instantiating the class — it’s just being automatically instantiated by the class method. So when we call the class method .call, it creates an instance and calls the call method of the instance (new(name).call).

If you test this code in IRB, the result will be the same as before.

Reusing the call Method

Since our intention is to use this pattern in all our services, let's extract the class method .call to an abstract class.

class ApplicationSevices
def self.call(name)
new(name).call
end
end

However, we still have an issue! The name parameter only applies to the NameCreator class. We need to be able to pass any parameter to the class method. In this case, we can use a splat operator *args.

class ApplicationSevices
def self.call(*args)
new(*args).call
end
end

The splat operator creates an array of parameters. This resolves the problem of different parameters for each class.

Starting from Ruby 2.7, we can also use a new sugar syntax called triple-dots.

class ApplicationSevices
def self.call(...)
new(...).call
end
end

Now our abstract class is ready.

Inheriting from Our Abstract Class

class ApplicationSevices
def self.call(...)
new(...).call
end
end

class NameCreator < ApplicationSevices
def initialize(nome)
@nome = nome
end

def call
puts "Nome: #{@nome}"
end
end

NameCreator.call('Thiago')

Now you can inherit this in all your Service Objects.

Another way

Another way to achieve this, if you don’t want to work with inheritance, is by extending a module with the call method.

module Called
def call(...)
new(...).call
end
end

class NameCreator
extend Called

def initialize(nome)
@nome = nome
end

def call
puts "Nome: #{@nome}"
end
end

NameCreator.call('Thiago')

--

--

Thiago Lima
Thiago Lima

Written by Thiago Lima

Hello! I’m Thiago Lima, I’m maried, I have a son named Isaac. I’m software engineer and I programming in Ruby on Rails.

No responses yet