Do Right Digital

Defining Duplication: Digging into DRY

The DRY principle (Don't Repeat Yourself) is an important one for developers but recently I've been recognising repetition where previously I may have overlooked it. I have found that the DRY principle is most powerful when paired with SRP (Single Responsibility Principle). For an application to be free from repetition while maintaining a single responsibility for each unit hints that it is indeed well written.

While maintaining various code bases I have been in discussions about the nature of duplication, I have been surprised to see how much varied opinion there is around what is and is not duplication. These are some classifications of duplication that I see:

Pure Duplication

This is the simplest to create and often causes the most problems if left to rot.  So you come across code like this: function toggleAndCallback(optionalCallback) { if (this.isOpen) { this.close(); if (optionalCallback) { optionalCallback('closed', new Date.getTime()) } } else { this.open(); if (optionalCallback) { optionalCallback('open', new Date.getTime()) } } }

In this block someone (in this case me) clearly copied and pasted that logic and made one small change. To fix this we can extract a function and simply turn it into: function toggleAndCallback(optionalCallback) { function useCallback(openOrClose) { if (optionalCallback) { optionalCallback(openOrClose, new Date.getTime()) } } if (this.isOpen) { this.close(); useCallback('close') } else { this.open(); useCallback('open') } }

This way we're configuring the one item we need to configure and we've removed some repetition.

Conceptual Duplication

Sometimes there are concepts that are particular to one area of expertise - to repeat these concepts throughout the codebase is another form of repetition. This takes a few different forms:

Formula Duplication

Let's say you're working on an application which uses basic circle math and your code looks like this:

function getCurcumfrances(config) {
  return {
    min: config.minRadius * 2 * Math.PI;
    max: config.maxRadius * 2 * Math.PI;
    actual: config.actualRadius * 2 * Math.PI;
}

Here we're repeating the concept that the circumference of a circle is 2 * PI * R. We can clean that up by using a helper function - in this case I'm creating it on Math so it exists alongside PI:

Math.prototype.circumfranceFromRadius = function (radius) {
  return radius * 2 * Math.PI;
}
function generateReport(config) {
  return {
    min: Math.circumfranceFromRadius(config.minRadius);
    max: Math.circumfranceFromRadius(config.maxRadius);
    actual: Math.circumfranceFromRadius(config.actualRadius);
}

Which leads us nicely onto the next type of concept repetition

Knowledge Duplication

Usually an outsider is better placed to spot this sort of repetition but once you're familiar with it you can spot it yourself. For me it came when I was showing a Java Dev some jQuery code and he questioned what it meant when I kept using:

if ($('.something').length > 0) { ... }

I explained it to him and we realised that this jQuery idiom wasn't easy to read and lead to a repeated concept throughout the codebase. We fixed that by using the following extraction:

$.fn.exists = function () { return $(this).length > 0; }

When we started using that our codebase became a lot more readable:

if ($('.something').exists()) { ... }

Fixing that repetition is beneficial to both experts and novices in that area of expertise. This takes away the expectation that everyone maintaining the codebase will have the same knowledge as the writer.

Requirement Duplication

If we're working on an application which shows notifications for 20 mins before hiding them we can end up with code like this:

if (message.timestamp < (new Date().getTime() - 20 * 60 * 1000)) {
  message.remove();
}

Meanwhile elsewhere we're looking up the messages in a database:

SELECT message, timestamp FROM MESSAGES WHERE timestamp < (now() - 20 * 60)

Here we're crossing between different languages and different measures of time (milliseconds and seconds) but the 20 minute requirement is repeated. Now, this may come as a surprise, but occasionally project managers and business owners change their minds. When they decide 20 minutes is too long and 5 minutes would be better you've got lots of individual changes to put it down and bugs can creep in due to the ones you miss.

Responsibility Duplication

This is the crossroads between DRY and Single Responsibility Principle. At it's most basic this form of duplication is really simple but it's among the most deceptive forms of repetition. Let's start with some code containing repetition:

if(window.width > 1000) {
  switchToWideMode();
} else {
  switchToNarrowMode();
}
var caltonSizeInLitres = milkCalton.voulumeInML / 1000;
var engineSizeInLitres = car.engineSizeInML / 1000;

Now we've got 3 hard-coded references to 1000, we could respond that by creating a new variable to avoid repeating 1000:

var thousand = 1000;
if (window.width > thousand) { ... }
var caltonSizeInLitres = milkCalton.voulumeInML / thousand;
var engineSizeInLitres = car.engineSizeInML / thousand;</pre>

That might keep automated code quality tools happy but it's clearly not the right thing. For example if the size of the viewport breakpoint changes the conversion to Litres will also be updated - that's obviously wrong. So to do this properly we're going to need 2 variables:

var mililitresinALitre = 1000;
var wideWindowBreakpoint = 1000;
if (window.width > wideWindowBreakpoint) { ... }
var caltonSizeInLitres = milkCalton.voulumeInML / mililitresinALitre;
var engineSizeInLitres = car.engineSizeInML / mililitresinALitre;</pre>

Now each variable has one meaning and we can change the width of the breakpoint without breaking our volume conversions. Now that we've seen a pure, simple, contrived example let's have a look at a much more realistic example:

APP.config.url = {
  homepage: '/',
  login: '/login.html',
  productListing: '/products.html',
  userAccount: '/my-stuff/profile.html'
};

Now we've got various parts of the application which redirect us to different places, if we're logged in we go to the user account otherwise we go to the homepage. As the application grows we have more and more occurrences of this but it's not until the requirement changes that the problem really shows itself. Now they want the logged in users to be redirected to the product list and we've got to change code in lots of places - this is a symptom of both Requirement Repetition and Responsibility Repetition. We can fix this by introducing a new config item:

APP.config.url = {
  homepage: '/',
  login: '/login.html',
  productListing: '/products.html',
  userAccount: '/my-stuff/profile.html',
  defaultLoggedInPage: '/products.html'
};

Now we have a way of identifying the product listing page and a way of identifying the default logged in page. We've solved our problem but we've introduced new duplication. Now it's time to fix that bit of duplication:

APP.config.url = {
  homepage: '/',
  login: '/login.html',
  productListing: '/products.html',
  userAccount: '/my-stuff/profile.html'
};
APP.config.url.defaultLoggedInPage = APP.config.url.productListing;
APP.config.url.defaultLoggedoutPage = APP.config.url.homepage;</pre>

Now we've got a lot of repetition from using the full namespace, we can fix that with a closure:

APP.config.url = (function () {
  var config = {
    homepage: '/',
    login: '/login.html',
    productListing: '/products.html',
    userAccount: '/my-stuff/profile.html'
  };
  config.defaultLoggedInPage = config.productListing;
  config.defaultLoggedoutPage = config.homepage;
  return config;
}());

Having cleaned up the repetition we've got one single responsibility for each configuration item. On top of this we've also got one single authoritative source for each concept in the configuration.

Should we go further?

Yes. These are just a few examples and if I were facing these problems in a real codebase I'd be looking to remove more duplication than I have here. Don't consider these examples perfect finished code, they are just to show the concepts of each type of Duplication.

Enjoy discovering DRY - it's an artform worth perfecting.

Tags: