O que é isso
Isto é uma joia light_operations
gem 'light_operations'
$ gem install light_operations
Como funciona
Basicamente, este é um Container para lógica de buissnes.
Você pode definir dependências durante a inicialização e executar com parâmetros personalizados. Quando você define ações adiadas em caso de sucesso e falha antes que a execução da operação seja concluída, após a execução, uma dessas ações depende do resultado da execução. As ações podem ser um bloco (Proc) ou você pode delgate a execução para outro método de objeto, por operação de ligação com objeto específico com esses métodos. Você também pode usar a operação como execução simples e verificar o status por sucesso? ou falhar? e, em seguida, usando o método subject e errors, crie sua própria lógica para terminar o resultado. Existem muitos casos de uso possíveis onde e como você pode usar as operações. Você pode construir csacade de opreations, usá-los um após o outro, usá-los recursivamente e muito mais.
Como parece no código
Classe
class MyOperation < LightOperations::Core
def execute(_params = nil)
dependency(:my_service) # when missing MissingDependency error will be raised
end
end
Inicialização
MyOperation.new(my_service: MyService.new)
Você pode adicionar ações adiadas para sucesso e fracasso
# 1
MyOperation.new.on_success { |model| render :done, locals: { model: model } }
# 2
MyOperation.new.on(success: -> () { |model| render :done, locals: { model: model } )
Ao vincular a operação a outro objeto, você pode delegar ações aos métodos de objeto vinculado
# 1
MyOperation.new.bind_with(self).on_success(:done)
# 2
MyOperation.new.bind_with(self).on(success: :done)
Método de execução #run
finaliza a execução das ações
MyOperation.new.bind_with(self).on(success: :done).run(params)
Após a operação de execução manter o estado de execução, você pode obter de volta todas as informações de que precisa
#success?
=>true/false
#fail?
=>true/false
#subject?
=>success or fail object
#errors
=>errors by default array but you can return any objec tou want
Uso padrão
operation.new(dependencies)
.on(success: :done, fail: :show_error)
.bind_with(self)
.run(params)
ou
operation.new(dependencies).tap do |op|
return op.run(params).success? ? op.subject : op.errors
end
bloco ou método de sucesso recebe assunto como argumento
(subject) -> { }
ou
def success_method(subject)
...
end
bloco ou método de falha recebem assunto e erros como argumento
(subject, errors) -> { }
ou
def fail_method(subject, errors)
...
end
Uso
Casos de uso
Lógica de voto básico
Operação
class ArticleVoteBumperOperation < LightOperations::Core
rescue_from ActiveRecord::ActiveRecordError, with: :on_ar_error
def execute(_params = nil)
dependency(:article_model).tap do |article|
article.vote = article.vote.next
article.save
end
{ success: true }
end
def on_ar_error(_exception)
fail!(vote: 'could not be updated!')
end
end
Controlador
class ArticleVotesController < ApplicationController
def up
response = operation.run.success? ? response.subject : response.errors
render :up, json: response
end
private
def operation
@operation ||= ArticleVoteBumperOperation.new(article_model: article)
end
def article
Article.find(params.require(:id))
end
end
Execução recursiva básica para coletar feeds de notícias de 2 fontes
Operação
class CollectFeedsOperation < LightOperations::Core
rescue_from Timeout::Error, with: :on_timeout
def execute(params = {})
dependency(:http_client).get(params.fetch(:url)).body
end
def on_timeout
fail!
end
end
Controlador
class NewsFeedsController < ApplicationController
DEFAULT_NEWS_URL = 'http://rss.best_news.pl'
BACKUP_NEWS_URL = 'http://rss.not_so_bad_news.pl'
def news
collect_feeds_op
.bind_with(self)
.on(success: :display_news, fail: :second_attempt)
.run(url: DEFAULT_NEWS_URL)
end
private
def second_attempt(_news, _errors)
collect_feeds_op
.on_fail(:display_old_news)
.run(url: BACKUP_NEWS_URL)
end
def display_news(news)
render :display_news, locals: { news: news }
end
def display_old_news
end
def collect_feeds_op
@collect_feeds_op ||= CollectFeedsOperation.new(http_client: http_client)
end
def http_client
MyAwesomeHttpClient
end
end
Básico com modelo ativo / objeto de registro ativo
Operação
class AddBookOperation < LightOperations::Core
def execute(params = {})
dependency(:book_model).new(params).tap do |model|
model.valid? # this method automatically provide errors from model.errors
end
end
end
Controlador
class BooksController < ApplicationController
def index
render :index, locals: { collection: Book.all }
end
def new
render_book_form
end
def create
add_book_op
.bind_with(self)
.on(success: :book_created, fail: :render_book_form)
.run(permit_book_params)
end
private
def book_created(book)
redirect_to :index, notice: "book #{book.name} created"
end
def render_book_form(book = Book.new, _errors = nil)
render :new, locals: { book: book }
end
def add_book_op
@add_book_op ||= AddBookOperation.new(book_model: Book)
end
def permit_book_params
params.requre(:book)
end
end
Caso simples quando você deseja ter autorização do usuário
Operação
class AuthOperation < LightOperations::Core
rescue_from AuthFail, with: :on_auth_error
def execute(params = {})
dependency(:auth_service).login(login: login(params), password: password(params))
end
def on_auth_error(_exception)
fail!([login: 'unknown']) # or subject.errors.add(login: 'unknown')
end
def login(params)
params.fetch(:login)
end
def password(params)
params.fetch(:password)
end
end
Maneira do controlador # 1
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op
.bind_with(self)
.on_success(:create_session_with_dashbord_redirection)
.on_fail(:render_account_with_errors)
.run(params)
end
private
def create_session_with_dashbord_redirection(account)
session_create_for(account)
redirect_to :dashboard
end
def render_account_with_errors(account, _errors)
render :new, locals: { account: account }
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
Forma do controlador # 2
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op
.on_success{ |account| create_session_with_dashbord_redirection(account) }
.on_fail { |account, _errors| render :new, locals: { account: account } }
.run(params)
end
private
def create_session_with_dashbord_redirection(account)
session_create_for(account)
redirect_to :dashboard
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
Maneira do controlador # 3
class AuthController < ApplicationController
def new
render :new, locals: { account: Account.new }
end
def create
auth_op.on_success(&go_to_dashboard).on_fail(&go_to_login).run(params)
end
private
def go_to_dashboard
-> (account) do
session_create_for(account)
redirect_to :dashboard
end
end
def go_to_login
-> (account, _errors) { render :new, locals: { account: account } }
end
def auth_op
@auth_op ||= AuthOperation.new(auth_service: auth_service)
end
def auth_service
@auth_service ||= AuthService.new
end
end
A ação de registro de sucesso e falha está disponível #on
como:
def create
auth_op.bind_with(self).on(success: :dashboard, fail: :show_error).run(params)
end
A operação tem alguns métodos auxiliares (para melhorar a execução recursiva)
#clear!
=> operação de retorno ao estado inicial#unbind!
=> desvincular objeto vinculado#clear_subject_with_errors!
=> assunto claro e erros
Quando o status da operação é mais importante, podemos simplesmente usar #success?
ou #fail?
na operação executada
Erros estão disponíveis #errors
após a operação ser executada
Espero que esta joia ajude você a construir um código mais legível e limpo com lógica separada. Esta é uma versão muito antiga, mas deve funcionar e ser agradável de usar.