Apr 22, 2022

A Better Definition of the DRY Programming Principle

A Better Definition of the DRY Programming Principle

One of the most frequently disregarded and misunderstood coding principle by junior developers is the DRY principle which stands for Don’t Repeat Yourself.

Often seen as the direct opposite of WET — which stands for Write Everything Twice — the DRY principle is actually not completely about avoiding code duplication at all cost by using complex abstractions but mostly about avoiding knowledge duplication.

Even though it is commonly admitted that copy-pasting the same piece of code throughout an entire application is the best way to foster inconsistencies and mistakes, solely focusing on this aspect is actually a mistake, as premature optimization attempts usually result in an increased complexity and code coupling.

A better DRY definition

That being said, a better definition for DRY would be the following one extracted from the book “The pragmatic programmer” that states that:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Where a piece of knowledge is either a specific logic in a business domain or an algorithm.

Let’s now illustrate this with three examples that will give you a better idea of when refactoring should and shouldn’t be done according to context, starting with constants.

Example #1: Constants

Refactoring constants is probably the first thing that comes to mind when trying to apply the DRY principle.

Let’s say we have the three following React components that are used to display text and that all use the same dark gray colour code.

/*
 * File: text.js
 */

function Title(props) {
  return (
    <h1 style={{color: "#3A3B3C"}}>
      {props.title}
    </h1>
  );
}

function Subtitle(props) {
  return (
    <h2 style={{color: "#3A3B3C"}}>
      {props.title}
    </h2>
  );
}

function Paragraph(props) {
  return (
    <p style={{color: "#3A3B3C"}}>
      {props.text}
    </p>
  );
}

In that context, if you were to decide on a different shade of gray tomorrow, you would have to manually replace every single occurrence of that colour code, making sure not to forget any, which indeed requires a lot more focus and work than it should.

The right approach would then be to create a new constants files named colours for instance, that will contain the value of that colour shade and that will represent the unique source of truth throughout your whole application.

/*
 * File: colours.js
 */

export default {
  DARK_GRAY: '#3A3B3C'
};

So that next time you want to update it, there’s only one quick change to apply.

/*
 * File: text.js
 */

import { DARK_GRAY } from '../constants/colours';

function Title(props) {
  return (
    <h1 style={{color: DARK_GRAY}}>
      {props.title}
    </h1>
  );
}

function Subtitle(props) {
  return (
    <h2 style={{color: DARK_GRAY}}>
      {props.title}
    </h2>
  );
}

function Paragraph(props) {
  return (
    <p style={{color: DARK_GRAY}}>
      {props.text}
    </p>
  );
}

Example #2: Same domain logic

Let’s now consider the two following functions editPost and deletePost which are respectively used to edit and delete a post on a blogging platform like Medium for instance.

function editPost(userId, postId) {
  const post = database.fetch(postId);
  if (post && post.ownerId === userId) {
    // Edit post
  } else {
    // Throw error
  }
}

function deletePost(userId, postId) {
  const post = database.fetch(postId);
  
  if (post && post.ownerId === userId) {
    // Delete post
  } else {
    // Throw error
  }
}

As you can see here, the same verification logic used to check wether a user is allowed or not to perform these actions is duplicated in both functions which causes two problems.

The first one is that if the logic changes overtime it will have to be duplicated again which may potentially introduce bugs if not done carefully.

And the second one is that, instead of solely doing what they are supposed to — which is updating and deleting — these functionalities have been given the responsibility of handling business logic outside of their scope.

The appropriate thing to do, would therefore be to extract this logic into a separate component, which will in turn free these functions from both these problems at once.

function isUserPermitted(userId, postId) {
  const post = database.fetch(postId);
  
  return post && post.ownerId === userId;
}

function editPost(userId, postId, data) {
  if (!isUserPermitted(userId, postId)) {
    // Throw error
  }
  // Edit post
}

function deletePost(userId, postId) {
  if (!isUserPermitted(userId, postId)) {
    // Throw error
  }
  // Delete post
}

Example #3: Different domain logic

Finally, let’s assume that we are running an e-commerce platform that implements the following two functions, getAmountWithVAT which calculates the amount a customer will have to pay for an order including VAT and getLatePaymentPenalties which calculates the penalty fee that will be applied on the bill if the customer doesn't pay on time.

function getAmountWithVAT(amount) {
  const VAT = 0.20;
  return amount + (amount * VAT);
}

function getLatePaymentPenalties(amount) {
  const penalty = 0.20;
  return amount + (amount * penalty);
}

As both functions appear to share the same logic, you could be tempted to actually refactor the code by merging them into a single function called addPercentage for example, but in practice, it turns out to be a really bad idea since these two functions don't actually share the same business domain.

function addPercentage(amount, percentage) {
  return amount + (amount * percentage);
}

Indeed, if tomorrow you decided not to only apply a fixed fee for late payment but a progressive one that would increase by 10% each week, you would have to split this addPercentage function back again into two different ones, in order to be able to implement your new logic and to get rid of the coupling you've previously created.

So make sure that the pieces of code you choose to refactor are actually sharing the same business domain and try to keep in mind that sometimes what appears to be a duplication of knowledge is just a pure coincidence.

Conclusion

To conclude, in order to avoid falling into the trap of creating unnecessary or wrong abstractions that might end up braking another crucial programming principle called YAGNI — which stands for You Ain’t Going to Need It — try to remember that code duplication doesn’t automatically imply a DRY principle violation but that knowledge duplication does.

Related posts