为了实现瘦控制器,Rails模型是否应该与其他模型一起使用?

时间:2020-03-05 18:53:49  来源:igfitidea点击:

我到处都读到业务逻辑属于模型而不是控制器,但是限制在哪里呢?
我在玩个人会计应用程序。

Account
Entry
Operation

创建工序时,只有创建相应的条目并将其链接到帐户,这样才能平衡工序,例如购买6件装:

o=Operation.new({:description=>"b33r", :user=>current_user, :date=>"2008/09/15"})
o.entries.build({:account_id=>1, :amount=>15})
o.valid? #=>false
o.entries.build({:account_id=>2, :amount=>-15})
o.valid? #=>true

现在,在基本操作情况下向用户显示的表单已简化,以隐藏条目详细信息,根据用户请求的操作类型(默认帐户->权益资产,支出资产- >费用,赚取收入->资产,借入负债->资产,偿还债务资产->负债...)我希望从默认值创建条目。

我还希望能够创建更复杂的操作(超过2个条目)。对于第二个用例,我将采用另一种形式来显示额外的复杂性。第二个用例使我无法在"操作"中包括借方和贷方字段,并且摆脱了"输入"链接。

最好的形式是什么?像现在一样,在SimpleOperationController中使用上面的代码,或者在Operation类上定义一个新方法,以便我可以调用Operation.new_simple_operation(params [:operation])

真正从Operation类创建和操作Entry对象是否打破了关注的分离?

我不是在寻求关于我扭曲的会计原则的建议:)

编辑-似乎我并不太清楚表达自己的意思。
我不太担心验证。我更担心创建逻辑代码应该去哪里:

假设控制器上的操作称为支出,则在使用支出时,params哈希将包含:金额,日期,描述。借方帐户和贷方帐户会从被称为操作的操作派生,但是随后我必须创建所有对象。有没有更好

#error and transaction handling is left out for the sake of clarity
def spend
  amount=params[:operation].delete(:amount)#remove non existent Operation attribute
  op=Operation.new(params[:operation])
  #select accounts in some way
  ...
  #build entries
  op.entries.build(...)
  op.entries.build(...)
  op.save
end

或者在Operation上创建一个方法,使上面看起来像

def spend
  op=Operation.new_simple_operation(params)
  op.save
end

这肯定会提供更薄的控制器和更胖的模型,但是随后该模型将创建并存储其他模型的实例,这正是我的问题所在。

解决方案

回答

就每个实体进行自我验证而言,以及相互依赖的实体将其状态委托给其关联条目的状态时,都更容易想到。以情况为例:

class Operation < ActiveRecord::Base
  has_many :entries
  validates_associated :entries
end

validates_associated将检查每个关联实体是否有效(在这种情况下,如果该操作有效,则所有条目都应有效)。

试图验证整个模型的整个层次结构是非常诱人的,但是正如我们所说的,最容易做到的地方是控制器,控制器应更多地充当请求和响应的路由器,而不是与业务打交道。逻辑。

回答

我的看法是,控制器应反映最终用户的观点,并将请求转换为模型操作和响应,同时还要进行格式化。在情况下,有两种操作代表使用默认帐户/条目的简单操作,以及具有用户选择的条目和帐户的更复杂的操作。表单应反映用户视图(2个表单具有不同的字段),并且控制器中应有2个动作要匹配。但是,控制器不应具有与如何处理数据有关的逻辑,而应具有与如何接收和响应有关的逻辑。我将在Operation类上使用类方法,该类方法从表单中获取适当的数据并根据需要创建一个或者多个对象,或者将这些类方法放在不是AR模型但具有跨模型业务逻辑的支持类上边界。单独的实用程序类的优点是使每个模型都专注于一个目的,不利的一面是这些实用程序类没有定义的生存空间。我将它们放在lib /中,但是Rails并没有为模型助手指定位置。

回答

虚拟属性(此处和此处的更多信息)将极大地。将整个参数传递回模型可以使控制器中的事情变得简单。这将使我们能够动态地构建表单并轻松地构建entry对象。

class Operation
  has_many :entries

  def entry_attributes=(entry_attributes)
    entry_attributes.each do |entry|
      entries.build(entry)
    end
  end

end

class OperationController < ApplicationController
  def create
    @operation = Operation.new(params[:opertaion])
    if @operation.save
      flash[:notice] = "Successfully saved operation."
      redirect_to operations_path
    else
      render :action => 'new'
    end
  end
end

如果一切无效,保存将失败。这使我们进行了验证。因为每个条目都是独立的,并且我们需要在"创建"时检查所有条目,所以我们可能应该覆盖Operation中的validate:

class Operation
  # methods from above
  protected
    def validate
      total = 0
      entries.each { |e| t += e.amount }
      errors.add("entries", "unbalanced transfers") unless total == 0
    end
end

现在,我们将收到一条错误消息,告诉用户金额已用完,他们应该解决该问题。我们可以在这里变得很特别,可以通过具体说明问题来增加很多价值,例如告诉他们解决了多少问题。

回答

but then the model will create and store instances of other models which is where my problem is.

这有什么问题?

如果"业务逻辑"指出某个Operation必须具有一组有效的Entries,那么对于Operation类而言,可以肯定并处理Entry对象没有任何错误。

如果我们走得太远,并且让模型操纵他们不需要了解的事情,例如EntryHtmlFormBuilder或者其他任何东西,我们只会遇到问题。

回答

如果我们担心将这种逻辑嵌入到任何特定模型中,为什么不将它们放入观察者类中,这将使我们创建关联项的逻辑与所观察的类分开。