Pessoal, puxei o Raul pra conversar sobre os pontos que ele levantou, acho pertinente compartilhar aqui, e talvez até alterar a decisão tomada anteriormente. Vai ficar um post meio grande, mas vamo nessa.
## Com a decisão atual (passando pelo construtor), teriamos uma classe no seguinte estilo:
``` Ruby=
class DecidePendencyTypeService
def initialize(author:, pendency_type: nil)
@author = author
@pendency_type = pendency_type
end
def call
return @pendency_type if pendency_type_known?
define_pendency_by_role
end
private def define_pendency_by_role
return 'evaluation' if @author.roles.map(&:to_s).include?('evaluator')
return 'operation' if @author.roles.map(&:to_s).include?('operator')
return 'antifraud' if @author.roles.map(&:to_s).include?('analyst')
end
private def pendency_type_known?
ProposalNote.all_pendency_type.map(&:to_s).include?(@pendency_type)
end
end
```
Nesse caso temos uma classe bem estruturada e até simples, porém tem alguns pontos que levantaram atenção:
1) Não estamos incentivando o uso de funções puras.
2) A existência do initialize pode gerar ambiguidade sobre onde chamar as funções (no call ou no próprio initialize), mesmo que tenhamos o controle disso no review de PRs e ADRs, o simples fato de existir o initialize definido já é uma brecha.
## Caso optemos por passar todos os parâmetros no call teríamos a mesma classe mais ou menos assim:
``` Ruby=
class DecidePendencyTypeService
def call(author:, pendency_type: nil)
return pendency_type if pendency_type_known?(pendency_type)
define_pendency_by_role(author)
end
private def define_pendency_by_role(author)
return 'evaluation' if author.roles.map(&:to_s).include?('evaluator')
return 'operation' if author.roles.map(&:to_s).include?('operator')
return 'antifraud' if author.roles.map(&:to_s).include?('analyst')
end
private def pendency_type_known?(pendency_type)
ProposalNote.all_pendency_type.map(&:to_s).include?(pendency_type)
end
end
```
Nesse caso o código pra mim me pareceu um pouco mais limpo.
1) Incentiva o uso de funcoes puras
2) Remove a possibilidade de ter comportamento espalhado (initialize e call)
Essas melhorias já seriam bons pontos que favoreceriam a adoção desse padrão. Mas olhando mais a fundo a gente ainda conseguiu encontrar algumas melhorias com o uso de mocks nos testes.
## Como isso ajuda na hora de fazer testes?
Se quisermos testar quem interage com `DecidePendencyTypeService` e precisarmos saber mockar os retornos para que se torne um teste unitario, e garantir que o teste seja paralelizável.
No case dos parâmetros enviados via construtor vamos precisar testar da seguinte forma:
```ruby=
instance_of_service = InstanceDouble(DecidePendencyTypeService)
allow(DecidePendencyTypeService)
.to receive(:new)
.with(author: author)
.and_return(instance_of_service)
allow(instance_of_service)
.to receive(:call)
.and_return(pendency_type)
```
Nesse caso tivemos que definir a instancia que o `new` retornara e dar um allow nessa instancia a retornar o valor que queremos para o metodo call.
```ruby=
allow_any_instance_of(DecidePendencyTypeService)
.to receive(:call)
.with(author: author)
.and_return(pendency_type)
```
Acabamos mockando menos e deixando o setup mais simples e objetivo.