Task E: Check Out #1

On the question of how to more loosely couple the methods, IMHO, the "Order#add_line_items_from_cart" does too much. The order roughly maps to a cart, while a product roughly maps to a line item. I would pull the iteration through the "cart_items" out of "add_line_items_from_cart" and place it in "from_cart_item". Here's my modified (and renamed) version of that code:

In OrderController.rb:

def save_order
  @cart = find_cart
  @order = Order.new(params[:order])
  # @order.add_line_items_from_cart(@cart)
  @order.build_from_cart(@cart)
 
  if @order.save
    session[:cart] = nil
    redirect_to_index("Thank you for your order")
  else
    render :action => :checkout
  end
end

In order.rb

def build_from_cart(cart)
    line_items << LineItem.from_cart(cart)
  end

In line_item.rb

def self.from_cart(cart)
    items = []
 
    cart.items.each do |cart_item|
      li = self.new
      li.product = cart_item.product
      li.quantity = cart_item.quantity
      li.total_price = cart_item.price
      items << li
    end
 
    items
  end

linoj says:

I question if the above solution does very much. It simply moves the loop from Order to LineItem?, and doesnt relieve the coupling, as LineItem? still needs to know how CartItem? is implemented. I think the answer lies in letting from_cart_item take values from cart_item without it having to directly know the details of cart_item. I dont know Ruby that well, but perhaps LineItem? could be made a subclass of CartItem??

carlosk says:

In my opinion the tight coupling that happens is the presence of both the classes LineItem? and CartItem?. I managed to remove the CartItem? class, so now the LineItem? class no longer needs to know anything about the CartItem? class (since there is no CartItem? class). While making the required refactorings I found an issue. My initial attempt for implementing the increment_quantity method was the following:

In line_item.rb

def increment_quantity
  quantity += 1
  total_price = product.price * quantity
end

When I tested the page, I found that the quantity wasn't incrementing. After some trial and error I managed to get this method:

In line_item.rb

def increment_quantity
  self.quantity += 1
  self.total_price = product.price * quantity
end

It seems that adding the "self" keyword managed to make the changes of the quantity value persistent. I really don't understand why this works… Anyone has any ideas???

jeromio says:

I can't address your issue with class member access, but I agree with your architecture. It dovetails with the need for the cart to be in the database, as opposed to the session. The cart _is_ an order, line items are cart items. Check-out merely transfers ownership from the customer to the order handler (the back-end store). That change of ownership does nothing to the underlying structure/schema.

