Eu tenho um método "destroy" do controlador que verifica vários objetos dependentes e falha com mensagens de alerta correspondentes. A exclusão também é impedida no lado do banco de dados, has_many :invoices, dependent: :restrict_with_error
mas falha silenciosamente para o usuário.
Rubocop acha que "Tamanho da Condição de Ramificação de Atribuição é muito alto" e eu gostaria de refatorar isso também porque parece meio repetitivo. No entanto, tenho dificuldades com os retornos. Quando movo verificações para uma função privada, o retorno retorna da função, mas também preciso que ele encerre o método "destroy". Preciso de um retorno duplo.
def destroy
unless Current.user.default_entity.owner == Current.user ||
Current.user.default_entity.user_privileged?('manage_contacts')
redirect_back(fallback_location: contacts_path, alert: t('errors.no_access_rights')) and return
end
unless @contact.invoices.count.zero?
redirect_back(fallback_location: contacts_path,
alert: t('errors.cant_delete_contact_with_invoices')) and return
end
unless @contact.offers.count.zero?
redirect_back(fallback_location: contacts_path,
alert: t('errors.cant_delete_contact_with_offers')) and return
end
@contact.destroy
redirect_to contacts_path, status: :see_other
end
Tenho métodos semelhantes em outros controladores em toda a minha aplicação. Talvez haja um padrão para refatorar esses tipos de métodos?
Sempre há (especialmente com Ruby) muitas maneiras de estruturar código. Aqui está minha abordagem e o que considero melhorias, mas suas opiniões e preferências podem ser diferentes. Além disso, você pode decidir ignorar o Rubocop. Este aviso não diz que é um código errado, apenas que talvez seja possível escrevê-lo melhor.
Código original
Variação 1
Vamos nos livrar da duplicação: podemos definir um alerta quando houver um problema e, se o definirmos, retornaremos e redirecionaremos.
Na verdade, não tornamos o código mais curto ou menos ramificado, mas obtivemos algumas informações: há lógica para encontrar possíveis problemas e há duas maneiras pelas quais esse método termina: redirect_back com erro ou destruição do contato.
Variação 2
Vamos mover a configuração de alerta para um método privado separado, tornando a ação mais limpa
O nome aqui pode não ser muito bom, mas foi apenas uma ideia rápida.
Isso novamente melhorou a legibilidade do
destroy
método principal e tornou mais simples mover os detalhes, o que leva a:Variação 3
Por que estamos verificando se o modelo pode ser excluído? O controlador não precisa saber disso, esse conhecimento possivelmente está no modelo. Além disso, os modelos ActiveRecord têm uma ótima maneira nativa de gerenciar validações e erros, o que nos ajudará a criar um ponto de extremidade Rails mais padrão. Aqui para referência - Documentação do Rails e Guia do Rails
Agradável e limpo.
Eu realmente não testei todo o código, então não garanto que funcione exatamente dessa maneira, estou apenas tentando apontar a direção
Algumas reflexões gerais:
Rubucop está certo em reclamar. A complexidade cíclica está, na verdade, indicando alguns problemas com esse código. Em geral, se seu método contiver mais de um return explícito, pare e refaça-o.
1. A autorização é importante. Trate-a adequadamente.
Fazer uma autorização "inline" dessa forma é uma péssima ideia:
Embora a melhor solução seja adicionar uma camada de autorização como o Pundit, você deve pelo menos seguir o exemplo do Pundit/CanCan e gerar uma exceção quando a solicitação não for autorizada, para garantir que ela seja desativada e que quaisquer retornos de chamada futuros sejam interrompidos.
Como bônus adicional, você pode ignorar rescue_from em seus testes/especificações para afirmar/esperar que uma exceção seja gerada.
Melhor ainda seria extrair
Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')
para que seu controlador não tenha muito conhecimento dos modelos. O Pundit faz isso com classes Policy que você pode emular com um PORO.2. Você está tornando mais difícil do que deveria ser
Em vez de apenas assumir que
@contact.destroy
terá sucesso, basta verificar o valor de retorno. Se a destruição falhar, essasrestrict_with
associações de erro preencherão o objeto de erros.E assim como ao criar/editar o objeto, você pode usá-los para fornecer feedback ao usuário. Com a diferença aqui de que você quer colocá-los no flash ("alerta" é realmente apenas uma tecla no flash) em vez de exibi-los em um formulário.
Como alternativa, em vez da concatenação de strings da mensagem flash, você pode usar uma matriz.
No entanto, isso exigirá algumas alterações no seu layout para lidar com a exibição de mensagens flash do array, o que deixo para o leitor.
Você pode refatorar seu método destroy usando um before_action ou um método auxiliar que retorna um valor booleano indicando se a exclusão deve prosseguir. Você pode usar um método de cláusula guard que retorna true se a exclusão for bloqueada e executa o redirecionamento dentro dela.