Wednesday, August 22, 2007

Making Constructors Protected in Ruby

I'm working on Refactoring: Ruby Edition with Jay Fields and Martin Fowler. One of the Java examples requires making the constructor protected, which is easy enough to do in Java. I'd never done it before in Ruby, so I thought I'd give it a shot. Lets say I have this scenario:



I don't want anyone constructing a Party object on its own, so I want to make the constructor protected. I basically want these tests to pass:
def test_new_raises_no_method_error
assert_raise(NoMethodError) { Party.new("some name") }
end

def test_department_initialize
d = Department.new("name")
assert_equal "name", d.name
end

def test_employee_initialize
e = Employee.new("name", 5, 12)
assert_equal "name", e.name
end

I first tried this:
class Party
attr_reader :name
def initialize(name)
@name = name
end

protected :initialize
end

But the initialize method can happily be called when constructing Party directly, so the first test fails. I then tried this:
class Party
attr_reader :name
def initialize(name)
@name = name
end

class << self
protected :new
end
end

class Employee < Party
def initialize(name, id, annual_cost)
super(name)
@id, @annual_cost = id, annual_cost
end
end

class Department < Party
def initialize(name)
super
end
end

which does prevent me from constructing Party directly. Unfortunately, I can't construct the concrete classes without getting a NoMethodError because I'm calling the protected method :new, so this isn't much good to me. But then Andy Kotlinski showed me that if I implement :new on both of the concrete classes to simply call super:
class Employee < Party
def initialize(name, id, annual_cost)
super(name)
@id, @annual_cost = id, annual_cost
end

def self.new(name, id, annual_cost); super; end
end

class Department < Party
def initialize(name)
super
end

def self.new(name); super; end
end

And now all of the tests pass, and my superclass constructor is protected.

Now, if you don't want to jump through all of those hoops, you can include a module like this:
module MakeConstructorProtected
def self.included(klass)
klass.module_eval do
class << self
protected :new

def inherited(klass)
klass.module_eval do
def self.new(*args); super; end
end
end
end
end
end
end


class Party
include MakeConstructorProtected
attr_reader :name
def initialize(name)
@name = name
end
end

And you don't need to modify any of your subclasses. Because the inclusion of the module documents the intention far better than the obscure code, I quite like this solution. It still feels a little un-ruby-like, but it does document that Party wasn't written with the intention of being constructed on its own, and I quite like that. If anyone has an easier way to do this, then I'd love to hear it.

4 comments:

Joshua Graham said...

http://grahamis.com/blog/2007/08/23/ruby-protected-constructors/

Daniel said...

I would rather see a method added to Class than the module approach.

Patrick Farley said...

I agree, Shane, an abstract base class is not a very Ruby thing to be doing. But if you have to, I would prob skip all the protected madness and go right for:

class Party
attr :name
def initialize name
@name = name
raise NoMethodError.new("Party is an abstract class and cannot be instantiated") if self.class == Party
end
end


just my two cents. Might use a different Error subclass.

Bodaniel said...

http://github.com/bjeanes/make_abstract

I used your example to create a simple way to make abstract classes. Just:

require 'make_abstract'

then to make any class abstract (and leave it's child classes untouched):

class ImAbstractNow
make_abstract
end