views:

90

answers:

4

I'm developing an online store, and the customer needs the ability to delete an order and have its products automatically restocked (e.g., for test orders). Here's my first try at implementing this:

class Order < ActiveRecord::Base
  def destroy_and_restock
    restock_products
    destroy
  end

  protected

  def restock_products
    line_items.each do |li|    
      li.product.quantity_on_hand += li.quantity
      li.product.save
    end
  end
end

But what if I need to create another destroy_and_x method later? Why not allow that X to be passed as a parameter to the destroy() method? So now I'm thinking of going with this:

alias :old_destroy :destroy
def destroy(options = {})
  if options['restock'] == true
    restock_products
  end
  old_destroy
end

protected

def restock_products
  line_items.each do |li|    
    li.product.quantity_on_hand += li.quantity
    li.product.save
  end

This is more extensible, but makes me feel somewhat dirty. Am I wrong to feel dirty? Is there a better way of doing this?

+2  A: 

I'd say "yes, this is dirty." Your intention isn't to modify the behavior of the 'destroy' method, but rather to do some domain-specific work, then run destroy. Your first approach is great -- define a method that does what you want, and invoke destroy as needed. I think that 'wrapping' or 'monkey-patching' a method, as you're considering, is a technique that's best applied when standard OO approaches can't be used -- eg, when you need to modify/augment behavior in a class that is defined and used outside of your realm of control.

Even if you are considering modifying the behavior of the destroy method itself, I'd suggest overriding the method here, rather than wrapping it:

def destroy(options = {})
  restock_products if options['restock']
  super() # I think parens are necessary here, to avoid passing options up the chain
end
joshng
First of all, your implementation of the "dirty" way is obviously much better than mine. Second of all, I'm not sure my "intention isn't to modify the behavior of the 'destroy' method" -- aren't I "really" trying to make the destroy method take parameters?
Horace Loeb
I'll add: overriding the method would be more appropriate if you were wanting to ensure that something ALWAYS happened when destroy was called -- but that still wouldn't require aliasing. A clue that you shouldn't be overriding/aliasing: your "optional" options parameter here would never be passed by the ActiveRecord framework when it was invoking 'destroy' for you. Only a knowledgeable caller would know how to pass the options, so they'd equivalently know to use another method instead.
joshng
Our comments overlapped, so I'll reply to yours now :-). Nope, I don't think you really are wanting to modify the behavior of 'destroy'. You're wanting to add new behaviors that include performing the existing 'destroy' behavior.Consider: you could take your idea to the extreme, and just write one method that does everything in the class (say, call it... "send" ;-). Then switch based on the "options" to decide what to do.
joshng
Eh, ok, I'll add another comment, at risk of driving this into the ground: my suggestion is directly motivated by the fact that this is an ActiveRecord class, and the destroy method is part of the AR API. If you were defining your own method, I'd say you'd be free to set up the interface however you like, and the options would be a good idea. But this is the AR::Base.destroy method, with well-known semantics, so adding behaviors there is dangerous business. HTH
joshng
You win! I'll use restock_and_destroy()! What do you think about Ryan Oberoi's response?
Horace Loeb
Heh... I'm not a fan of RyanOberoi's suggestion at all. See my comment below. But it does remind me that AR provides builtin support for attaching behavior to lifecycle events, including destroy... Take a look at AR::Base.before_destroy. (It wouldn't let you do what you're describing -- parametrized destruction -- but it's good to know about, and more flexible than overriding the destroy method itself.) ANYway, mind clicking the checkmark if you like my answer? :-)
joshng
A: 

How about just use a block? Then you dont have to pull your hair apart while designing this in the class and you can do more as and when you need to:

def destroy_after &block
  yield if block
  destroy
end

Then call it like this:

order.destroy_after { order.restock_products }

I can not think of a good name for this function... but I hope you get the idea.

Ryan Oberoi
One problem here is that I can't think of a reason you'd want to restock_products() outside the context of destroy(), and so I thought I'd make restock_products() protected (which I couldn't do with your solution).
Horace Loeb
I don't see what this approach accomplishes... you might as well just run the block outside, then call destroy:order.restock_productsorder.destroy
joshng
err... formatting got lost. order.restock_products; order.destroy
joshng
A: 

Horace, I misunderstood your question. I think you are looking for this:

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

Now you can keep your method protected and add as many before_destroy things as you like. Hope this works for you without overriding destroy.

Best of luck.

Ryan Oberoi
A: 

If monkey patching doesn't let you sleep at night, you can achieve the same thing by subclassing. When I'm in the need of a quick hack, or a quick debug hack, I monkey patch as well.

Geo