← Back to Articles

Duplicate Code: Better Than Wrong Abstraction

Featured Image

Duplicate Code is Better than the Wrong Abstraction

When writing software, developers often know about the importance of the DRY (Don’t Repeat Yourself) principle.

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

And for good reason! Eliminating duplicate code reduces the risk of inconsistencies, makes changes easier to propagate, and generally improves maintainability. You should alway strive to make your code dry. However, blindly chasing DRY can sometimes lead to the wrong abstraction — an abstraction so convoluted that it’s hard to understand, debug, and extend than if the code had been left duplicated. In such cases, duplicating code can actually be the better option.

The Danger of the Wrong Abstraction

Sometimes abstractions can do more harm than good. If you have similar pieces of code and decide to unify them into a single reusable function or component and the abstraction requires complex logic, numerous conditionals, or tightly coupled dependencies, it can result in a very messy code that’s brittle, unintelligible, and difficult to maintain.

This is where the WET principle (Write Everything Twice) comes into play. WET doesn’t advocate for rampant code duplication but rather for careful and deliberate duplication when it’s not yet clear how to create a meaningful abstraction. By keeping code duplicated, you leave room for clarity and iteration. Over time, as patterns emerge, the right abstraction will often become obvious.

An Example of the Wrong Abstraction

Let’s consider an example from a real project (believe it or not) to illustrate this. Imagine you’re working on a Vue project and you need to add a modal component. But you're thinking - I don't want to have duplicate modals everywhere. So you create one modal in the root of your app and when you need to show a modal, you just call this root modal and pass the content to it. Sounds great in theory, right?

First, you stash the code for opening the modal inside of your Payments API, so you don't end up calling showModal() multiple times - DRY!

// Payments.vue
<template>
    <div>
        <button @click="showData">
            Show Data
        </button>
    </div>
</template>

<script>
    import PaymentsApi from '@/api/payments';

    export default {
        methods: {
            showData() {
                PaymentsApi.showData(
                    this.$root,
                    this.$root.extendComponent(SuccessfulPaymentInfo, {
                        user,
                    })
                );
            }
        }
    }
</script>

Now PaymentsAPI becomes responsible for showing the modal and fetching data. But since the modal lives at the root of our app, we need to tap into the root app instance to actually show the modal. We also need to pass the Vue $root instance to our API method. Let's also make sure we pass along the root instance and the component to render.

// PaymentApi.js
function showData(root, component) {
    root.showModal(root, component).then(() => {
        fetchData();
    })
}

Now we've coupled our API with both the root instance of our app and the modal component. But we also need to pass in the content dynamically, to really make our code DRY.

// app.js
export default {
    methods: {
        showModal() {
            this.$refs.modal.open(component);
        }
    }
}

Since our root instance doesn't have the modal itself, we need to access the actual modal component and tell it to open by directly calling of of the component's methods. We need to surgically remove the old HTML and replace it with our new component. It will be DRY! Here's our actual modal component:

// modal.vue
<template>
    <div v-if="isOpen" ref="content">Modal</div>
</template>

<script>
    export default {
        data() {
            return {
                isOpen: false,
            }
        },

        methods: {
            open(component) {
                this.isOpen = true

                this.$refs.content.innerHTML = "";
                this.$refs.content.appendChild(component.$el);
            }
        }
    }
</script>

And the modal itself is being rendered in index.html:

<html>
    <body>
        <modal></modal>
        <div id="app"></div>
    </body>
</html>

Look at that! Our code is perfectly DRY. We've broken every rule in the book but our code is DRY! But we've ended up with code that is so tangled up and twisted that hardly anyone will be able to understand it. The technical term for this is spaghetti code. This is a good example of creating the wrong abstraction. The end goal should never be to have as dry code as possible. It should be to have code that is easy to read, understand and maintain.

The Right Abstraction

This is an example where a little bit of duplication is actually helpful. Instead of having a single instance of modal, let's add the modal where we need it. You would still end up with a single Modal component which would get mounted in multiple places.

// modal.vue
<template>
    <slot v-if="modelValue" />
</template>

<script>
    export default {
        props: {
            modelValue: {
                type: Boolean
            }
        }
    }
</script>

Now whenever you need to use a modal, simply include it on the page:

// Payments.vue
<template>
    <div>
        Payments

        <Modal v-model="isPaymentsModalOpen">
            Content
        </Modal>
    </div>
</template>

<script>
    import PaymentsApi from '@/api/payments';

    export default {
        data() {
            return {
                isPaymentsModalOpen: false
            }
        },

        methods: {
            async showData() {
                await PaymentsApi.getPayments()
                this.isPaymentsModalOpen = true
            }
        }
    }
</script>

Conclusion

While DRY is an important principle and you should always keep your code as dry as possible, it should never come at the expense of readability and maintainability. If creating an abstraction results in convoluted, hard-to-understand code, it’s better to leave the duplication in place. By embracing deliberate duplication when necessary, you allow patterns to emerge naturally, paving the way for the right abstraction at the right time.

Listen to Deep Dive

Download