In Progress
Unit 1, Lesson 21
In Progress

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_fathersday and #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_parentsday. The #date_for_mothersday and #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!

Responses