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.