The Myth of Future-Proofing: Start with YAGNI
Published: 2024-01-30 19:41:17

## Introduction

Often times over my career I've always heard the phrase "We should 'future-proof' X in case it needs Y feature". On paper, it's a great idea! Why wouldn't someone want to make sure that we account for the unknown? This leads to naive and unexperienced engineers suggesting that functionality is added _before the feature itself is requested_. However, herein lies the dilemma; when you add a bunch of unnecessary functionality to your codebase it becomes confusing, unreadable, and in the long run, unmaintainable. The reality of the situation is that you may never need that particular functionality in the first place. Let's dive into some examples as to why this is generally bad practice and some strategy to mitigate the want to 'future-proof' your code.


## Adding Features before they're requested is BAD

In the introduction, I mentioned that it was bad practice to "future-proof" your code by adding functionality for features _before they are requested_. I cannot emphasize enough on the  _before they are requested_ portion of my statement because it's the crux of the issue. Lets use a simple example to illustrate my point. Lets say we have a TODO app and the Product owner has requested that the app simply keeps track of tasks a person wants to accomplish and can display them back to the user. The classes for this app could very simply look like this:

```
class Task:
    def __init__(self, description):
        self.description = description
class TaskList:
    def __init__(self):
        self.tasks = []
    
    def addTask(self, task):
        self.tasks.append(task)

    def listTasks(self):
        for task in self.tasks:
            print(task.description)
```

Here we have a straightforward pair of objects. We have a Task Object to hold the description, and a TaskList object which is a wrapper around an array to hold our Task Objects. What does our code look like if someone suggests the code be "future-proofed" by adding the functionality of collecting extra metadata bout the task such as a "complete by date" coupled with alerting if the date has passed when the app is ran? Let's take a look: 


```
class Task:
    def __init__(self, description, completeByDate):
        self.description = description
        self.completeByDate = completeByDate
        self.isComplete = False

class TaskList:
    def __init__(self):
        self.tasks = []
    
    def addTask(self, task):
        self.tasks.append(task)

    def listTasks(self):
        for task in self.tasks:
            print(task.description)

    def alertIncompleteTasks(self):
        for task in self.tasks:
            if not task.isComplete and task.completeByDate > datetime.now():
                print(f'task with description:{task.description} has not been completed!')
```


At first glance, this is okay. However, we can discover that this leads to a couple of different issues in the long-term: 

1. There is a now a _relatively_ complex function added to the code base that needs a unit test reducing time to market with this product.
2. The new code added is _dead code_ creating difficulty in understanding the code base, especially for less experienced engineers. 
3. While this is only a "single unused feature", we've now introduced the concept of "death by a 1000 cuts". By this I mean for every "small bit of functionality" we add to the codebase that goes unused, we slowly increase the complexity of maintaining the codebase and the difficulty in understanding it. 
4. Users may relay to the product owner/engineering team that this is a feature they want, but with differing requirements. The function that was never used has to be revisited and refactored in order to accommodate the new behavior, as well as any unit tests written. What could have been a small level of effort to add new functionality, potentially becomes a much larger undertaking.
5. We've increased engineering time by writing extra functionality and accompanying unit tests (this is even worse if you don't write the unit test, ALWAYS BE TESTING). 

While the idea to prevent future issues or be able to predict product user wants and needs ahead of time may be appealing, there are too many drawbacks to this approach. If we follow YAGNI however, we can avoid these drawbacks by staying focused on the requirements necessary for the product to run as expected. 

## YAGNI, A Principle to Guide You

So far I've stated quite a few issues by going through with the 'future-proofing' approach, but how do we prevent them? It's actually quite simple. Follow the YAGNI principle. YAGNI (which stands for You Aren't Gonna Need It) essentially helps guide you away from thinking "what SHOULD my code do" vs "what MUST my code do" to help engineers stay focused on the task at hand. I originally tried to add more to this section, but honestly, that's really all it is. When it comes down to it, really analyze the request for your apps functionality and stick to the requirements of just that request. 

## A word of Caution

Before I continue the article, I would like to issue some caveats to the rest of its content. YAGNI is _not the only thing_ that is needed to maintain a clean, robust and maintainable code base. For example, I can very easily translate the previous example to a singular function that follows YAGNI:

```
def manageTasks (description):
    task = { "description": description}
    taskList = []
    taskList.append(task)
    for task in taskList:
        print(task)
```

The new function follows the YAGNI principle and doesn't have any extra unneeded functionality, so what's the problem? It is not _extensible_. I won't get into the drawbacks of this here, as the Open/Closed Principle will be my next topic of discussion in terms of code quality. However, a quick definition for extensibility is the measure of a system's ability to add new features and the level of effort required for it. If you look at the function written in this section vs. the classes written in the section before, the classes are much more extensible in the sense that functionality is easily added through the use of instance methods and object properties. The function takes much more rework to add new functionality to. In sum, the point of this article isn't to highlight that YAGNI is the "end all be all" of code quality. 

## You're bored, and I Get it. Wrapping Up

To wrap it all up, it's always tempting to want to "future-proof" things. I've been an engineer for over 8 years at the time this article was written, and I can't count how many times I've been bitten in the rear by a bug that "future-proofing" could have prevented, or an "obvious customer need" wasn't met in a product I was working on. However, adding unused and unnecessary functionality to the app is _NEVER_ the answer. By staying focused on the task at handed and thinking "Am I going to need this?", we can deliver much more readable, maintainable and robust code in the long run.