5

Let's say I have a Product model and ProductsController. Controller has all the standard CRUD method and Product does all kinds of validations etc.

Here is an issue. I have several custom very complex actions which also need to respond in multiple formats( json, html, xml, csv, pdf etc). Business logic reasons for this are beyond the scope of the question. Let's just way this is the way it has to be done. Also I use InheritedResources gem but I don't think it matters for the question.

For example ( this is a mock app, which is GREATLY simplified - I removed all kinds of if else statements and loops and localization, etc ):

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 

This is just to give an idea of complexity of some actions.

Now the question: should all of that goodness be moved into model? I'm dealing with business logic, which should be in the controller, but with trying keep with the rule of thumb Fat Models & Thin Controllers, it seems to me that it should be moved away, if so then what there is left to move?

Bonus Q: I'm coming accross use cases where I might need to use some of that functionality in the code, not through a REST interface. I.E. I need to use check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order, while running a rake task. Like generating some orders based on low stock, or from an email event, etc. While I can use options described here: How do I call controller/view methods from the console in Rails? , having this action as part of a model would make it easier wouldn't it?

So what is a Rails Best Practices course of action in a case like this?

Community
  • 1
  • 1
konung
  • 6,908
  • 6
  • 54
  • 79
  • i cant help you. i just see `: check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order` EVERYWHERE!!! my favourite part `render status: 400, json: {status: :success` – phoet Jun 03 '13 at 23:47
  • Well know you understand my pain :-) That's why I'm trying to refactor this – konung Jun 04 '13 at 04:29

3 Answers3

6

Consider moving your logic into a service object. I think shoving controller logic into the model is simply moving the problem to a different location. Yes, you do isolate the logic to a single area, but there are instances where you end up moving logic to models because of convention rather than the fact that it really belongs there.

A service object can help you reduce duplication and isolate your business logic without getting the model too involved in things it does not need to know about (your repeated json response, for example).

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

From the controller, you can just call

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

Take a look at 7 Patterns to Refactor Fat ActiveRecord Models. I found it very helpful. There is also a RailsCasts episode on service objects (requires pro subscription).

Mohamad
  • 34,731
  • 32
  • 140
  • 219
  • So essentially you would use SomeService as a proxy object to comunicate between Model and Cotroller. Where would you put it? Just create app/service_objects and 'autoload_path' it from application.rb? – konung Jun 04 '13 at 04:34
  • I think I read this article (7 patterns) a while back and totally forgot about it . Thank you! – konung Jun 04 '13 at 04:46
  • @NickGorbikoff Depending on which version of Rails you are using, you shouldn't have to mess with your autoload paths. I just create a `services` directory inside `app` and it works automatically (Rails 4 and 3). – Mohamad Jun 04 '13 at 13:03
  • @NickGorbikoff you might have to restart the Rails app, though, if you add new folders. – Mohamad Jun 04 '13 at 14:09
  • I'm actually pretty sure you have to in Rails 3. In Rails 4 concerns will be included automatically, but I kind of agree with Ryan Bates that they seems easy to abuse and harder to follow what piece of code does. I'll go with ServiceObjects pattern. Thank you for your suggestions! – konung Jun 04 '13 at 14:47
2

Take a look at the Draper gem (https://github.com/drapergem/draper). It provides nice decorator style wrappers that are the perfect place for putting logic on what to show or respond to. I agree with @mohamad that it doesn't belong in the model or controller. A service object is definitely a good way to go, or using Draper to create presentation specific logic methods.

Alex Peachey
  • 4,606
  • 22
  • 18
  • Alex just to see if I got this right: Draper is used mostly for ModelView (so to speak). So it would help me define JSON view, XML view, etc for my models, but as far as complex actions go, I should still go with ServiceObject pattern suggested by Mohamed. Then it seems Draper wouldn't work as it relies on MC magic part of Rails MVC? Or am I wrong? – konung Jun 04 '13 at 05:27
  • Actually I would consider using both a service object and Draper, though based on the logic you shared you could encapsulate that in just a Draped Object or just a Service Object. I usually put light logic in a Draped Object and if it starts getting too heavy move it out into a Service Object. – Alex Peachey Jun 04 '13 at 07:06
  • Thank you Alex for your suggestion. For my specific question ServiceObjects are the answer ( thus I accepted Mohammed's answer). And I kind of have something in my app that acts as a draper - solution I came up with: I have app/outlines/pdfs/PdfOutline, csv/CsvOutline, xls/XlsOutline - which serve as my drapers / templates for various formats I need. For objects where I need to override default format - ProductPdf < PdfOutline. PdfOutline inherits from Prawn::Document, CsvOutline < CSV, XlsOutline < Axlsx::Package. They just provide common interface for me to create formats, and common – konung Jun 04 '13 at 14:53
  • functionality & template parts. Any PDF I create should have phone / address / company logo in the top/bottom margin which I repeated through the document. – konung Jun 04 '13 at 14:58
0

As I can see, your problem is not moving the code to model from the controller, but re-factoring the controller method of

check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order

As an example

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"}}

are same except their flash message. So you might probably generate the flash message only, like:

#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}

So try to re factor your code and personally I will not give the xml, json out put in the same controller. Because having a web api method like check_stock_using_legacy_identifier_and_create_a_unique_po_number_and_place_an_order does not look nice for me :)

I would move all my web api methods to a namespace like:

<url>/api/ etc

And if you have a good test coverage you can just go ahead and re-factor the code, if not now you know the advantage of having a good test suite :)

halfer
  • 19,824
  • 17
  • 99
  • 186
sameera207
  • 16,547
  • 19
  • 87
  • 152
  • I know it looks long, but the whole point of this action is that from one piece of information ( like specific product number) based on some criteria you should be able to call multiple actions. And while api suggestion is not bad, it doesn't solve the problem of my "html" actions. Json & XML are really there to just mimic and provide programtic access to clients to actions that already exist.It's needed but it's secondary. That requirement was added later. – konung Jun 04 '13 at 04:28
  • I could go with a route of creating an api service first, and then have my web interface talk to it, but refactoring is not an option at this point and doesn't really deal with a problem of a complex action it's more of decoupling of interfaces suggestion. – konung Jun 04 '13 at 04:29