How to preserve idioms in Ruby subclassing

Ever wonder why you call new on a class but define initialize? Let's take a look.

Leaning on inheritance is a great way to solve related problems in our applications while allowing for some variation in behavior. Inheritance is simple and easy to implement.

Here's a quick example of where developers often stumble by writing classes that require too much knowledge about their subclasses.

Let's make a simple class that we'll use to say hello:

class Greeter
  def initialize(args)
    @name = args.fetch(:name){ 'nobody' }
  end

  def greet
    "I'm #{@name}."
  end
end

Here we have a Greeter class and when you initialize an object from it you'll be able to tell it to greet with an introduction:

greeter = Greeter.new(:name => 'Jim')
greeter.greet
=> "I'm Jim."

We later find that in some cases we need a variation of this with something more formal. So let's make a class to support that:

class FormalGreeter < Greeter
  def greet
    "I am your servant, #{@name}."
  end
end

With this FormalGreeter class we have a different introduction:

greeter = FormalGreeter.new(:name => 'Jim')
greeter.greet
=> "I am your servant, Jim."

Adding Default Behavior During Initialization

Later you decide that your FormalGreeter really ought to do something special during the greeting.

greeter = FormalGreeter.new(:name => 'Amy', :movement => 'curtsy')
greeter.greet
=> "I am your servant, Amy."
greeter.action
=> "curtsy"

Here our FormalGreeter is the class that will move during the greeting. So we can just define initialize:

class FormalGreeter < Greeter
  def initialize(args)
    @movement = args.fetch(:movement){ 'bow' }
    super
  end

  def action
    @movement.to_s
  end
end

All you have to do for everything else to work is to remember to call super.

But having to remember things can be a stumbling block in your code. Humans forget things.

One way to do this would be to setup a special initialize method to be called in the base class. There you could define your own behavior in the subclasses:

class Greeter
  def initialize(args)
    @name = args.fetch(:name){ 'nobody' }
    special_initialize(args)
  end

  def special_initialize(args)
    # hook for subclass
  end
end

This is better because we don't need to remember in what order we should run our code. Does my code go first and then super? Or do I call super first and then do what I need? Now, we don't need to mentally recall the procedure.

This simplifies our initialization process, but we still have the need to remember that instead of calling super within our initialize method, we need to define our special_initialize method.

Humans forget things.

This should still be easier.

Instead of needing to remember things, you can hook into your initialization in a way that requires less thinking during the development of your subclasses. Fallback to standards like defining initialize.

Here's how you protect idiomatic code:

class Greeter
  def self.new(args)
    instance = allocate # make memory space for a new object
    instance.send(:default_initialize, args)
    instance.send(:initialize, args)
    instance
  end

  def default_initialize(args)
    @name = args.fetch(:name){ 'nobody' }
  end
  private :default_initialize

  def initialize(args)
    # idiomatic hook!
  end
end

Now our subclasses have the benefit of being able to use their own initialize without the need for us to remember super nor a custom method name.

class FormalGreeter < Greeter
  def initialize(args)
    @movement = args.fetch(:movement){ 'bow' }
  end

  def action
    @movement
  end
end

Now you have some insight into the way that Ruby uses your initialize method.

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.