Friday, January 26, 2007

Class Definitions

One of the things that makes Ruby very interesting to me is the ability to do things that I previously could not do in C#. For example, Ruby has open classes so I can define behavior of a class in various places.

In this post I'll need a term to describe code written in Ruby that could be written in the same way using Java or C#. Since I worked primarily with C# before Ruby I'll use R#.

I'll start with a fairly easy example where I have a class that has some class and instance methods and some of the class methods need to be protected.
# R# version
class Navigator
class << self
def standard_navigator
self.new standard_path
end

def promo_navigator
self.new promotion_path
end

protected
def standard_path
[:index, :account_creation, :terms_of_service, :check_out]
end

def promotion_path
[:promo_landing, :account_creation, :check_out]
end
end

def initialize(path)
@path = path
@current = 0
end

def next
@current += 1
@path[@current]
end
end
The above example is a fairly standard implementation that puts the class methods at the top, public above the protected, and then the instance methods below. A possible problem with the above code is that if you don't have the class << self code visible when viewing the file (because you've scrolled down) it is not immediately obvious that the methods are class methods. You could argue that the using the class << self is the problem; however, it's the only clean way I've found that allows you to define protected class methods.

An alternative way to define the class can be found below.
class Navigator
def initialize(path)
@path = path
@current = 0
end

def next
@current += 1
@path[@current]
end
end

class << Navigator
def standard_navigator
self.new standard_path
end

def promo_navigator
self.new promotion_path
end

protected
def standard_path
[:index, :account_creation, :terms_of_service, :check_out]
end

def promotion_path
[:promo_landing, :account_creation, :check_out]
end
end
Is this actually better? I'm not sure, but I'd be willing to try it out.

Here's another example. I want a class that has three attributes (properties in C#), is able to initialize these properties in the constructor or default them, and contains some behavior.
# R# version
class PhoneNumber
attr_accessor :area_code, :exchange, :station

def initialize(area_code=nil, exchange=nil, station=nil)
@area_code, @exchange, @station = area_code, exchange, station
end

def formatted_number
"(#{area_code}) #{exchange} #{station}"
end
end
The above code is easy enough to read, but Ruby already has a class that encapsulates the attribute and constructor initialization pattern: Struct. The above code can also be written like below.
PhoneNumber = Struct.new :area_code, :exchange, :station

class PhoneNumber
def formatted_number
"(#{area_code}) #{exchange} #{station}"
end
end
Less readable? Perhaps, but is it if you know what behavior Struct.new encapsulates?

Here's another example in the context of a test.
# R# version
class ValidatableTest < Test::Unit::TestCase
class StubClass
include Validatable
attr_accessor :name
validates_presence_of :name
end

test "when a name is empty, then the instance is invalid" do
assert_equal false, StubClass.new.valid?
end
end
While the above code works, it begins to break down when you need multiple stub classes. You can use names such as StubClass2, StubClass3 or look for more descriptive names but when you are testing similar but slightly different situations it can be very hard to come up with good names. It is also less desirable to have the class defined outside the scope of the test that uses the class.

An alternative solution allows you to use an unnamed class within the test that needs it.
class ValidatableTest < Test::Unit::TestCase
test "when a name is empty, then the instance is invalid" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_presence_of :name
end
assert_equal false, klass.new.valid?
end
end
The above test is very explicit about what behavior can be expected from the class under test. The above test also ensures that if the test fails you wont need to go elsewhere to see any set up code. This example is the one where I have an opinion, and I much prefer the 2nd version.

Ruby allows you to add behavior to classes in many ways. Using a combination of the various ways we may be able to find more descriptive class definitions, or prove that the traditional ways are superior.
Post a Comment