Código organizado, reutilizável e fácil de testar utilizando Ruby Service Objects

Opa! Tudo bem? A missão de hoje é tentar simplificar as coisas quando falamos sobre Ruby Service Objects, abordar de uma forma simples e falar dos conceitos, ideias de uso, convenções e dicas pra quem quer ou precisa utilizá-los no seu projeto. Enjoy it!

Antes de tudo, um tempo atrás assisti uma sequência de vídeos no Youtube gravados pelo Rodrigo Branas e achei sensacional a conexão que ele fez entre a satisfação que sentimos na nossa profissão e a qualidade do serviço que prestamos, o que faz total sentido se pararmos pra pensar na qualidade do serviço/produto que entregamos e o tempo que se leva pra realizar esse processo.

Depois de assistir esses vídeos, percebi ainda mais a importância da qualidade do código que escrevo e as boas práticas que diariamente ouço falar mas que algumas vezes por não dar tanta importância ou até mesmo pela necessidade de entrega em um curto espaço de tempo, acabo deixando de lado, enfim… vamos ao que interessa!

Geralmente quando estamos iniciando, ouvimos muito falar das convenções do Ruby on Rails, uma das mais famosas é a “Fat Model, Skinny Controller“, que segundo alguns comentários e posts, trata-se de mover as lógicas de negócios do Controller para o Model, deixando o Controller basicamente com a função de tratar as requisições e respostas, o que de cara é uma coisa muito boa.

Controller vs Model

Geralmente não é uma boa prática deixar lógica de negócios na camada dos controllers, então a primeira coisa que pensamos é “Opa! vou tirar isso daqui e jogar no Model”, dessa forma, estamos transferindo mais responsabilidades para o Model, consequentemente deixando-o mais “inchado”, quanto mais esse processo for feito, maior a probabilidade da nossa classe estar com uma grande quantidade de código, difícil de utilizar, testar e de efetuar manutenções.

Sabendo disso, vamos direto ao ponto e falar sobre Ruby Service Objects!

Ruby Service Objects ou apenas Services como gosto de chamartambém são conhecidos como PORO (Plain Old Ruby Object), termo bastante conhecido na comunidade Ruby, talvez você já tenha ouvido falar. 🙂

Mas o que é de fato um Ruby Service ObjectBom, vamos tratar os Ruby Service Objects como uma forma de desmembrar lógicas de negócio em Classes e Métodos, visando sempre facilitar o uso, a manutenção e a criação de testes para nosso código, consequentemente deixando a vida dos Controllers e Models mais simples, já que eles não serão mais os responsáveis por manter essas lógicas.

No artigo do Tomek ele indica 3 características geralmente encontradas nos Services:

  • Recebe os parâmetros
  • Executa as tarefas
  • Retorna um resultado

Parece que estamos descrevendo uma simples função, mas a idéia é justamente essa, simplificar!. Lógico que dependendo das regras de negócio incluídas nos Services, eles podem ter características diferentes, mas na grande maioria e independente das tarefas que realizam, a estrutura não muda.

Como é comum falarmos das convenções do Rails, existem também algumas convenções para o uso dos Services, nesse post vou abordar duas e que acho bastante relevantes. A primeira delas é o princípio da responsabilidade única ou seja, nosso Service deve manter o foco em uma única tarefa ou ação e ser responsável apenas por ela, esse é um princípio SOLID.

Leia mais sobre SOLID.

A segunda é: “tente o máximo possível ter apenas um método público“, ou seja, vamos deixar um método público que seja responsável por efetuar ou delegar as tarefas do nosso Service, ou seja…

Vamos supor que temos um Service responsável pela lógica de emissão de nota fiscal do nosso app, dentro desse Service um método público chamado charge é responsável por chamar outros dois métodos, que podem ter o controle de acesso privado (private). O primeiro é responsável por efetuar uma requisição para uma API de emissão de nota fiscal e o segundo por gravar os dados da resposta da API no nosso banco.

Só pra reforçar, convenções não são regras, o seu programa não vai parar de funcionar se você não aplicar, mas são dicas interessantes pra manter um código que faça sentido e sempre organizado como já citei algumas vezes.

Então pra deixar a coisa menos abstrata, imagine uma obra, seu app é um encarregado de obras e precisa sempre delegar tarefas para os seus pedreiros, da mesma forma os pedreiros também precisam delegar algumas atividades pra seus ajudantes, ou seja, nossos Models e Controllers seriam nossos pedreiros e os ajudantes seriam os Services.

Descomplicando mais ainda, vamos supor que um dos nossos pedreiros (Model/Controller) está com as funções de emassar a parede da sala e retirar o resto da massa que cair no chão. São duas coisas difíceis de se fazer ao mesmo tempo, dessa forma o nosso amigo pedreiro (Model/Controller) pode passar a tarefa de retirar a massa que cair no chão para o seu ajudante, o Service

Agora, imagine um app com pagamentos e notas fiscais, digamos que toda vez que um pagamento é criado é necessário criar uma nota fiscal associada a esse pagamento, além disso o status da nota depende do valor total do pagamento, ou seja, se o valor do pagamento for menor que 1k, será “issued“, se for maior será “pending“, supondo que pagamentos acima de mil reais precisam ser verificados por um gerente.

Então nossos Models serão:

Lógica dentro do payments_controller:

Como é um exemplo simples, a lógica é pequena, mas poderia ser bem maior e mais complexa ou pode crescer conforme o tempo e as futuras manutenções que podem existir no código, por isso a importância de extrair essa lógica do Controller e colocar dentro de um Service.

E onde devem ficar os Services dentro do app? Bom, eu gosto de criar uma pasta dentro da pasta app chamada services, mas isso é de cada um, é comum encontrar Services dentro da pasta Models, mas nesse caso vamos utilizar o caminho app/services.

Nomenclatura das classes também é importante, deixar o nome do Serviceassociado a tarefa que o mesmo executa junto com a palavra service ao fim da classe é bem comum e interessante.

Em relação a estrutura, o Service vai receber os parâmetros, executar sua tarefa e retornar algo ao Controller, nesse caso acho interessante retornar o status do Invoice criado, assim podemos modificar a notificação que vai para a View. O método “charge” fica responsável por chamar o método privado create_invoice, que de fato cria nosso ServiceInvoice e retorna o status que acaba de ser criado:

Como o Service retorna o valor do status, podemos verificar no Controller e retornar uma mensagem mais dinâmica depois da criação do pagamento.

Instanciamos nossa classe passando o parâmetro necessário, no caso o nosso @payment.id e já indicamos que o método charge do nosso CreateInvoiceService deve ser executado.

Criando um pagamento com o valor acima de 1k:

Mensagem personalizada conforme valor retornado

Agora criando um abaixo de 1k:

Mensagem personalizada conforme valor retornado.

Removemos a lógica de dentro do nosso Controller, nem passou pela nossa cabeça de coloca-la dentro de algum Model e agora temos uma classe de fácil manuseio, manutenção e simples pra testarmos. E já que falamos em teste:

Então é isso, uma breve iniciação ao mundo dos Ruby Service Objects, espero que gostem e bons estudos!

Código fonte.