Not sure how you solved the specifics of the back-end fulfillment (it's skipped in the book also). I presume/propose that the table containing cartitems and line_items is the same table (since those are the same objects). The orders table will point to cart ids that are orders. If there is no order associated with a cart, then it's just a cart, not yet an order. Cart/Line items can also be in multiple states: browsing, ordered, wishlist, etc.

Jim Davis says:

A guess (I am a newbie): Assigning to self.quantity forces the assignment to use the quantity= accessor (mutator) method. Without it, it's an assignment to a local variable. I'm guessing that ActiveRecord? creates mutator methods for you by reflecting on the schema.

Anthony Ettinger says:

I think the original problem is that the attributes of cart_item are hardcopied into line_item. I am thinking of a solution similar to CartItem? model, perhaps attr_reader used inside LineItem?, but I'm not sure.

You could probably loop through cart_item and map its properties/attributes to line_item, but again I'm not sure you want all of them should your model for cart_item change. I think the best way is to get your attributes from the line_items table, and then grab those out of the cart_item object if they exist.

Russell Healy says:

I think the bad coupling is between Order and CartItem. Cart owns the CartItems, and should have the responsibility for working with them, as follows:

In store_controller.rb:

def save_order
  @cart = find_cart
  @order = Order.new params[:order]
  @cart.fill @order
  if @order.save
   # ...

In cart.rb:

def fill(order)
  @items.each do |cart_item|
    line_item = LineItem.from_cart_item(cart_item)
    order.add line_item
  end
end

In order.rb:

def add(line_item)
  line_items << line_item
end

Donald Jean-Baptiste says:

At first, I came up with the same solution as the first post, but like Linoj, I didn't feel it was the right solution.

I felt like the CartItem should manage the conversion to a LineItem (kinda like 5.to_s = "5")

Finally I came up with this:

in store_controller.rb:

@cart = find_cart
    @order = Order.new(params[:order])
    @order.add_line_items_from_cart(@cart)
    if @order.save
    ....

In order.rb:

def add_line_items_from_cart(cart)
     cart.items.each do |item|
         line_items << item.to_line_item
     end
end

in cart_item.rb:

def to_line_item
    li = LineItem.new
    li.product  = self.product
    li.quantity = self.quantity
    li.total_price  = self.price
    li
end

I commented out the code in line_item.rb.

The Moose Says

I don't claim to be an expert but I reckon you need to decouple both LineItem and Order classes from the Cart class. Before you make the changes both LineItem and Order could be adversely affected by changes to the Cart class, and what if you wanted to create Orders in the future from sources other than a Cart? My solution would be:

in line_item.rb:

def self.create_line_item(product, quantity, price)
    li = self.new
    li.product     = product
    li.quantity    = quantity
    li.total_price = price
    li
  end

Note, this is now completely de-couple from the Cart class

in order.rb:

def add_line_item(line_item)
       line_items << line_item
   end

Again, completely decoupled from Cart class.

in store_controller.rb:

def save_order
    @cart = find_cart
    @order = Order.new(params[:order])
 
    @cart.items.each do |item|
       li = LineItem.create_line_item(item.product, item.quantity, item.price)
       @order.add_line_item(li)
    end

I was thinking I might split that last bit of code down and pull out the loop into a separate (maybe private) method in the StoreController. Any thoughts?

I personally reckon it would be good if the author of the book added some comments here, considering he sent us here for tips in the first place!!!

Javi Says

I agree with some of The Moose's points. I'd do the same changes to line_item.rb, but I'd chage the Store Controller so it isn't coupled to LineItem class:

def save_order
  @cart = find_cart
  @order = Order.new(params[:order])
  @order.line_items =  @cart.create_line_items
  #...
end

Then I'd change the Cart class. In the book, we ask an order object to add line items from a cart object. My point is: since we want to create line items from a cart, we should ask the cart object to generate the line items, and each cart item to create each line item.

Cart.rb:

def create_line_items
  line_items = []
  @items.each do |item|
     li = item.create_line_item
     line_items << li
  end
  line_items
end

CartItem.rb:

def create_line_item
  LineItem.create(@product, @quantity, price)
end

Gregory Says

Hi,

According to ActiveRecord::Base documentation:
"Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when you‘re receiving the data from somewhere else….."

Therefore what I propose to decouple LineItem from CartItem is to make following changes:

1. In CartItem class add the method which returns hash that can be used for LineItem initialization:

cart_item.rb

def line_item_hash
    parameters = {:product=>@product, :quantity=>@quantity, :total_price=>self.price}
end

2. Slightly change add_line_items_from_cart method in Order class. Instead of calling LineItem class method we create new LineItem instance, passing hash parameter described above.

order.rb

def add_line_items_from_cart(cart)
    cart.items.each do |item|
        #  li = LineItem.from_cart_item(item)     -- commented out!
        li = LineItem.new(item.line_item_hash)
        line_items << li
    end
end

3. LineItem class looks now pretty simple:

line_item.rb

class LineItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :order
 
end

Ico Says

Hello,

After exploring your solutions came out with combination of those of the Moose & Gregory

Here it is:

In Store Controler
save_order

def save_order
    @cart = find_cart
    @order = Order.new(params[:order])
 
    @cart.items.each { |i| @order.order_items << OrderItem.new( :product => i.it, :qty => i.qty, :total => i.price ) }
    if @order.save
        session[:cart] = nil
        redirect_to_index("Thank you for your order" )
    else
        render :action => :checkout
    end
  end

In OrderItem -> LineItem according the book we do not need no additional methods so it looks as in George posting

In Order model

class Order < ActiveRecord::Base
  has_many    :order_items 
  attr_writer :order_items
  PAYMENT_TYPES = [
    # Displayed          stored in db
    [ "Check" ,          "check" ],
    [ "Credit card" ,    "cc"   ],
    [ "Purchase order" , "po" ]
  ]
 
  validates_presence_of :name, :address, :email, :pay_type
  validates_inclusion_of :pay_type, :in => PAYMENT_TYPES.map {|disp, value| value}
end

and we done with decoupling :)

But the actual question is do we really need those Cart & CartItem objects or we could implement the functionality into Order & LineItem itself and just save them on order_save :) ?

regards,
ico

vastus Says

Hi,
Thanks for all these posts, I learned some things:) I thought to do this a slightly different way so thought to put it up.

In store_controller I did

@cart = find_cart
    @order = Order.new(params[:order])
    @order.add_line_items( @cart.items_to_ahash )
    if @order.save
# ...

In order there is

def add_line_items( collection )
    collection.each do |item|
      li = LineItem.from_hash_item( item )
      line_items << li
    end
  end

Note the collection is the array so we don't have to know how cart stores items. We just expect them to be in the hash.

line_item isn't much different than before…

def self.from_hash_item( item )
    return nil if item.nil?
    li = self.new
    li.product    = item[:product]
    li.quantity   = item[:quantity]
    li.total_price = item[:total_price]
    li
  end

cart passes the message to cart_items to get themselves into hashes

def items_to_ahash
    arr  = Array.new
    @items.each { |item| arr << item.to_hash }
    arr
  end

cart_items get themselves into a hash

def to_hash
    { 
      :product   => self.product,
      :quantity  => self.quantity,
      :total_price => self.price
    }
  end

not sure if there's any improvement to much on the last few ways. I agree it would be nice to have some direction from a pragmatic master:) maybe they're busy with their gerbils? :P

page_revision: 14, last_edited: 1213927747|%e %b %Y, %H:%M %Z (%O ago)
Unless stated otherwise Content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License