When working with subclasses ensure methods called are available on all possible instances

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the object-oriented-programming category.

Last Updated: 2024-03-28

In this particular online store, what was known as a "product" could be either be an instance of IndividualProduct or of BundleProduct - both inherited from the same base Product.

With that in mind, I had the following front-end code:

<td class="line-item-name">
  <%= link_to product.name, admin_subject_path(product.subject) %>
</td>

The problem here was that only instances of IndividualProduct had a subject method. Instances of BundleProduct did not, these being related to many products and therefore only permitting of plural subjects. This caused a fatal error since these objects did not respond to the subject message.

The fix was to branch the logic.

  <% if product.bundle? %>
    <%= link_to product.name, admin_notes_pack_path(product.notes_pack) %>
  <% else %>
    <%= link_to product.name, admin_subject_path(product.subject) %>
  <% end%>

--

Another time: I added a JSON endpoint that translates the price on page for a given product to the discounted price, based on the logged in user's data. Something like:

def show
  product = Product.find(params[:id])
  render json: {
    discounted_price: CalculateDiscount.for(product)
  }
end

What I realized too late was that the product ID passed could trigger either an IndividualProduct or a BundleProduct to instantiate (due to single table inheritance behind the scenes), and a method called within the CalculateDiscount class was only available on BundleProduct instances. Together this caused the endpoint to blow up sporadically.

Lesson

When working with class hierarchies that diverge in method availabilities, ask yourself whenever you call any particular method whether it is available on all possible subclasses. If the methods aren't, then do some branching or create safe no-ops etc.

Relatedly, test all possible subclasses in your unit/manual tests to ensure there are no bugs with certain types.