How to preserve idioms in Ruby subclassing

by Jim

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.

Clean Up Your Code

If you liked this post and want more like it, get periodic tips in your inbox by leaving your email address below.

To get more information about cleaning up your objects, classes, and code, then check out Clean Ruby, an ebook which will describe ways to keep your code clean, maintainable, and focused on business value. Make your OOP more obvious, easier to understand, easier to test, and easier to maintain.



Comments

Fran├žois Beausoleil said on Tuesday, April 17, 2012:

You're reimplementing super, but in a completely non-idiomatic way. For hierarchies one deep, fine, it'll work. As soon as you subclass a subclass, you'll start tearing your hair out, because your subclass will implement the same idiom, and you'll repeat if you're subclassing another level.

It probably happened to me to forget to call super, but aren't thoses tests supposed to catch those cases?

Jim Gay said on Tuesday, April 17, 2012:

It's sort of reimplementing super, but removes the requirement of remembering in what order to call it.

The default_initialize method could do other things. Rather than parse arguments it could assign variables to point to related objects in the environment during initialization.

Find more in the archives

1999 - 2014 © Saturn Flyer LLC 2321 S. Buchanan St. Arlington, VA 22206

Call Jim Gay at 571 403 0338