Calling Services Object without instance in Ruby
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
.
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')