【问题标题】:Creating another record from within the model: bad practice?从模型中创建另一个记录:不好的做法?
【发布时间】:2018-05-03 18:38:11
【问题描述】:

我有三个模型:任务、用户和响应。

当用户完成一项任务时,结果将存储为响应。在该响应时间内,用户将获得积分。

我的第一个问题是,更新积分属性的逻辑应该去哪里?在任务、用户或响应模型中?目前,我在 Response 模型中有它,它获取 Response.task.points 并将该值添加到 User.task.points。

Response.create 然后看起来像:

  # POST /responses
  # POST /responses.json
  def create
    @response = Response.new(response_params)

    respond_to do |format|
      if @response.save
        @response.reward_user

        format.html { redirect_to @response, notice: 'Response was successfully created.' }
        format.json { render :show, status: :created, location: @response }
      else
        format.html { render :new }
        format.json { render json: @response.errors, status: :unprocessable_entity }
      end
    end
  end

第二件事是我想记录每笔积分交易。所以我创建了另一个名为 points_transaction 的模型。我的另一个问题是,我应该在哪里创建 points_transaction?在 Response.create 控制器中?在响应模型中?

从 Response 创建方法中创建 PointsTransaction 似乎是错误的,但从模型中创建它似乎同样错误。哪一个更符合 MVC?

我的 Response 对象如下所示:

class Response < ApplicationRecord
  belongs_to :task, optional: true
  belongs_to :user, optional: true

  def reward_user
    point_value = task.point_value
    user.points += point_value

    PointTransaction.new({/*params go here*/})
  end
end

【问题讨论】:

  • 你有一个很好的问题。我认为你已经达到了一个点,当你需要进入下一个层次的架构解决方案时:)

标签: ruby-on-rails activerecord model-view-controller ruby-on-rails-5


【解决方案1】:

解决此类问题的一种常见方法是创建服务对象,该对象可以将所有处理代码包装到一个代码“单元”中。您将让服务对象检查Response,然后更新用户的积分并跟踪PointTransaction,这样您就可以保持控制器的瘦身,并且可以防止您的模型接触其他模型或产生其他可能不清楚的副作用.

假设您在 app 目录中创建了一个名为 services 的文件夹,并将您的服务类放在那里。

# app/services/response_checker.rb
class ResponseChecker
  attr_reader :success

  def initialize
  end

  def call(response, task, user)
    @success = if response.save
      user.points += task.point_value
      point_trans = PointTransaction.new(/*params go here*/)

      user.save && point_trans.save
    else 
      false
    end
  end
end

然后,在您的控制器中使用该服务:

# app/controllers/response_controller.rb
def create
  @response = Response.new(response_params)
  @response_checker = ResponseChecker.new.call(@response, @response.task, @response.user)

  respond_to do |format|
    if @response_checker.success
      # conditional controller response logic
    end
  end
end

您可能希望在服务对象上创建一个错误属性,以在服务执行期间捕获有关错误的信息,然后将它们公开给正在使用该服务的人/任何人(在本例中为控制器)。

【讨论】:

  • 快速提问。初始化函数的目的是什么?与调用相比,我应该将什么作为初始化变量传入?
  • 我把initialize 方法放进去是为了清楚它在这里真的没有做任何特别的事情。我们可以把它排除在外,一切照旧。关于如何实例化服务和与服务交互有一些不同的看法,但我喜欢的一种方法是使用 new/initialize 注入可能需要的任何其他服务类型依赖项,并使用 call 来传入我们需要用来完成工作的状态的对象。这种方式更容易测试。这里有一些例子:hackernoon.com/…
【解决方案2】:

我认为您肯定遇到了一种情况,即您需要超越简单的 MVC

首先,在理想世界中,模型根本不应该相互了解。所以你不应该像你一样引用Response 的任何其他模型。另一方面......控制器肯定更糟糕,然后将其放入模型中。

其次,如果您不确定将代码放在两个位置之间的哪个位置。那么这两个都不够好,你需要寻找第三个。

这就是服务对象进入游戏的时候。它在 Rails 中非常广泛地传播和常见的模式。我不确定它是否是完美的解决方案,但它可以解耦你的代码,让你的代码保持干净和易于测试。我还没有发现这种方法有任何问题,除了有太多的服务对象:)。

这是一个包含逻辑的示例,涉及我们项目中的几个模型 (app/services/active_site_service.rb):

class ActivateSiteService
  attr_reader :error

  def initialize(user, template, password)
    @user = user
    @template = template
    @activation = @user.activation_for(@template)
    @password = password
  end

  def call
    return false unless self.valid?

    generate_site_service = GenerateSiteService.new(@user, @template)
    generate_site_service.call

    @activation.update(quantity: @activation.quantity - 1)

    @user.transactions.create(status: :success,
                              target: generate_site_service.site,
                              amount: 0,
                              transaction_type: :site_activation)
    true
  end

  protected
  def valid?
    validate_password && validate_activation
  end

  def validate_password
    return true if @user.valid_password?(@password)
    @error = 'Неправильный пароль'
    false
  end

  def validate_activation
    return true if @activation.present? && @activation.quantity > 0
    @error = 'У вас нет предоплаченных активаций'
    false
  end
end

我们遵循的规则:

  1. 从概念上讲,服务对象是一个业务流程,包括几个模型
  2. 名称总是以动词开头
  3. 服务对象只有两种方法:initializecall
  4. call 总是只返回真/假
  5. 只允许使用 2 个 attr_reader 变量 -- result 从服务中获取一些数据或对象,或者 error 获取错误

控制器:

class ActivationsController < ApplicationController
  def create
    template = Site.templates.find(params[:template_id])

    activate_site_service = ActivateSiteService.new(current_user, template, params[:password])

    if activate_site_service.call
      redirect_to sites_path, notice: 'Активация сайта прошла успешно'
    else
      redirect_to new_purchase_path(template_id: template.id), alert: activate_site_service.error
    end
  end
end

如果你不能落入这些规则,那么它主要不是服务对象。您可以 google 更多关于服务对象的信息

【讨论】:

  • 嗯。有趣的。服务模式将走向何方?在库中?
  • 另外,谁调用了服务对象?服务对象应该是另一个控制器吗?还是我应该从 Response.create 中调用它?这意味着它将记录响应并奖励用户作为响应中的原子操作?
  • 在我的回答中包含了所有内容。为什么我不会选择 lib。 Lib 主要是一个模块,它是一个单独的库,可以在其他项目中使用。所以对我来说,lib 更像是技术助手,与业务逻辑没有任何关系。与业务逻辑相关的所有内容都应进入app/ 文件夹
猜你喜欢
  • 2021-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多