DEV Community

Cover image for The case for DRY code
Kinanee Samson
Kinanee Samson

Posted on

The case for DRY code

Good day guys, you must have stumbled across this funny acronym 'DRY' as a programmer maybe you stick to it or maybe you think it creates more problem than it solves? If you haven't then let me introduce it to you. The acronym stands for Do not Repeat Yourself. This is a software development principle that suggests that you should avoid unnecessary or unjustified repetition in your code. The idea behind this principle is that if we avoid repeating logic in our code, it will be easier to maintain and manage. I totally agree with this, if you are a beginner, you might wonder how does this makes sense? We will come to that later, l stumbled across an article on DEV some time ago that prompted me to write this article. The author suggested that our code should be WET (Write Everything Twice). They presented a good argument about why code should be as WET as possible, however i don't totally agree with that, WET code causes more harm than good because at the end of the day why should you do something twice or thrice when you can do it once ?

If you are guilty of repeating yourself or you don't see reason why your code should be DRY as much as possible, or you are finding it difficult to make your code DRY, i will demonstrate to two tips with examples of why you should stick to this principle.

Take a Short Look at the Problem

As a software developer your primary job is to solve problems, and most of the time we are so eager to solve the problem that we don't spend time thinking about the problem. You might wonder how does this help? First thing first, how can you be sure that you totally understand the problem when you have not done due diligence on it? Thinking about the problem is crucial and i would advise you to spend some time doing so, why?

Case 1

Imagine we have a database with a list of orders, now each order has an address property that is an object with a country, state and city property;

[{
    id: 1,
    items: ['shoe', 'bag'],
    address: {
        country: 'nigeria',
        state: 'rivers',
        city: 'port harcourt',
      }
},
{
    id: 2,
    items: ['jean shirt', 'sneakers'],
    address: {
        country: 'nigeria',
        state: 'lagos',
        city: 'victoria island',
    }
},
 {
    id: 3,
    items: ['tank top', 'leather belt'],
    address: {
        country: 'nigeria',
        state: 'delta',
        city: 'warri',
    }
},
 {
    id: 4,
    items: ['sneakers', 'bag'],
    address: {
        country: 'nigeria',
        state: 'rivers',
        city: 'port harcourt',
      }
},
 {
    id: 5,
    items: ['shoe', 'leather belt'],
    address: {
        country: 'nigeria',
        state: 'lagos',
        city: 'surelere',
      }
},
]
Enter fullscreen mode Exit fullscreen mode

Now imagine that we need to write a function that will allow us to search for an order based on a property of the address;

Examine the problem

The address field has three properties, a country, a state and a city. And we need to be able to find an order based on;

  • The country
  • The state
  • The City

We can go ahead and write three functions, one for the city, another for the state and one more for the country but in the end our code would not be dry, and we will have three functions to manage.

The properties we are going to be searching for exists on one object, that lies inside the each order; if we just went ahead and wrote our code without it being DRY, i would look something like this;

function findOrderByCountry (country){
    return orders.find(order => order.address.country === country)
}

function findOrdersByState (state) {
    return orders.find(order => order.address.state === state)
}

function findOrdersByCity (city) {
    return orders.find(order => order.address.city === city)
}
Enter fullscreen mode Exit fullscreen mode

This is fine because our solution are just one liners, imagine that it spanned over 20 lines or more and we had to change something? Maybe we renamed the address field on each order to deliveryAddress? Now we have to change address in three different places. It could be more than just the name of the field we are changing. What if we decided to add a street property to the address?? or a zip and a street property? Now we have to write more functions, which could lead to more potential headaches in the future.

Remember, that the solution to a problem is not too far from the problem itself.

Take a long look at the solution

If you don't spend sometime looking at the solution you provided for the problem then you are not employing proper problem solving skills, this will enable you to see some loopholes inside your solution and it will give you a bigger picture of the problem thus helping you get your abstraction layer right. Looking at your solution will help you determine if your abstraction layer is right or not. Do you even have an abstraction layer ??

