Operações Slim em vez de bagunça de código usando operações Light

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 #runfinaliza 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 #oncomo:

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 #errorsapó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.

links

code_source

rubygems