Coincidental Duplication Redux
“DRYing up code is just a matter of comparing logic and extracting sections with duplication.”
Or is it?
In this episode we’ll revisit the concept of “coincidental duplication”. You’ll learn how to apply the Don’t Repeat Yourself principle when you have two independent pieces knowledge which are superficially similar.
Video transcript & code
The other day I did an episode on coincidental duplication. To be perfectly honest, I wasn't completely happy with the example I used. Then Katrina Owen sent me an example which I thought was such a perfect demonstration of coincidental duplication and over-DRYing code that I decided to revisit the topic.
Let's say we have some code for calculating the dates on which certain holidays fall in Norway. There are methods for calculating Mother's Day and Father's Day for a given year, as well as for determining when the next Mother or Father's day will occur. Finally, there are some helper methods that the other methods use.
You may note that the code for determining
#next_mothersday is substantially similar. This is the kind of repetitive pattern we usually look for when refactoring.
require 'date' # in Norway module Calendar module Holiday FEBRUARY = 2 NOVEMBER = 11 def mothersday(year) second_sunday_in(FEBRUARY, year) end def fathersday(year) second_sunday_in(NOVEMBER, year) end def next_mothersday(today = Date.today) this_years = mothersday(today.year) if this_years >= today this_years else mothersday(today.year + 1) end end def next_fathersday(today = Date.today) this_years = fathersday(today.year) if this_years >= today this_years else fathersday(today.year + 1) end end def second_sunday_in(month, year) # Day number 8 always falls within # the second week of the month second_week = Date.new(year, month, 8) second_week + days_until_sunday(second_week) end def days_until_sunday(date) (7 - date.wday) % 7 end end end
Now let's take a look at a refactored version of this code, in which the logic for calculating Father's Day and Mother's Day has been combined into a single method called
#date_for_fathersday methods are both implemented in terms of this combined method.
require 'active_support/all' # in Norway module Calendar module Holiday def date_for_mothersday(today = Time.now.utc) date_for_parentsday(2, today) end def date_for_fathersday(today = Time.now.utc) date_for_parentsday(11, today) end def date_for_parentsday(month, today) # Day number 8 will guarantee the second Sunday of the month close_date = Time.utc(today.year, month, 8) possible_parentsday = next_sunday_after(close_date) if possible_parentsday >= today.beginning_of_day actual_parentsday = possible_parentsday else actual_parentsday = next_sunday_after(close_date + 1.year) end return actual_parentsday end def next_sunday_after(time) return time + ((7 - time.wday) % 7) * 1.day end end end
Is this code better? Well, it's certainly shorter. But that's about all I can say for it. First off, nobody says "Parent's Day". It's not a recognizable domain concept, so the code is already suffering from reduced readability as a result of unfamiliar, over-genericized terminology.
I don't know about you, but just looking at this code, I found the old version more readable. And I think the fundamental reason is this: Mother's Day and Father's Day are two separate concepts, each with their own rules for when they fall in the calendar. The fact that the logic for applying those rules may be substantially similar is a coincidence: it does not reflect a shared concept that both Mother's Day and Father's Day depend on.
As such, I feel that the original code is more intention revealing. And because the duplication is coincidental, the original code is no less DRY.
The moral here is this: DRYing up code isn't just a matter of mechanically comparing logic and extracting any sections with duplication. The Don't Repeat Yourself principle is about having a single home for each discrete piece of knowledge. If you have two pieces of independent knowledge, they should have two different homes - even if those homes look superficially similar.
That's it for today. Happy hacking!