Looking at the solution will also give you a different perspective about the problem or your solution to it, whereas you were looking at the problem like an earthly observer, you can even start seeing the problem like someone on Mars, you get the idea ? Back to our orders problem. We can re-factor our solution so we have only one function that can handle the three search scenarios rather than 3 different function.

We know that the properties we are going to be searching for exists on one object which is a property of each order, using our knowledge of working with JavaScript objects, our solution should look like this;

function findOrderByAddressField(field) {
    let foundOrders = []
    orders.forEach(order => {
        if (Object.values(order.address).indexOf(field) !== -1) {
            foundOrders.push(order)
        }
    })
    return foundOrders
}

const riversOrders = findOrderByAddressField('rivers') // find order by state
const phOrders = findOrderByAddressField('port harcourt') // find orders by city
const NigerianOrders = findOrderByAddressField('nigeria') // find orders by country
Enter fullscreen mode Exit fullscreen mode

Now we have one function that handles the three search cases, even if we add more properties to to the address field on each orders, we don't need to even touch the solution because it is already set up for this, if we remove a field same thing. If we change the name of the field we only replace one word in the solution, you start to get the picture now ?

No matter how difficult the problem is, if you are repeating yourself then you are either;

  • Not looking at the problem properly or
  • Not looking closely at your solution to the problem.

Spend some time on the problem or the solution and re-factor your code accordingly, ensure that you get your abstraction layer right and you will reap the rewards of ensuring that your code is DRY.

I do hope that you find this useful.

Top comments (3)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

This example needs a little bit of a rethink as at the moment you are comparing apples and oranges.

The first function searches one field whereas the second searches all fields.

Secondly the second function returns partial matches which may not be desirable.

Lets say I wanted to search for only properties in the state "rivers", in the first example I would do findOrdersByState ("rivers"); and get the expected result.

In the second example I might have a town called "tranquil rivers" and it would also be returned.

Finally the first code would be far more performant as you aren't having to loop through each object parameter.

Perhaps the following would be a better example (untested)?

function findOrderByAddressField(value, field) {
    field = field || "state";
    let foundOrders = []
    orders.forEach(order => {
        if (order.address[field] == value) {
            foundOrders.push(order)
        }
    })
    return foundOrders
}

const riversOrders = findOrderByAddressField('rivers', 'state') // find order by state
const phOrders = findOrderByAddressField('port harcourt', 'city') // find orders by city
const NigerianOrders = findOrderByAddressField('nigeria', 'country') // find orders by country
const riversOrders2 = findOrderByAddressField('rivers') // find orders by state by default
Enter fullscreen mode Exit fullscreen mode

Obviously I had to add a check for a null "field" otherwise it could throw an error, the advantage is it now has a default if you want (if you didn't want this behaviour you could just return false if it wasn't set).

Obviously it would still throw an error if an invalid field was entered, so at that point you might also want to check that depending on what behaviour you want. (i.e. findOrderByAddressField('rivers', 'area') would throw an error);

Collapse
 
kalashin1 profile image
Kinanee Samson • Edited

I see, this wouldn't be the final solution, and if indexOf returns partial matches, you can use array.find() instead.

function findOrderByAddressField(field) {
    let foundOrders = []
    let foundOrder;
    orders.forEach(order => {
      foundOrder = Object.values(order.address).find(v => v === field)
     foundOrders.push(foundOrder)
    })
    return foundOrders
}

const riversOrders = findOrderByAddressField('rivers') // find order by state
const phOrders = findOrderByAddressField('port harcourt') // find orders by city
const NigerianOrders = findOrderByAddressField('nigeria') // find orders by country
Enter fullscreen mode Exit fullscreen mode
Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

You still have the issue of matching across all fields in the above example, so a search for "rivers" would match on state, city and country.

Collisions would be rare but if this was something else like names with first name and surname fields, you could search for "Frank" and it would match "Frank Johnson" and "John Frank".

The code I wrote was to mirror your original functions where each one was for a specific field but your function is ideal for if you want to search across all 3 fields at once, it just doesn't do the same as the first functions so isn't ideal as a comparison.

Obviously it all depends on what the end use case is!