Represente casos de uso de forma simples e poderosa ao escrever código modular, expressivo e sequencialmente lógico.
Principais objetivos deste projeto:
- Fácil de usar e aprender ( entrada >> processamento >> saída ).
- Promover imutabilidade (transformar dados ao invés de modificar) e integridade de dados.
- Nada de callbacks (ex: before, after, around) para evitar indireções no código que possam comprometer o estado e entendimento dos fluxos da aplicação.
- Resolver regras de negócio complexas, ao permitir uma composição de casos de uso (criação de fluxos).
- Ser rápido e otimizado (verifique a seção de benchmarks).
Nota: Verifique o repo https://github.com/serradura/from-fat-controllers-to-use-cases para ver uma aplicação Ruby on Rails que utiliza esta gem para resolver as regras de negócio.
- Compatibilidade
- Dependências
- Instalação
- Uso
Micro::Case
- Como definir um caso de uso?Micro::Case::Result
- O que é o resultado de um caso de uso?- O que são os tipos de resultados?
- Como definir tipos customizados de resultados?
- É possível definir um tipo sem definir os dados do resultado?
- Como utilizar os hooks dos resultados?
- Por que o hook sem um tipo definido expõe o próprio resultado?
- O que acontece se um hook de resultado for declarado múltiplas vezes?
- Como usar o método
Micro::Case::Result#then
?
Micro::Cases::Flow
- Como compor casos de uso?- É possível compor um fluxo com outros fluxos?
- É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?
- Como entender o que aconteceu durante a execução de um flow?
- É possível declarar um fluxo que inclui o próprio caso de uso?
Micro::Case::Strict
- O que é um caso de uso estrito?Micro::Case::Safe
- Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?u-case/with_activemodel_validation
- Como validar os atributos do caso de uso?
Micro::Case.config
- Benchmarks
- Exemplos
- Desenvolvimento
- Contribuindo
- Licença
- Código de conduta
u-case | branch | ruby | activemodel | u-attributes- |
---|---|---|---|---|
unreleased | main | >= 2.2.0 | >= 3.2, < 7.0 | >= 2.7, < 3.0 |
4.5.1 | v4.x | >= 2.2.0 | >= 3.2, < 7.0 | >= 2.7, < 3.0 |
3.1.0 | v3.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
2.6.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
1.1.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
Nota: O activemodel é uma dependência opcional, esse módulo que pode ser habilitado para validar os atributos dos casos de uso.
-
Gem
kind
.Sistema de tipos simples (em runtime) para Ruby.
É usado para validar os inputs de alguns métodos do u-case, além de expor um validador de tipos através do
activemodel validation
(veja como habilitar). -
u-attributes
gem.Essa gem permite definir atributos de leitura (read-only), ou seja, os seus objetos só terão getters para acessar os dados dos seus atributos. Ela é usada para definir os atributos dos casos de uso.
Adicione essa linha ao Gemfile da sua aplicação:
gem 'u-case', '~> 4.5.1'
E então execute:
$ bundle
Ou instale manualmente:
$ gem install u-case
class Multiply < Micro::Case
# 1. Defina o input como atributos
attributes :a, :b
# 2. Defina o método `call!` com a regra de negócio
def call!
# 3. Envolva o resultado do caso de uso com os métodos `Success(result: *)` ou `Failure(result: *)`
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure result: { message: '`a` and `b` attributes must be numeric' }
end
end
end
#===========================#
# Executando um caso de uso #
#===========================#
# Resultado de sucesso
result = Multiply.call(a: 2, b: 2)
result.success? # true
result.data # { number: 4 }
# Resultado de falha
bad_result = Multiply.call(a: 2, b: '2')
bad_result.failure? # true
bad_result.data # { message: "`a` and `b` attributes must be numeric" }
# Nota:
# ----
# O resultado de um Micro::Case.call é uma instância de Micro::Case::Result
Um Micro::Case::Result
armazena os dados de output de um caso de uso. Esses são seus métodos:
#success?
retornatrue
se for um resultado de sucesso.#failure?
retornatrue
se for um resultado de falha.#use_case
retorna o caso de uso responsável pelo resultado. Essa funcionalidade é útil para lidar com falhas em flows (esse tópico será abordado mais a frente).#type
retorna um Symbol que dá significado ao resultado, isso é útil para declarar diferentes tipos de falha e sucesso.#data
os dados do resultado (umHash
).#[]
e#values_at
são atalhos para acessar as propriedades do#data
.#key?
retornatrue
se a chave estiver present no#data
.#value?
retornatrue
se o valor estiver present no#data
.#slice
retorna um novoHash
que inclui apenas as chaves fornecidas. Se as chaves fornecidas não existirem, umHash
vazio será retornado.#on_success
or#on_failure
são métodos de hooks que te auxiliam a definir o fluxo da aplicação.#then
este método permite aplicar novos casos de uso ao resultado atual se ele for sucesso. A ideia dessa feature é a criação de fluxos dinâmicos.#transitions
retorna um array com todas as transformações que um resultado teve durante um flow.
Nota: por conta de retrocompatibilidade, você pode usar o método
#value
como um alias para o método#data
.
Todo resultado tem um tipo (#type
), e estes são os valores padrões:
:ok
em casos de sucesso;:error
ou:exception
em casos de falhas.
class Divide < Micro::Case
attributes :a, :b
def call!
if invalid_attributes.empty?
Success result: { number: a / b }
else
Failure result: { invalid_attributes: invalid_attributes }
end
rescue => exception
Failure result: exception
end
private def invalid_attributes
attributes.select { |_key, value| !value.is_a?(Numeric) }
end
end
# Resultado de sucesso
result = Divide.call(a: 2, b: 2)
result.type # :ok
result.data # { number: 1 }
result.success? # true
result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>2}, @a=2, @b=2, @__result=...>
# Resultado de falha (type == :error)
bad_result = Divide.call(a: 2, b: '2')
bad_result.type # :error
bad_result.data # { invalid_attributes: { "b"=>"2" } }
bad_result.failure? # true
bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=...>
# Resultado de falha (type == :exception)
err_result = Divide.call(a: 2, b: 0)
err_result.type # :exception
err_result.data # { exception: <ZeroDivisionError: divided by 0> }
err_result.failure? # true
err_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>
# Nota:
# ----
# Toda instância de Exception será envolvida pelo método
# Failure(result: *) que receberá o tipo `:exception` ao invés de `:error`.
Resposta: Use um Symbol
com argumento dos métodos Success()
, Failure()
e declare o result:
keyword para definir os dados do resultado.
class Multiply < Micro::Case
attributes :a, :b
def call!
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure :invalid_data, result: {
attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
}
end
end
end
# Resultado de sucesso
result = Multiply.call(a: 3, b: 2)
result.type # :ok
result.data # { number: 6 }
result.success? # true
# Resultado de falha
bad_result = Multiply.call(a: 3, b: '2')
bad_result.type # :invalid_data
bad_result.data # { attributes: {"b"=>"2"} }
bad_result.failure? # true
Resposta: Sim, é possível. Mas isso terá um comportamento especial por conta dos dados do resultado ser um hash com o tipo definido como chave e true
como o valor.
class Multiply < Micro::Case
attributes :a, :b
def call!
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure(:invalid_data)
end
end
end
result = Multiply.call(a: 2, b: '2')
result.failure? # true
result.data # { :invalid_data => true }
result.type # :invalid_data
result.use_case.attributes # {"a"=>2, "b"=>"2"}
# Nota:
# ----
# Essa funcionalidade será muito útil para lidar com resultados de falha de um Flow
# (este tópico será coberto em breve).
Como mencionando anteriormente, o Micro::Case::Result
tem dois métodos para melhorar o controle do fluxo da aplicação. São eles:
#on_success
, on_failure
.
Os exemplos abaixo os demonstram em uso:
class Double < Micro::Case
attribute :number
def call!
return Failure :invalid, result: { msg: 'number must be a numeric value' } unless number.is_a?(Numeric)
return Failure :lte_zero, result: { msg: 'number must be greater than 0' } if number <= 0
Success result: { number: number * 2 }
end
end
#================================#
# Imprimindo o output se sucesso #
#================================#
Double
.call(number: 3)
.on_success { |result| p result[:number] }
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
# O output será:
# 6
#===================================#
# Lançando um erro em caso de falha #
#===================================#
Double
.call(number: -1)
.on_success { |result| p result[:number] }
.on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
# O output será:
#
# 1. Imprimirá a mensagem: Double was the use case responsible for the failure
# 2. Lançará a exception: ArgumentError (the number must be greater than 0)
# Nota:
# ----
# O caso de uso responsável estará sempre acessível como o segundo argumento do hook
Resposta: Para permitir que você defina o controle de fluxo da aplicação usando alguma estrutura condicional como um if
ou case when
.
class Double < Micro::Case
attribute :number
def call!
return Failure(:invalid) unless number.is_a?(Numeric)
return Failure :lte_zero, result: attributes(:number) if number <= 0
Success result: { number: number * 2 }
end
end
Double
.call(number: -1)
.on_failure do |result, use_case|
case result.type
when :invalid then raise TypeError, "number must be a numeric value"
when :lte_zero then raise ArgumentError, "number `#{result[:number]}` must be greater than 0"
else raise NotImplementedError
end
end
# O output será uma exception:
#
# ArgumentError (number `-1` must be greater than 0)
Nota: O mesmo que foi feito no exemplo anterior poderá ser feito com o hook
#on_success
!
A sintaxe para decompor um Array pode ser usada na declaração de variáveis e nos argumentos de métodos/blocos. Se você não sabia disso, confira a documentação do Ruby.
# O objeto exposto em hook sem um tipo é um Micro::Case::Result e ele pode ser decomposto. Exemplo:
Double
.call(number: -2)
.on_failure do |(data, type), use_case|
case type
when :invalid then raise TypeError, 'number must be a numeric value'
when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
else raise NotImplementedError
end
end
# O output será a exception:
#
# ArgumentError (the number `-2` must be greater than 0)
Nota: O que mesmo pode ser feito com o
#on_success
hook!
Resposta: Se o tipo do resultado for identificado o hook será sempre executado.
class Double < Micro::Case
attributes :number
def call!
if number.is_a?(Numeric)
Success :computed, result: { number: number * 2 }
else
Failure :invalid, result: { msg: 'number must be a numeric value' }
end
end
end
result = Double.call(number: 3)
result.data # { number: 6 }
result[:number] * 4 # 24
accum = 0
result
.on_success { |result| accum += result[:number] }
.on_success { |result| accum += result[:number] }
.on_success(:computed) { |result| accum += result[:number] }
.on_success(:computed) { |result| accum += result[:number] }
accum # 24
result[:number] * 4 == accum # true
Este método permite você criar fluxos dinâmicos. Com ele, você pode adicionar novos casos de uso ou fluxos para continuar a transformação de um resultado. Exemplo:
class ForbidNegativeNumber < Micro::Case
attribute :number
def call!
return Success result: attributes if number >= 0
Failure result: attributes
end
end
class Add3 < Micro::Case
attribute :number
def call!
Success result: { number: number + 3 }
end
end
result1 =
ForbidNegativeNumber
.call(number: -1)
.then(Add3)
result1.data # {'number' => -1}
result1.failure? # true
# ---
result2 =
ForbidNegativeNumber
.call(number: 1)
.then(Add3)
result2.data # {'number' => 4}
result2.success? # true
Nota: este método altera o
Micro::Case::Result#transitions
.
Ele passará o próprio resultado (uma instância do Micro::Case::Result
) como argumento do bloco, e retornará o output do bloco ao invés dele mesmo. e.g:
class Add < Micro::Case
attributes :a, :b
def call!
if Kind.of?(Numeric, a, b)
Success result: { sum: a + b }
else
Failure(:attributes_arent_numbers)
end
end
end
# --
success_result =
Add
.call(a: 2, b: 2)
.then { |result| result.success? ? result[:sum] : 0 }
puts success_result # 4
# --
failure_result =
Add
.call(a: 2, b: '2')
.then { |result| result.success? ? result[:sum] : 0 }
puts failure_result # 0
Passe um Hash
como segundo argumento do método Micro::Case::Result#then
.
Todo::FindAllForUser
.call(user: current_user, params: params)
.then(Paginate)
.then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
.on_success { |result| render_json(200, data: result[:todos]) }
Chamamos de fluxo uma composição de casos de uso. A ideia principal desse recurso é usar/reutilizar casos de uso como etapas de um novo caso de uso. Exemplo:
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success result: { numbers: numbers.map(&:to_i) }
else
Failure result: { message: 'numbers must contain only numeric types' }
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number + 2 } }
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * number } }
end
end
end
#-----------------------------------------#
# Criando um flow com Micro::Cases.flow() #
#-----------------------------------------#
Add2ToAllNumbers = Micro::Cases.flow([
Steps::ConvertTextToNumbers,
Steps::Add2
])
result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
result.success? # true
result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
#--------------------------------#
# Criando um flow usando classes #
#--------------------------------#
class DoubleAllNumbers < Micro::Case
flow Steps::ConvertTextToNumbers,
Steps::Double
end
DoubleAllNumbers.
call(numbers: %w[1 1 b 2 3 4]).
on_failure { |result| puts result[:message] } # "numbers must contain only numeric types"
Ao ocorrer uma falha, o caso de uso responsável ficará acessível no resultado. Exemplo:
result = DoubleAllNumbers.call(numbers: %w[1 1 b 2 3 4])
result.failure? # true
result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
result.on_failure do |_message, use_case|
puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertTextToNumbers was the use case responsible for the failure
end
Resposta: Sim, é possível.
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success result: { numbers: numbers.map(&:to_i) }
else
Failure result: { message: 'numbers must contain only numeric types' }
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number + 2 } }
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * number } }
end
end
end
DoubleAllNumbers =
Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Double])
SquareAllNumbers =
Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Square])
DoubleAllNumbersAndAdd2 =
Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
SquareAllNumbersAndAdd2 =
Micro::Cases.flow([SquareAllNumbers, Steps::Add2])
SquareAllNumbersAndDouble =
Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers])
DoubleAllNumbersAndSquareAndAdd2 =
Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2])
SquareAllNumbersAndDouble
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |result| p result[:numbers] } # [6, 6, 12, 12, 22, 36]
DoubleAllNumbersAndSquareAndAdd2
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |result| p result[:numbers] } # [6, 6, 18, 18, 38, 66]
Nota: Você pode mesclar qualquer approach para criar flows - exemplos.
É possível que um fluxo acumule sua entrada e mescle cada resultado de sucesso para usar como argumento dos próximos casos de uso?
Resposta: Sim, é possível! Veja o exemplo abaixo para entender como funciona o acúmulo de dados dentro da execução de um fluxo.
module Users
class FindByEmail < Micro::Case
attribute :email
def call!
user = User.find_by(email: email)
return Success result: { user: user } if user
Failure(:user_not_found)
end
end
end
module Users
class ValidatePassword < Micro::Case::Strict
attributes :user, :password
def call!
return Failure(:user_must_be_persisted) if user.new_record?
return Failure(:wrong_password) if user.wrong_password?(password)
return Success result: attributes(:user)
end
end
end
module Users
Authenticate = Micro::Cases.flow([
FindByEmail,
ValidatePassword
])
end
Users::Authenticate
.call(email: '[email protected]', password: 'password')
.on_success { |result| sign_in(result[:user]) }
.on_failure(:wrong_password) { render status: 401 }
.on_failure(:user_not_found) { render status: 404 }
Primeiro, vamos ver os atributos usados por cada caso de uso:
class Users::FindByEmail < Micro::Case
attribute :email
end
class Users::ValidatePassword < Micro::Case
attributes :user, :password
end
Como você pode ver, Users::ValidatePassword
espera um usuário como sua entrada. Então, como ele recebe o usuário?
R: Ele recebe o usuário do resultado de sucesso Users::FindByEmail
!
E este é o poder da composição de casos de uso porque o output de uma etapa irá compor a entrada do próximo caso de uso no fluxo!
input >> processamento >> output
Nota: Verifique esses exemplos de teste Micro::Cases::Flow e Micro::Cases::Safe::Flow para ver diferentes casos de uso tendo acesso aos dados de um fluxo.
Use Micro::Case::Result#transitions
!
Vamos usar os exemplos da seção anterior para ilustrar como utilizar essa feature.
user_authenticated =
Users::Authenticate.call(email: '[email protected]', password: user_password)
user_authenticated.transitions
[
{
:use_case => {
:class => Users::FindByEmail,
:attributes => { :email => "[email protected]" }
},
:success => {
:type => :ok,
:result => {
:user => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
}
},
:accessible_attributes => [ :email, :password ]
},
{
:use_case => {
:class => Users::ValidatePassword,
:attributes => {
:user => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
:password => "123456"
}
},
:success => {
:type => :ok,
:result => {
:user => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
}
},
:accessible_attributes => [ :email, :password, :user ]
}
]
O exemplo acima mostra a saída gerada pelas Micro::Case::Result#transitions
.
Com ele é possível analisar a ordem de execução dos casos de uso e quais foram os inputs
fornecidos ([:attributes]
) e outputs
([:success][:result]
) em toda a execução.
E observe a propriedade accessible_attributes
, ela mostra quais atributos são acessíveis nessa etapa do fluxo. Por exemplo, na última etapa, você pode ver que os atributos accessible_attributes
aumentaram devido ao acúmulo de fluxo de dados.
Nota: O
Micro::Case::Result#then
incrementa oMicro::Case::Result#transitions
.
[
{
use_case: {
class: <Micro::Case>,# Caso de uso que será executado
attributes: <Hash> # (Input) Os atributos do caso de uso
},
[success:, failure:] => { # (Output)
type: <Symbol>, # Tipo do resultado. Padrões:
# Success = :ok, Failure = :error or :exception
result: <Hash> # Os dados retornados pelo resultado do use case
},
accessible_attributes: <Array>, # Propriedades que podem ser acessadas pelos atributos do caso de uso,
# começando com Hash usado para invocá-lo e que são incrementados
# com os valores de resultado de cada caso de uso do fluxo.
}
]
Resposta: Sim! Você pode usar o Micro::Case.config
para fazer isso. Link para essa seção.
Resposta: Sim! Você pode usar a macro self
ou self.call!
. Exemplo:
class ConvertTextToNumber < Micro::Case
attribute :text
def call!
Success result: { number: text.to_i }
end
end
class ConvertNumberToText < Micro::Case
attribute :number
def call!
Success result: { text: number.to_s }
end
end
class Double < Micro::Case
flow ConvertTextToNumber,
self.call!,
ConvertNumberToText
attribute :number
def call!
Success result: { number: number * 2 }
end
end
result = Double.call(text: '4')
result.success? # true
result[:number] # "8"
Note: Essa funcionalidade pode ser usada com Micro::Case::Safe. Verifique esse teste para ver um example: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe/with_inner_flow_test.rb
Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
Double.call({})
# O output será:
# ArgumentError (missing keyword: :numbers)
Micro::Case::Safe
- Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?
Sim, assim como Micro::Case::Strict
, o Micro::Case::Safe
é outro tipo de caso de uso. Ele tem a capacidade de interceptar automaticamente qualquer exceção como um resultado de falha. Exemplo:
require 'logger'
AppLogger = Logger.new(STDOUT)
class Divide < Micro::Case::Safe
attributes :a, :b
def call!
if a.is_a?(Integer) && b.is_a?(Integer)
Success result: { number: a / b}
else
Failure(:not_an_integer)
end
end
end
result = Divide.call(a: 2, b: 0)
result.type == :exception # true
result.data # { exception: #<ZeroDivisionError...> }
result[:exception].is_a?(ZeroDivisionError) # true
result.on_failure(:exception) do |result|
AppLogger.error(result[:exception].message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
end
Se você precisar lidar com um erro específico, recomendo o uso de uma instrução case. Exemplo:
result.on_failure(:exception) do |data, use_case|
case exception = data[:exception]
when ZeroDivisionError then AppLogger.error(exception.message)
else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
end
end
Note: É possível resgatar uma exceção mesmo quando é um caso de uso seguro. Exemplos:
u-case/test/micro/case/safe_test.rb
Lines 90 to 118 in 714c6b6
Como casos de uso seguros, os fluxos seguros podem interceptar uma exceção em qualquer uma de suas etapas. Estas são as maneiras de definir um:
module Users
Create = Micro::Cases.safe_flow([
ProcessParams,
ValidateParams,
Persist,
SendToCRM
])
end
Definindo dentro das classes:
module Users
class Create < Micro::Case::Safe
flow ProcessParams,
ValidateParams,
Persist,
SendToCRM
end
end
Na programação funcional os erros/exceções são tratados como dados comuns, a ideia é transformar a saída mesmo quando ocorre um comportamento inesperado. Para muitos, as exceções são muito semelhantes à instrução GOTO, pulando o fluxo do programa para caminhos que podem ser difíceis de descobrir como as coisas funcionam em um sistema.
Para resolver isso, o Micro::Case::Result
tem um hook especial #on_exception
para ajudá-lo a lidar com o fluxo de controle no caso de exceções.
Note: essa funcionalidade funcionará melhor se for usada com um flow ou caso de uso
Micro::Case::Safe
.
Como ele funciona?
class Divide < Micro::Case::Safe
attributes :a, :b
def call!
Success result: { division: a / b }
end
end
Divide
.call(a: 2, b: 0)
.on_success { |result| puts result[:division] }
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
# Output:
# -------
# Can't divide a number by 0
# Oh no, something went wrong!
Divide
.call(a: 2, b: '2')
.on_success { |result| puts result[:division] }
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
# Output:
# -------
# Please, use only numeric attributes.
# Oh no, something went wrong!
Como você pode ver, este hook tem o mesmo comportamento de result.on_failure(:exception)
, mas, a ideia aqui é ter uma melhor comunicação no código, fazendo uma referência explícita quando alguma falha acontecer por causa de uma exceção.
Requisitos:
Para fazer isso a sua aplicação deverá ter o activemodel >= 3.2, < 6.1.0 como dependência.
Por padrão, se a sua aplicação tiver o ActiveModel como uma dependência, qualquer tipo de caso de uso pode fazer uso dele para validar seus atributos.
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
Success result: { number: a * b }
end
end
Mas se você deseja uma maneira automática de falhar seus casos de uso em erros de validação, você poderá fazer:
- require 'u-case/with_activemodel_validation' no Gemfile
gem 'u-case', require: 'u-case/with_activemodel_validation'
- Usar o
Micro::Case.config
para habilitar ele. Link para essa seção.
Usando essa abordagem, você pode reescrever o exemplo anterior com menos código. Exemplo:
require 'u-case/with_activemodel_validation'
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
Success result: { number: a * b }
end
end
Nota: Após habilitar o modo de validação, as classes
Micro::Case::Strict
eMicro::Case::Safe
irão herdar este novo comportamento.
Se eu habilitei a validação automática, é possível desabilitá-la apenas em casos de uso específicos?
Resposta: Sim, é possível. Para fazer isso, você só precisará usar a macro disable_auto_validation
. Exemplo:
require 'u-case/with_activemodel_validation'
class Multiply < Micro::Case
disable_auto_validation
attribute :a
attribute :b
validates :a, :b, presence: true, numericality: true
def call!
Success result: { number: a * b }
end
end
Multiply.call(a: 2, b: 'a')
# O output será:
# TypeError (String can't be coerced into Integer)
A gem kind possui um módulo para habilitar a validação do tipo de dados através do ActiveModel validations
. Então, quando você fizer o require do 'u-case/with_activemodel_validation'
, este módulo também irá fazer o require do Kind::Validator
.
O exemplo abaixo mostra como validar os tipos de atributos.
class Todo::List::AddItem < Micro::Case
attributes :user, :params
validates :user, kind: User
validates :params, kind: ActionController::Parameters
def call!
todo_params = params.require(:todo).permit(:title, :due_at)
todo = user.todos.create(todo_params)
Success result: { todo: todo }
rescue ActionController::ParameterMissing => e
Failure :parameter_missing, result: { message: e.message }
end
end
A ideia deste recurso é permitir a configuração de algumas funcionalidades/módulos do u-case
.
Eu recomendo que você use apenas uma vez em sua base de código. Exemplo: Em um inicializador do Rails.
Você pode ver abaixo todas as configurações disponíveis com seus valores padrão:
Micro::Case.config do |config|
# Use ActiveModel para auto-validar os atributos dos seus casos de uso.
config.enable_activemodel_validation = false
# Use para habilitar/desabilitar o `Micro::Case::Results#transitions`.
config.enable_transitions = true
end
Gem / Abstração | Iterações por segundo | Comparação |
---|---|---|
Dry::Monads | 315635.1 | O mais rápido |
Micro::Case | 75837.7 | 4.16x mais lento |
Interactor | 59745.5 | 5.28x mais lento |
Trailblazer::Operation | 28423.9 | 11.10x mais lento |
Dry::Transaction | 10130.9 | 31.16x mais lento |
Show the full benchmark/ips results.
# Warming up --------------------------------------
# Interactor 5.711k i/100ms
# Trailblazer::Operation
# 2.283k i/100ms
# Dry::Monads 31.130k i/100ms
# Dry::Transaction 994.000 i/100ms
# Micro::Case 7.911k i/100ms
# Micro::Case::Safe 7.911k i/100ms
# Micro::Case::Strict 6.248k i/100ms
# Calculating -------------------------------------
# Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s
# Trailblazer::Operation
# 28.424k (±15.8%) i/s - 141.546k in 5.087882s
# Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s
# Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s
# Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s
# Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s
# Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s
# Comparison:
# Dry::Monads: 315635.1 i/s
# Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower
# Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower
# Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower
# Interactor: 59745.5 i/s - 5.28x (± 0.00) slower
# Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower
# Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/success_results.
Gem / Abstração | Iterações por segundo | Comparação |
---|---|---|
Dry::Monads | 135386.9 | O mais rápido |
Micro::Case | 73489.3 | 1.85x mais lento |
Trailblazer::Operation | 29016.4 | 4.67x mais lento |
Interactor | 27037.0 | 5.01x mais lento |
Dry::Transaction | 8988.6 | 15.06x mais lento |
Mostrar o resultado completo do benchmark/ips.
# Warming up --------------------------------------
# Interactor 2.626k i/100ms
# Trailblazer::Operation 2.343k i/100ms
# Dry::Monads 13.386k i/100ms
# Dry::Transaction 868.000 i/100ms
# Micro::Case 7.603k i/100ms
# Micro::Case::Safe 7.598k i/100ms
# Micro::Case::Strict 6.178k i/100ms
# Calculating -------------------------------------
# Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s
# Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s
# Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s
# Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s
# Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s
# Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s
# Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s
# Comparison:
# Dry::Monads: 135386.9 i/s
# Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower
# Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower
# Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower
# Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower
# Interactor: 27037.0 i/s - 5.01x (± 0.00) slower
# Dry::Transaction: 8988.6 i/s - 15.06x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/failure_results.
Gem / Abstração | Resultados de sucesso | Resultados de falha |
---|---|---|
Micro::Case::Result pipe method |
80936.2 i/s | 78280.4 i/s |
Micro::Case::Result then method |
0x mais lento | 0x mais lento |
Micro::Cases.flow | 0x mais lento | 0x mais lento |
Micro::Case class with an inner flow | 1.72x mais lento | 1.68x mais lento |
Micro::Case class including itself as a step | 1.93x mais lento | 1.87x mais lento |
Interactor::Organizer | 3.33x mais lento | 3.22x mais lento |
* As gems Dry::Monads
, Dry::Transaction
, Trailblazer::Operation
estão fora desta análise por não terem esse tipo de funcionalidade.
Resultados de sucesso - Mostrar o resultado completo do benchmark/ips.
# Warming up --------------------------------------
# Interactor::Organizer 1.809k i/100ms
# Micro::Cases.flow([]) 7.808k i/100ms
# Micro::Case flow in a class 4.816k i/100ms
# Micro::Case including the class 4.094k i/100ms
# Micro::Case::Result#| 7.656k i/100ms
# Micro::Case::Result#then 7.138k i/100ms
# Calculating -------------------------------------
# Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s
# Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s
# Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s
# Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s
# Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s
# Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s
# Comparison:
# Micro::Case::Result#|: 80936.2 i/s
# Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error
# Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error
# Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower
# Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower
# Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower
Resultados de falha - Mostrar o resultado completo do benchmark/ips.
# Warming up --------------------------------------
# Interactor::Organizer 1.734k i/100ms
# Micro::Cases.flow([]) 7.515k i/100ms
# Micro::Case flow in a class 4.636k i/100ms
# Micro::Case including the class 4.114k i/100ms
# Micro::Case::Result#| 7.588k i/100ms
# Micro::Case::Result#then 6.681k i/100ms
# Calculating -------------------------------------
# Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s
# Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s
# Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s
# Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s
# Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s
# Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s
# Comparison:
# Micro::Case::Result#|: 78280.4 i/s
# Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error
# Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error
# Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower
# Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower
# Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/
Clone este repositório e acesse a sua pasta, então execute os comandos abaixo:
Casos de uso
ruby benchmarks/perfomance/use_case/failure_results.rb
ruby benchmarks/perfomance/use_case/success_results.rb
Flows
ruby benchmarks/perfomance/flow/failure_results.rb
ruby benchmarks/perfomance/flow/success_results.rb
Casos de uso
./benchmarks/memory/use_case/success/with_transitions/analyze.sh
./benchmarks/memory/use_case/success/without_transitions/analyze.sh
Flows
./benchmarks/memory/flow/success/with_transitions/analyze.sh
./benchmarks/memory/flow/success/without_transitions/analyze.sh
Confira as implementações do mesmo caso de uso com diferentes gems/abstrações.
Um exemplo de fluxo que define etapas para higienizar, validar e persistir seus dados de entrada. Ele tem todas as abordagens possíveis para representar casos de uso com a gem
u-case
.Link: https://github.com/serradura/u-case/blob/main/examples/users_creation
Este projeto mostra diferentes tipos de arquitetura (uma por commit), e na última, como usar a gem
Micro::Case
para lidar com a lógica de negócios da aplicação.Link: https://github.com/serradura/from-fat-controllers-to-use-cases
Rake tasks para demonstrar como lidar com os dados do usuário e como usar diferentes tipos de falha para controlar o fluxo do programa.
Link: https://github.com/serradura/u-case/tree/main/examples/calculator
Link: https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb
Após fazer o checking out do repo, execute bin/setup
para instalar dependências. Então, execute ./test.sh
para executar os testes. Você pode executar bin/console
para ter um prompt interativo que permitirá você experimenta-lá.
Para instalar esta gem em sua máquina local, execute bundle exec rake install
. Para lançar uma nova versão, atualize o número da versão em version.rb
e execute bundle exec rake release
, que criará uma tag git para a versão, enviará git commits e tags e enviará o arquivo .gem
para rubygems.org.
Reportar bugs e solicitar pull requests são bem-vindos no GitHub em https://github.com/serradura/u-case. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e espera-se que os colaboradores sigam o código de conduta do Covenant do Contribuidor.
A gem está disponível como código aberto nos termos da licença MIT.
Espera-se que todos que interagem com o codebase do projeto Micro::Case
, issue trackers, chat rooms and mailing lists sigam o código de conduta.