5

假设我有一个产品模型和 ProductsController。控制器具有所有标准的 CRUD 方法,产品进行各种验证等。

这是一个问题。我有几个自定义的非常复杂的操作,它们也需要以多种格式(json、html、xml、csv、pdf 等)响应。造成这种情况的业务逻辑原因超出了问题的范围。让我们就这样吧,这是它必须完成的方式。我也使用 InheritedResources gem,但我认为这对这个问题并不重要。

例如(这是一个模拟应用程序,非常简化 - 我删除了各种 if else 语句、循环和本地化等):

class ProductController < InheritedResources::Base
  ....
    def check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order
      @order = Order.new
      @product = Product.find(params[:legacy_alphanumeric_product_number])
      if @product.stock > 5
        @po = LegacyOrder.create_po
        if @po
          if @order.save
            format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {success: "Wow! Input was good!"}}
            format.json{ render status: 400, json: {status: :success, message: "Order created"}}
          else
            format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {error: "Can't create order, some validations failed"}}
            format.json{ render status: 400, json: {status: :error, message: "Problem with order", errors: @order.errors}}
          end
        else
          format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {error: "Can't create order, PO number wasn't generated"}}
          format.json{ render status: 400, json: {status: :error, message: "Problem with po", errors: @po.errors}}
        end  
      else
        respond_to do |format|
          format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {error: "Can't create order, stock is low"}}
          format.json{ render status: 400, json: {status: :error, message: "Problem with product", errors: @product.errors}}
        end
      end  
    end   
  ....
end 

这只是为了说明某些操作的复杂性。

现在的问题是:所有这些优点都应该转移到模型中吗?我正在处理应该在控制器中的业务逻辑,但是在尝试遵循胖模型和瘦控制器的经验法则时,在我看来它应该被移走,如果是这样,那么还有什么需要移动?

奖励问: 我遇到了可能需要在代码中使用某些功能的用例,而不是通过 REST 接口。IE 我需要在运行 rake 任务时使用 check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order。就像根据低库存或电子邮件事件等生成一些订单。虽然我可以使用此处描述的选项:如何从 Rails 的控制台调用控制器/视图方法?,将这个动作作为模型的一部分会更容易,不是吗?

那么在这种情况下,Rails 最佳实践的行动方案是什么?

4

3 回答 3

6

考虑将您的逻辑移动到服务对象中。我认为将控制器逻辑推入模型只是将问题转移到不同的位置。是的,您确实将逻辑隔离到单个区域,但在某些情况下,您最终将逻辑移动到模型中,因为约定而不是它真正属于那里的事实。

服务对象可以帮助您减少重复并隔离您的业务逻辑,而不会让模型过多地参与它不需要知道的事情(例如,您的重复 json 响应)。

class OrderService
  def initialize(legacy_alphanumeric_product_number)
    # do stuff
  end
  # do more stuff
end

从控制器,您可以调用

def check_whatever
  @order = OrderService.new(params[:some_product_code])
  @order.check_something
  # do more stuff
end

看看7 种模式来重构 Fat ActiveRecord 模型。我发现它很有帮助。还有一个关于服务对象的RailsCasts 集(需要专业订阅)。

于 2013-06-03T23:53:26.987 回答
2

看看 Draper gem ( https://github.com/drapergem/draper )。它提供了很好的装饰风格包装器,这是将逻辑放在显示或响应内容上的理想场所。我同意@mohamad 的观点,它不属于模型或控制器。服务对象绝对是一个好方法,或者使用 Draper 创建特定于表示的逻辑方法。

于 2013-06-03T23:56:43.347 回答
0

正如我所看到的,您的问题不是将代码从控制器移动到模型,而是重构控制器方法

check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order

举个例子

format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {success: "Wow! Input was good!"}}
format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: {error: "Can't create order, stock is low"}}

除了他们的闪光信息外,其他的都是一样的。因此,您可能只生成 flash 消息,例如:

#conditions for the flash message
format.html{ render :check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, flash: msg}

因此,请尝试重新考虑您的代码,并且我个人不会将 xml、json 放在同一个控制器中。因为拥有像这样的 web api 方法check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order对我来说并不好看 :)

我会将我所有的 web api 方法移动到一个命名空间,例如:

<url>/api/ etc

如果你有一个很好的测试覆盖率,你可以继续重构代码,如果不是现在你知道拥有一个好的测试套件的好处:)

于 2013-06-04T00:01:45.263 回答