Chamando Services Object sem instância in Ruby
Quando chamamos o método público de uma classe em Ruby, precisamos antes instanciá-la com o método construtor .new
. Por exemplo, se precisamos chamar os métodos .price
e .type
da classe Product
a gente faria assim:
product = Product.new(params)
puts product.price
puts product.type
A partir da instancia de produto, eu posso acessar todos os seus métodos públicos.
Quando criamos uma classe Service Object, estamos criando um trabalhador, um funcionário que vai executar uma ação. É natural chamarmos essa classe com se fosse alguém que vai fazer algo. Exemplo: BookDestroyer, PluginConstructor, Finalizator.
O serviço tem um algorítimo com uma série de execuções que entrega um trabalho. Vamos ao exemplo do ProductCreator
.
ProductCreator.new(params).call
Note que instanciamos a classe e já chamamos seu único método publico .call
. É normal chamamos assim o método único de uma classe Service. Não é obrigatório, apenas uma convenção já praticada pelos desenvolvedores. Isso acontece porque o service só fará uma ação, que é a de executar o que a classe propõe. Desta forma seria interessante que não precisássemos instanciar a classe, para economizar espaço.
ProductCreator.call(params)
Convenhamos, é mais simples, fácil de ler e suave.
Você deve estar pensando, mas para fazer isso é só criar um método de class, assim não precisamos de instancias. Está certo em pensar assim, porém vamos fazer algo mais bem elaborado. Se a gente criasse apenas um método de classe, não estaríamos aproveitando todo potencial do paradigma orientado a objeto. Seria o mesmo que criarmos apenas um module
e colocarmos uma função call, não é a nossa proposta.
Modelando um Serviço
class NameCreator
def initialize(nome)
@nome = nome
end
def call
puts "Nome: #{@nome}"
end
end
NameCreator.new('Thiago').call
Aqui temos uma classe que recebe um nome e imprime uma string
contendo o nome passado no argumento. Se você executar este código no IRB ele irá retornar isto:
Nome: Thiago
=> nil
Chamada direta
Vamos adicionar à nossa classe um método de classe chamado call e dentro dela vamos instanciar a própria classe.
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')
Note que não deixamos de instanciar a classe, apenas é instanciada automaticamente pelo método de classe. Assim quando chamamos o método de classe .call
ele estará criando a instancia e chamando o método call da instancia. new(nome).call
Se testar este código no IRB o resultado será o mesmo do anterior.
Reaproveitando o método call
Como nosso intuito é usar esse padrão em todas os nossos serviços, vamos extrai o método de classe .call
para uma classe abstrata.
class ApplicationSevices
def self.call(name)
new(name).call
end
end
Ainda temos um problema! O parâmetro name
só se aplica à classe NameCreator
. Precisamos poder passar qualquer parâmetro no método de classe. Neste caso podemos passar um splat operator *args
.
class ApplicationSevices
def self.call(*args)
new(*args).call
end
end
O splat operator vai cria um array de parâmetros. Isso vai resolver o problema da diferença de parâmetros de cadas classe.
A partir do Ruby 2.7, podemos contar com uma nova sugar syntax chamada de triple-dots.
class ApplicationSevices
def self.call(...)
new(...).call
end
end
Agora nossa classe abstrata está pronta.
Herdando a nossa classe abstrata
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')
Agora você pode herdar em todos os seus Services Object.
Outro caminho
Outra forma de fazer, caso não queira trabalhar com herança, seria estendendo um modulo com o método call.
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')