The other day I was writing some code in which I needed to know if a specific class was a descendant of another class.
I've done a similar thing with Ruby objects in the past, but never with classes, and I think I'll start from there.
If we want to know if a Ruby object's class is a descendant of the Vehicle
class, we can
simply ask it if it is a Vehicle
Vehicle = Class.new
Animal = Class.new
Car = Class.new(Vehicle)
Dog = Class.new(Animal)
Car.new.is_a?(Vehicle) # => true
Dog.new.is_a?(Vehicle) # => false
And we can even go one level down in the lineage an it'll still answer as espected
Sedan = Class.new(Car)
Sedan.new.is_a?(Car) # => true
But when asking the same thing to a class, the story is different. And for that, I want to show you my specific use case.
I had a class called Field
that represented a field in a form. A form could
have many fields and so on.
This was part of a Form Objects library, and in that library I wanted a small DSL that allowed me to create a field and assign a type (it has more than than, but I'll simplify)
class MyForm < Form
field :name, String
# ^ ^
# | Field type
# Field name
end
This method created 2 new instance methods on the form: a getter and a setter. And that setter needed to know the type we needed and set the apropriate value.
If the field type was Field
or any descendant, the value is unchanged, but if
it's something else, we needed to wrap it in a Field
(getting a uniform level
of abstraction).
class Form
def self.field(field_name, field_type)
define_method field_name do
instance_variable_get "@#{field_name}"
end
define_method "#{field_name}=" do |raw_value|
value = if <type_is_field>
raw_value
else
Field.for(raw_value)
end
instance_variable_set "@#{field_name}", value
end
end
end
Let's center our focus in that placeholder.
My first approach was to use is_a?
out of habit. But as it turns out always
returned false:
Field = Class.new
String.is_a? Field # => false
Field.is_a? Field # => false
And that's because I'm always passing a class in.
Now, seeing it as in the last example seams dumb on my part. But on my defense, I was seeing something like this:
field_type.is_a? Field # => false
Because I was asking it to a variable passed in (and also, this particular case came in after several days of developing the library).
So I had to investigate how to do it. The first thing I did was just asking the
class if it's ancestors included the Field
class.
field_type.ancestors.include?(Field)
And this got my specs passing. But after looking at it for some time, I started finding that approach more and more annoying. It was very verbose and un-Ruby-like to me, so I went where every developer goes in these cases… Stackoverflow.
I found out that the Class
class in Ruby defines the <
operator to ask
exactly what I wanted to ask: Are you a descendant of Field
?
Field = Class.new
MyField = Class.new(Field)
MyField < Field # => true
And it's not just limited to that, it defines all the comparison operators, so we can ask for ancestors or descendants with this idiom, which is absolutely awesome to me
MyField < Field # => true
Field < Field # => false
MyField <= Field # => true
Field <= Field # => true
Field > MyField # => true
Field > Field # => false
Field >= MyField # => true
Field >= Field # => true
So next time you need to know about the lineage of a class, you have an excellent short-hand to do so. Thanks Ruby!