It's common in Ruby to see some code setup a Struct
like this:
class Specialized < Struct.new(:whatever)
# ... define custom methods here...
end
Struct
supports this kind of modification using a block though, so the above could be written as:
Specialized = Struct.new(:whatever) do
# ... define custom methods here...
end
Those are the possibilities.
There's no right or wrong way to handle this. However, I do prefer the second form. I'm going to try to make a case for why that is.
I have two main reasons for favoring the second form.
First, I find the second style conceptually easier to clarify. To me, the first form is kind of heavyweight, concept-wise. You need to understand that Struct.new()
returns a Class
and that the parent class in a class definition can be an arbitrary Ruby expression.
I think it gets even tougher to understand when you eventually run into the problem of combining this style with Ruby's open classes or some autoloading mechanism. When someone comes to me and asks, "Why am I getting a parent class mismatch error?" Explaining that involves more twists. Well, you see, you didn't just customize a Struct
. You built a Struct
and then defined a subclass of that. Because that Struct
is dynamically constructed, you get a different parent class anytime Ruby evaluates that expression, and Ruby doesn't allow the same subclass to reference different parent classes. Yuck. I don't even like describing this complexity and it's sort of reflected in the code by the anonymous Class
:
=> [MyStruct,
#<Class:0x007f8ba7389d18>,
Struct,
Enumerable,
Object,
PP::ObjectMixin,
Kernel,
BasicObject]
In contrast, I feel like describing the second form is about as simple as "… and the constructor can take a block for customizing what is created, say by defining new methods in it."
The second thing I prefer about the block form is a concept I've become aware of thanks to Josh Susser: code malleability. Consider that you begin with a trivial Struct
:
Trivial = Struct.new(:whatever)
Now, if I later need to customize it, I can either just tack a block onto the end of the call or begin restructuring the statement. Then what if I decide to back out the customizations? Again, I can restructure or just remove the block.
In other words, the block form flows naturally into and out of the normal Struct
usage. It's more malleable. This means refactoring is easier and you are more likely do it when needed.
There's another kind of related point here. Normal Struct
usage has a similar form to block Struct
usage. This visual similarity gives me the hint, "Oh, this is just a Struct
that I am dealing with."
Again, these are just my opinions.
I like structs as a cheap way of creating objects that I might replace with a custom implementation later (ie, use it both without subclassing and without the block). It's better than passing primitives around.
Also, given that you're worried about the superclass mismatch, shouldn't it be
Something ||= Struct.new(...) { }
to avoid surviving instances to have a different class?