Back to Course

Spot the Tax · Card 8 of 20

A subclass that breaks the parent's promises isn't really a subclass

Why GuestUser inheriting from User causes random NoMethodErrors throughout the app.

The code

What will this cost you in six months?

class User < ApplicationRecord
  validates :email, presence: true
end

class GuestUser < User
  def save
    raise "Guests cannot be saved"
  end

  def email
    nil
  end
end

# In a service that takes any User:
def send_welcome(user)
  WelcomeMailer.welcome(user.email.downcase).deliver_later
  user.save
end

The problem

GuestUser inherits from User, so as far as the type system (or anyone reading the code) is concerned, you can pass a GuestUser anywhere a User is expected. The trouble is that it doesn't actually behave like one. Its email returns nil instead of a string, and its save raises instead of saving. So a method like send_welcome, which assumes the user it's been given has a real email and can be saved, blows up the moment a GuestUser flows through it — usually deep inside a service object that's painful to trace back to the source.

Take a moment. Before revealing, ask yourself: should GuestUser inherit from User at all? If not, what should it be instead?