The Shared Green: Rails Polymorphism Commons

When writing a Rails app I love polymorphic relations. If there’s a good chance I can reuse a model with other objects and I can’t make it a PORO (plain old Ruby object), my relation of choice is likely to be a polymorphic one.
Once challenge with polymorphism is documenting the interface future objects will need to use. For example, let’s say I have a shopping application where a Cart has_many CartItems. Of course all sorts of items can be in a cart, so let’s make the CartItem polymorphic and ‘cartable’
class Cart < ApplicationRecord
  has_many :cart_items, dependent: :destroy
end

class CartItem < ApplicationRecord
  belongs_to :cartable, polymorphic: true, required: true
  belongs_to :cart
end
So far so good. At first I sell T-shirts. When the t-shirt is added to the cart, I want a nice description of it, so I add a cart_line_description:
class Shirt
  def cart_line_description
    "#{color} T-shirt, size #{size}, with #{design.description}"
  end
Six months later I decide to start selling Pants as well. The challenge is how to remember all the methods I need to have? Of course looking through the tests as well as my Shirt object: with trial-and-error I’ll eventually find everything. However it would sure be nice to have that documented somewhere.

Module As Commons

Thanks to my programming idol Sandi Metz and POODR (Chapter 7 specifically), I came to embrace the use of modules. As I thought of them as a shared space for multiple objects, my mind went back to a Town called Tarboro in NC and their town commons:
The town commons in Tarboro, NC
‘Commons’ seemed like a great description of these programmatic shared spaces, so I created CartableCommons:
module CartableCommons
  ##
  # The amount that shows in the cart at checkout before adjustments
  def cart_line_amount
    estimate_charge
  end
  ...
end
..and of course it’s a great place to put shared behavior, but what about in cases where the behavior is so different it’s not definable or I don’t have a sane default? I wanted to still put something in place to let me know the method needed tackled so I didn’t have to go combing through code.

If It Ain’t Broke Yet, Make it Broken

In these cases I really wanted to just get notified I needed to do something, so enter ‘NotImplementedError‘. By including the method in the module and making the default to raise this, I get a much better idea if what I need (okay, forgot) to do:
module CartableCommons
  ##
  # Description that shows for the cart item
  def cart_line_description(show_previously_paid: true)
    raise NotImplementedError
  end
  ...
end
This gives me a lovely error and tells me where to look for more information:
Completed 500 Internal Server Error in 220ms (ActiveRecord: 52.2ms)


  
NotImplementedError (NotImplementedError):
  
app/modules/cartable_commons.rb:46:in `cart_line_description'
So using a module as my commons gives me the important benefits of sharing behavior, defining common behavior, and most importantly, imagining my objects as little shepherds taking turns with their herds in a verdant shared space.