On adding presumed future functionality in your source code

“Let’s add this functionality in the code, we may need it in the future.” 

Sometimes this thought comes up in a programmer’s mind. You may find yourself in such a situation, where you assume you will need some presumed functionality in the future, and you think you should add it in your code now. But odds are you’re not going to need it and you shouldn’t add it [1]. 

Engineers should try not to add code for imagined future scenarios, because such scenarios may not happen in reality. And you may end up wasting your time and money if no one uses what you build. It’s also possible that by chasing your imagination you are forgoing better present opportunities. 

Is it easy to avoid adding code for presumed scenarios? With major features, it’s easy to spot and avoid. But it’s hard to spot when you are doing minor tweaks in the code. And this presents challenges for engineers, particularly inexperienced ones, because even the smallest bug fix can lure you to build on assumptions. 

A while ago, an engineer told me a short story about how fixing a simple bug tempted him to build on imagination. But some thinking about the problem before the implementation saved him not only from building software in that way but also from writing unnecessary code. Let’s get to the story.

The engineer, new to Python, had to fix a bug in a function in a Python repository. The function was calling a URL based on values received as function parameters, and it was returning the response received from the server. 

The function looked like:

def get_messages(read, delivered, metric):

   if read:
       url = "{base_url}?read={read}".\
format(base_url=base_url, read=read)
       # do stuff in db based on parameters passed
       return requests.get(url)

   if delivered:
       url = "{base_url}?delivered={delivered}}".\
format(base_url=base_url, delivered=delivered)
       # do stuff in db based on parameters passed
       return requests.get(url)

   if metric:
       url = "{base_url}?metric={metric}".\
format(base_url=base_url, metric=metric)
       # do stuff in db based on parameters passed
       return requests.get(url)

   if delivered and read:
       url = "{base_url}?delivered={delivered}&read={read}".\
format(base_url=base_url, delivered=delivered, read=read)
       # do stuff in db based on parameters passed
       return requests.get(url)

   if delivered and metric:
       ...
       # do stuff in db based on parameters passed
       return requests.get(url)

   if read and metric:
       ...
       # do stuff in db based on parameters passed
       return requests.get(url)

Historically, the function was called with only one parameter, and it had worked fine. But now an important customer demanded some functionality that required calling the function with more than one parameter. But calling the function with more than one parameter caused it to return incorrect results. The engineer had to solve this immediately. 

The function was doing a few other things (such as calculations and saving results in the database), not relevant for now and omitted in the code excerpt above. Also, this function had no unit tests. Since the engineer was new to the codebase, it took him a little while to completely understand this function.

Eventually, he observed that he had to fix the “return” and “conditional” statements. He realised he could fix this and be done with it, release the code and declare victory. “But there has to be a better way,” he mused.

He felt he should take this opportunity also to handle possible scenarios that may arise in the future. For example, what if new query string parameters needed to be added in this function? Looking at the current code, it was clear that adding support for new query string parameters in the future would make the current code bloated. 

He thought it would be neat to implement “get_messages” in such a way that whenever a new query string parameter was added and a new parameter was passed to the function, no code would need to be changed in the function. He thought this would save the company some future costs, ignoring that building on assumptions can raise present costs. How to do this now occupied his mind. So he decided on an implementation that would look something like this (he was still using pencil and paper and not the source code editor):

def get_messages(read, delivered, metric, **kwargs):
   params = {}
   if read:
 # do stuff in db based on parameters passed
       params.update({'read': read})
 
   if delivered:
 # do stuff in db based on parameters passed
       params.update({'delivered': delivered})
 
   if metric:
 # do stuff in db based on parameters passed
       params.update({'metric': metric})
 
   for param, value in kwargs.items():
 # do stuff in db based on parameters passed
       params.update({param: value})
   return requests.get(base_url, params=params)

He concluded it was cool.

The function now supported adding new query string parameters without requiring any changes in the “get_messages.” Now if, in the future, a new query string parameter needs to be added, he thought, all an engineer has to do is add that new query string parameter in the function call, as below, and that would be it:

def get_messages('all', 'all', ‘some’, abc='abc')

But the engineer thought a bit more about this. 

He then had a quick chat with a senior colleague, who pointed out that the engineer should look more closely in the repository to see how this function was being used. And also, before making this change, he should consult the code history and the product manager. 

The engineer looked in the repository to find out how many times this function was used in the code. He found out it was called from three dozen places. This meant requiring him to test many code paths, not just for existing functionality but for the new functionality as well. Lest we forget, the function was doing more things than shown here, so in the absence of any automated tests (unit or integration) and because of time constraints, testing the new functionality would take more time than the customer had. 

He also looked at the commit history of the code;  the function had not changed in the last two years. He asked the product manager if there were any changes planned around this code. The answer was no. Nor could he find any such feature requests from any of their customers. So he decided not to add support for such missing functionality, if neither customers nor the product team desired it, and dropped this solution.

Please note that up till this point he had been using paper and pencil to write and sketch ideas. He had not yet written any source-code.  Eventually he thought of an implementation that looked like below. 

def get_messages(read, delivered, metric):
   params = {}
   if read:
       params.update({'read': read})
   if delivered:
       params.update({'delivered': delivered})
   if metric:
       params.update({'metric': metric})
   # do stuff in db based on parameters passed
   return requests.get(base_url, params=params)

He thought about the pros and cons of this solution for a while. He finally went ahead and implemented it. The function isn’t a clever piece of code, but it fixed the bug, and was readable and short enough to fit on the screen. Also, compared to the original code, it looked a bit cleaner. 

He also added a few unit tests and comments in the final code, and then validated the changes through manual testing. Things looked good. The code was released,  it worked out fine; it served the business need. 

The point here isn’t to show the solution he chose, one of many (possibly clever) solutions. There are a couple of important lessons to be learned from this tale. The first is that an engineer shouldn’t rush to open the code editor, and implement the first solution that comes to mind. The second lesson is that we should try not to build for presumed future scenarios. If we do, we may end up paying a heavy cost by ignoring other urgent and important matters at hand. And such costs can prove to be fatal if you’re a startup or struggling just to survive. 

References:

  1.  This is also known as “You aren’t going to need it” (Yagni) in the Extreme Programming world.