Want to cut down on the mental friction of good object design? Use this organic approach to sprouting new classes in Ruby!
Video transcript & code
Let's start at the end, shall we?
Here's an ActiveRecord-based
Course class that has a
final_exam attribute. (You might recognize this from episode 569 with Corey Haines).
The final exam might be a list of questions, a multiple-choice question, or an essay. Because the type of the question varies, this field is stored as a
jsonb column in the database. This code uses ActiveRecord's "polymorphic attribute" support.
ActiveRecord will use the
FinalExamType object to load and store one of several different question types from and to this column.
We can see how that works if we go to the
FinalExamType class, where a
case statement instantiates different question types based on what is found in the JSONB data.
What about these question types? Each of these has its own file as well.
There's a question set type,
a multiple-choice type,
and an essay type.
This is an example of good separation of concerns. A constellation of small classes, each responsible for one thing. But today I don't want to talk about Rails polymorphic attributes, or about object design. I want to talk about how we get to this point.
Sometimes I'll be working with a pair-programming client, and I'll suggest breaking some logic out into its own class. And immediately they'll be like:
"OK, I guess we need a new file..."
"Hmmm... where should we put it??"
"Do we need a new directory...?"
This digression into file housekeeping is a form of friction. It's a discontinuity from our coding flow.
And then, once the new file exists, we find ourselves constantly flipping back and forth between the originating file and the newly-created one. This leads to little breaks in context. Sometimes my pair or I will blank out, not remembering which file we need to be in to make the next change.
Things get even worse once we start flipping through three or four sprouted classes.
Put all this friction together, and what happens? Factoring out a new class starts to feel subliminally heavyweight. It feels like less effort to just keep adding code and methods to a single class.
But there's another approach available.
And that's to simply introduce new classes within the current class.
There's nothing terribly novel or esoteric about this technique. But it's one of those things where, in my experience, even Ruby programmers who are aware classes can be nested just... don't make much use of this capability. In some cases it's because a developer isn't certain of the semantics of nested classes. In other cases I think they may be accustomed to a Rails-style one-file-per-class breakdown and feel like nesting classes "simply isn't done" in Ruby-land.
What I like about sprouting new classes inside the class where I want to use them is that it completely eliminates that "new class friction". There's no pause to figure out where we want to put the class, and to create a file. We just make the class we want, right where we are.
And because human brains are optimized to remember spatial relationships, it takes less mental effort to flip back and forth rapidly between nested classes that we're rapidly iterating in parallel.
If you wonder about the semantics of nested classes in Ruby: Ruby isn't like Java, where an inner class has special access to its containing class. Nested classes are no different from any other class. They are completely independent.
Their constant is simply nested inside containing-class' constant scope, just as if they were inside a module.
This means that when referenced within their containing class, their names don't need to be prefixed.
One thing to bear in mind when using nested classes: If you have a class-level reference to them, as we have here in this attribute declaration,
you do need to define the class above where the first class-level reference appears.
Mind you, I'm not saying these inner classes should stay nested forever. Nested classes are a stepping-stone. Once the period of rapid iteration is over and the classes have stabilized, then it's a good time to extract them out into their own files.
But when you first get the idea to introduce a new class, don't hobble your flow by creating a new file before you're even sure what you want to call it. First, sprout it inline as a nested class. You'll avoid friction, and you'll still have a nice clean seam for extraction once things have settled down.