AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / coding / 问题 / 79488556
Accepted
Ricky883249
Ricky883249
Asked: 2025-03-06 15:28:50 +0800 CST2025-03-06 15:28:50 +0800 CST 2025-03-06 15:28:50 +0800 CST

如何使用几种不同的失败消息重构控制器方法

  • 772

我有一个控制器“destroy”方法,用于检查各种依赖对象,并会失败并显示相应的警告消息。数据库端也会阻止删除,has_many :invoices, dependent: :restrict_with_error但对于用户来说,删除失败是无声的。

Rubocop 认为“分配分支条件大小太高”,我也想重构它,因为它看起来有点重复。但是,我对返回值感到困惑。当我将检查移到私有函数中时,返回值会从函数中返回,但我还需要它来结束“destroy”方法。我有点需要双重返回。

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

在我的应用程序中,其他控制器中也有类似的方法。是否有重构此类方法的模式?

ruby-on-rails
  • 3 3 个回答
  • 37 Views

3 个回答

  • Voted
  1. janpeterka
    2025-03-06T23:43:57+08:002025-03-06T23:43:57+08:00

    总是有很多方法(尤其是使用 ruby​​ 时)。这是我的方法以及我认为可以改进的地方,但您的意见和偏好可能有所不同。此外,您可以决定忽略 Rubocop。此警告并不是说这是错误的代码,只是说也许可以写得更好。

    原始代码

    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
    

    变体 1

    让我们摆脱重复 - 我们可以在出现问题时设置警报,如果我们设置了警报,我们就返回并重定向。

    def destroy
      alert = nil
      unless Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')
        alert = t('errors.no_access_rights')
      end
    
      unless @contact.invoices.count.zero?
        alert = t('errors.cant_delete_contact_with_invoices')
      end
    
      unless @contact.offers.count.zero?
        alert = t('errors.cant_delete_contact_with_offers')
      end
    
      return redirect_back(fallback_location: contacts_path, alert:) if alert
    
      @contact.destroy
    
      redirect_to contacts_path, status: :see_other
    end
    

    我们实际上并没有使代码更短或更少分支,但我们获得了一些见解 - 有查找可能问题的逻辑,并且此方法有两种结束方式 - 出现错误时进行 redirect_back,或者破坏联系。

    变体 2

    让我们将设置警报移至单独的私有方法,使操作更加简洁

    def destroy
      return redirect_back(fallback_location: contacts_path, alert:) if alert
    
      @contact.destroy
    
      redirect_to contacts_path, status: :see_other
    end
    
    private
    
    def alert
      unless Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')
        return t('errors.no_access_rights')
      end
    
      unless @contact.invoices.count.zero?
        return t('errors.cant_delete_contact_with_invoices')
      end
    
      unless @contact.offers.count.zero?
        return t('errors.cant_delete_contact_with_offers')
      end
    end
    

    这里的命名可能不是很好,但这只是快速的头脑风暴。

    这再次增强了核心destroy方法的可读性,并且使得移动细节变得更加简单,从而导致:

    变体 3

    为什么我们要检查模型是否可以删除?控制器不需要知道这一点,这些知识可能存在于模型中。此外,ActiveRecord 模型有一种很好的原生方式来管理验证和错误,因此这将帮助我们制作更标准的 Rails 端点。此处供参考 - Rails 文档和Rails 指南

    # app/models/contact.rb
    class Contact < ApplicationRecord
      ...
    
      before_destroy :validate_destruction, prepend: true
    
    
      private
    
      def validate_destruction
        unless Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')
          errors.add t('errors.no_access_rights')
          return false
        end
    
        unless invoices.count.zero?
          errors.add t('errors.cant_delete_contact_with_invoices')
          return false
        end
    
        unless offers.count.zero?
          errors.add t('errors.cant_delete_contact_with_offers')
          return false
        end
      end
    end
    
    # app/controllers/contracts_controller.rb
    
    def destroy
      if  @contact.destroy
        redirect_to contacts_path, status: :see_other
      else
        redirect_back(fallback_location: contacts_path, alert:) if alert
      end
    end
    

    漂亮又干净。

    我并没有真正测试过所有的代码,所以我不能保证它能完全按照这种方式工作,我只是想指出方向

    一些一般的想法:

    • 您可以自行决定控制器中驻留的逻辑量。但是,控制器中的代码更难测试和重用。将代码放在模型中,可以非常轻松地为具有不同状态的联系人编写单元测试,检查是否允许我们删除它。
    • 甚至还有一种有点极端的方法——让你的控制器始终只对一个对象执行一个动作,并返回 ok 或 not ok 的响应。
    • 询问“谁应该知道/做这件事?”。控制员是否应该知道联系人有发票?只有在必要时才知道。这里不需要。
    • 您可以随时尝试移​​动代码,将其捆绑到函数/方法/帮助程序/... 中,然后看看感觉如何。 试验、尝试,并获得对良好代码的直觉。
    • 2
  2. Best Answer
    max
    2025-03-07T03:37:32+08:002025-03-07T03:37:32+08:00

    Rubucop 的抱怨是正确的。循环复杂性实际上表明此代码存在一些问题。一般来说,如果您的方法包含多个显式返回,请停止并重做。

    1. 授权很重要。请妥善对待授权。

    像这样进行“内联”授权是一个非常糟糕的主意:

    unless Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')
        alert = t('errors.no_access_rights')
    end
    
    • 当未经授权时,它不能正确保释并阻止进一步执行。
    • 您正在重复授权逻辑。
    • 你返回了错误的状态代码,所以客户端认为除了闪现消息之外一切都正常。这对可测试性来说非常糟糕。
    • 有太多部分悬垂在模型之外,并且这三行代码违反了迪米特法则。

    虽然最好的解决方案是添加一个授权层(例如 Pundit),但您至少应该借鉴 Pundit/CanCan 的做法,在请求未获得授权时引发异常,以确保它能被保释并且任何进一步的回调都会停止。

    # app/errors/authorization_error.rb
    class AuthorizationError < StandardError
    end
    
    class ContactsController
      # you probably want to reuse this for creating and editing
      before_action :authorize_contact, only: [:destroy]
    
      private
    
      def authorize_contact
        # This still stinks but fixing it completely is out of scope
        if entity_owner? || manage_contacts?
          @contact = Contact.find(params[:id])
        else
          raise AuthorizationError      
        end
      end
    
      def default_entity
        Current.user.default_entity
      end
    
      def entity_owner?
        default_entity.owner == Current.user
      end
    
      def manage_contacts?
       default_entity.user_privileged?('manage_contacts')
      end
    end
    
    class ApplicationController < ActionController::Base
      rescue_from AuthorizationError, with: :deny_access
    
      def current_user
        Current.user
      end 
    
      def deny_access
        # either redirect to the login or render an error page
        render plain: "Oh uh. You can't do that silly bear!",
          status: :forbidden
      end
    end
    

    作为额外的奖励,您可以在测试/规范中绕过 rescue_from 来断言/预期引发异常。

    更好的方法是提取, Current.user.default_entity.owner == Current.user || Current.user.default_entity.user_privileged?('manage_contacts')这样您的控制器就不会对模型有太多了解。Pundit 使用 Policy 类来实现这一点,您可以使用 PORO 进行模拟。

    2. 你让事情变得比应有的更难

    不要仅仅假设它@contact.destroy会成功,而要检查返回值。如果销毁失败,这些restrict_with错误关联将填充错误对象。

    就像创建/编辑对象时一样,您可以使用它们向用户提供反馈。不同之处在于,您想将它们推入闪存中(“警报”实际上只是闪存中的一个键),而不是将它们显示在表单上。

    class ContactsController < ApplicationController
      before_action :authorize_contact, only: [:destroy]
    
      def destroy
        if @contact.destroy
          redirect_to contacts_path, status: :see_other
        else
          flash[:alert] = @contact.errors.full_messages.join(', ')
          redirect_back(fallback_location: contacts_path)
        end
      end
    end
    

    或者,你可以使用数组来代替闪存消息的不稳定字符串连接。

    class ContactsController < ApplicationController
      before_action :authorize_contact, only: [:destroy]
    
      def destroy
        if @contact.destroy
          redirect_to contacts_path, status: :see_other
        else
          flash[:alert] = @contact.errors.full_messages
          redirect_back(fallback_location: contacts_path)
        end
      end
    
      # ...
    end
    

    然而,这将需要对布局进行一些更改以处理显示数组闪存消息,我将其留给读者。

    • 1
  3. Jasjeet Singh
    2025-03-06T15:51:27+08:002025-03-06T15:51:27+08:00

    您可以使用 before_action 或辅助方法重构 destroy 方法,该方法返回一个布尔值,指示是否应继续删除。您可以使用一个保护子句方法,如果删除被阻止,则返回 true,并在其中执行重定向。

    before_action :authorize_destroy, only: :destroy
    before_action :check_dependent_records, only: :destroy
    
    def destroy
      @contact.destroy
      redirect_to contacts_path, status: :see_other
    end
    
    private
    
    def authorize_destroy
      return if 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
    
    def check_dependent_records
      if @contact.invoices.exists?
        redirect_back(fallback_location: contacts_path, alert: t('errors.cant_delete_contact_with_invoices')) and return
      end
    
      if @contact.offers.exists?
        redirect_back(fallback_location: contacts_path, alert: t('errors.cant_delete_contact_with_offers')) and return
      end
    end

    • -1

相关问题

  • 在 Active Admin 中显示 Rails 服务器信息

  • Grape api (rails) - 未初始化常量 Endpoints::TodoAPI (NameError)

  • 我必须使用哪个回调来获取先前的值?

  • Rails - 停止提供任何前端视图

  • 定义方法时,在 ruby​​ on Rails 中出现未定义方法错误

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve