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:
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.