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# versionThe 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 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
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 NavigatorIs this actually better? I'm not sure, but I'd be willing to try it out.
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
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# versionThe 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.
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
PhoneNumber = Struct.new :area_code, :exchange, :stationLess readable? Perhaps, but is it if you know what behavior Struct.new encapsulates?
class PhoneNumber
def formatted_number
"(#{area_code}) #{exchange} #{station}"
end
end
Here's another example in the context of a test.
# R# versionWhile 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.
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
An alternative solution allows you to use an unnamed class within the test that needs it.
class ValidatableTest < Test::Unit::TestCaseThe 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.
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
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.
> Less readable? Perhaps, but is it if you know what behavior Struct.new encapsulates?
ReplyDeleteNo. Especially, since you get value-based equality checking and hash codes out of that same line.
I've always liked Ruby's open class feature but have also wondered how do you keep track of all the changes made to the class definition during runtime? At any given point of execution, how can you be sure that a particular class has a certain method that works a certain way or not? For example, your "test" method of TestCase... I thought it was an in-built thing until I found a link that said you'd added it yourself.
ReplyDeleteI guess an explanation of this will take a complete blog entry of its own. :-) Perhaps you could point me to a mailing list discussion or something. Thanks.