Sometimes, the only thing standing between some confusing code and a better design is a better choice of names.
In this episode, you can watch a story about one of those situations. You’ll see how a pair-programming conversation led to finding a missing domain term, which clarified a problem and opened the way to further code improvements.
When I work with other programmers, some of the most fruitful discussions we have are the ones that center around naming things. I've had many pairing sessions in which we've struggled with some difficult design questions. Then we'll identify and name a concept which had formerly been lying implicit in the code. Suddenly, obvious answers to our questions will seem to naturally fall out as a consequence of identifying the concept.
A lot of people have asked me how to get better at naming things. Unfortunately, there are no simple rules when it comes to naming. It's something you learn by experience, by reading other people's code, and through discussion with other developers.
This has left me in a bit of a bind: I'd like to cover naming in these videos; but I don't know of any generally applicable rules when it comes to giving names to things. So instead, I've decided to collect examples of specific naming decisions "in the wild", and present them along with some of the reasoning that went into them. Hopefully you'll find some design inspiration in following the thought processes that resulted in these real-world naming choices.
Today's example is inspired by a pairing session I did recently. Consider a program that has to do with live events. Events have attendees, who are identified by their email address.
This program also has the concept of an "attendance". An attendance links an event to an attendee, but also contains a plus_one
flag. This flag indicates whether the attendee will be bringing another person to the event.
Each event has a name, a venue capacity, and a list of attendances. It also has a couple of methods.
For various reasons we want to be able to get a count of attendees going to the event. So the class has a method called attendees
which goes through the attendances and tallies them up. In order to get an accurate count, it has to take into account whether a given attendance is flagged as "plus one" or not.
We also need to be able to add a new attendance to an event. The method that handles this has some special logic. It figures out how many people the new attendance will add to the event, again using the plus_one
flag on attendances. Then, if that would put the event over capacity, it rejects the new attendance. Otherwise, it adds the new attendance to the list.
Attendee = Struct.new(:email)
Attendance = Struct.new(:event, :attendee, :plus_one)
Event = Struct.new(:name, :capacity, :attendances) do
def initialize(*)
super
self.attendances ||= []
end
def attendees
attendances.reduce(0) {|count, attendance|
if attendance.plus_one
count + 2
else
count + 1
end
}
end
def add_attendance(attendance)
new_attendees = if attendance.plus_one
2
else
1
end
if attendees + new_attendees > capacity
raise "Sorry, this event is full!"
else
attendances << attendance
end
end
end
We can identify a couple of problems with this code. For one thing, there is a method named #attendees
on Event
. A newcomer to the code might reasonably expect this method to return a list of Attendee
objects, but in fact it returns an integer count. This is potentially surprising behavior.
Another issue is that the if/else conditional based on the plus_one
flag is repeated in both the attendees
and #add_attendance
methods. Not only is this duplication, it looks like feature envy: a code smell wherein a class seems more interested in another object's methods than it is in its own.
During the pairing session that inspired this code, we quickly zeroed in on the problematically-named #attendees
method. We tossed some alternate terms around for what the concept this method represents. We tried terms like "attendee count" and "people".
Finally, one of us said "head count", and everything suddenly clicked. "Head count" is familiar, recognizable terminology for the absolute number of people at an event. In other words, it is domain language. And it is clearly distinguished from the concept of an "attendance".
So we quickly changed #attendees
to #head_count
. This did away with the ambiguity in that method's name.
Attendee = Struct.new(:email)
Attendance = Struct.new(:event, :attendee, :plus_one)
Event = Struct.new(:name, :capacity, :attendances) do
def initialize(*)
super
self.attendances ||= []
end
def head_count
attendances.reduce(0) {|count, attendance|
if attendance.plus_one
count + 2
else
count + 1
end
}
end
def add_attendance(attendance)
new_attendees = if attendance.plus_one
2
else
1
end
if head_count + new_attendees > capacity
raise "Sorry, this event is full!"
else
attendances << attendance
end
end
end
Then, we realized that if we were talking about head counts, it would make sense for an individual attendance object to know its own number of "heads". We created a new method called #heads
on the Attendance
class. This method used the same conditional logic we used in the Event
class to return either 2
or 1
depending on the state of the plus_one
flag.
Attendance = Struct.new(:event, :attendee, :plus_one) do
def heads
if plus_one
2
else
1
end
end
end
While we were updating this class, we also decided to deal with another naming nitpick. Since plus_one
was a boolean flag, we decided it would be more idiomatic Ruby for the code to use a method with a question mark on the end to access it. So we aliased the plus_one
accessor to a question mark version, and updated our #heads
code accordingly.
Attendance = Struct.new(:event, :attendee, :plus_one) do
alias_method :plus_one?, :plus_one
def heads
if plus_one?
2
else
1
end
end
end
Once we added the #heads
method to Attendance
, we were able to go back to the Event
class and massively simplify it. Calculating #head_count
became a simple matter of summing up the heads across all attendances. And an extra conditional could be completely removed from #add_attendance
.
Event = Struct.new(:name, :capacity, :attendances) do
def initialize(*)
super
self.attendances ||= []
end
def head_count
attendances.reduce(0) {|count, attendance|
count + attendance.heads
}
end
def add_attendance(attendance)
if head_count + attendance.heads > capacity
raise "Sorry, this event is full!"
else
attendances << attendance
end
end
end
In my experience, it's often the case that as soon as we identify an as-yet unnamed concept that's implicit in the code, multiple beneficial refactorings seem to cascade out naturally from that change. This was certainly the case with the attendees
to #head_count
renaming.
I hope this little example has gotten the naming gears turning in your brain. If you find examples like these helpful let me know, and I'll keep on documenting them as I run across them. And if you have any examples of your own—either cases where a rename clarified the code, or examples of code where you feel like the right name has yet to emerge—feel free to send them my way!
That's it for now. Happy hacking!
Responses