Changing software
Most of the work I do involves changing software. In order to add a new feature to a program, modify its behavior, or fix a defect, I have to change some elements of the system one way or another.
The only situation where I don’t need to “change” software is when I’m creating something new from scratch. But even then, the nature of development quickly turns from creating into changing. As the system grows, sooner or later I’m forced to touch existing parts of the system. I have to modify parts of the system to accommodate new features, changed requirements, or to fix bugs. Therefore, I would argue that changing software forms the overwhelming majority of the work I do.
Changing any system takes time and effort. Since my time and mental capacity is limited, I would prefer such changes to take only a short amount of time and not too much effort. In an ideal world, it would be quick and effortless to change the software whenever needed.
Limiting the time and effort to change software seems obvious from an economical point of view. Most of the time, somebody is paying me to write this software, so it would benefit them as well if I didn’t have to spend an eternity changing the software to fit their needs.
Alas, as systems grow, changing them becomes more and more difficult. In my understanding, there are two big contributors to that difficulty:
- The effort required to understand the system
- The effort required to change the structure of the system
Let’s imagine that I have decided to change the behavior of an existing software system. In order to change the behavior, I need to change the software in some way. To be able to make the change, I must be able to understand how the system is built. Unless I have a good understanding of the system to begin with, I have to spend some time investigating how it works. I also have to figure out what kind of changes I would need to make to cause the system to exhibit the new, wanted behavior.
At this point, let’s pretend that I have figured out what change I have to make. Let’s also pretend that I have made an important discovery: If I make the aforementioned change, I also have to change other parts of the system. Well, that doesn’t sound good, does it? What if changing those parts also requires me to make more changes elsewhere in the system? I would also hope that each of those parts of the system are not too difficult to understand.
In this case, the biggest problem is the tight coupling between the different parts of the system. It results in a cascade of changes throughout the system. Each of those changes could be big or small (hopefully the latter). Regardless, each change adds resistance to the change I’m trying to make. The effort required to change the structure of the system increases with every such relationship.
The second—and not much less important—problem is the lack of cohesion. If in order to make a change I have to make changes to other parts of the system, I now have to understand a lot more about the system than if the change were contained to a small part of it. There are now many parts that are changing simultaneously, so I have to somehow keep all of them in my head. Keeping even a single part in my head at a time becomes more difficult because of the coupling between different parts of the system: I cannot fully understand one part without looking into another that lies somewhere else.
By now it should be obvious how I can avoid this kind of mess. To prevent the rippling effects of change, I should minimize coupling between different parts of the system whenever possible. Such coupling cannot be completely avoided (a system without coupling is trivial and doesn’t do anything useful), but the pathological types of coupling should be reduced as much as possible. Loosely coupled systems are less resistant to change.
The other part of the solution is to maximize cohesion inside modules. Cohesive code is easier to understand than incohesive code kind of by definition, since all the relevant pieces are contained within a cohesive module. You don’t need to look into other modules in order to understand how the code works. So, by increasing the cohesion of a module, we can make it easier to reason about.
Now the only problem remaining is how to minimize inter-modular coupling and maximize intra-modular cohesion in practice. Unfortunately, I don’t have a silver bullet for this. I would also be very skeptical of anybody who claims they do. It’s a difficult topic. Many books and articles have been written about it over several decades. Yet here we are, still struggling with the same fundamental issues.
It seems to me that the best I have at the moment are some heuristics that can help design software in a way that it doesn’t become completely unmaintainable. My current understanding is that I have to somehow strike a balance between coupling and cohesion based on the context. There must be a balance, because coupling and cohesion are ultimately two sides of the same coin. But how to find that balance seems too context-specific to reduced to a few simple rules.