By Andrew Rousseau. Published on March 13, 2023.
In the previous blog post we explored detail complexity and some of the things we can do to reduce it. As it happens sometimes the very thing we do to reduce one type of complexity can inadvertently introduce another type of complexity. To this end it will seem as though some of the suggestions throughout this blog contradict one another.
The reality is that the readability and maintainability of our code is highly dependent upon the choices that we make. In making those choices we must consider the trade-offs of one approach versus another and then live with the decisions that we've made. Oftentimes this will feel as though there is not a right solution but rather a least bad solution.
Dynamic complexity is when the effect of our code is not obvious when the cause is introduced. That is to say that the engineer working on the code has difficulty following the logic from beginning to end. Where detail complexity makes this difficult due to density, dynamic complexity makes this difficult due to navigation or the amount of paths conditional logic introduces.
Broken into too many smaller methods
One of the recommendations to reduce detail complexity was to take long or highly detailed methods and divide them into smaller methods. Ideally each of these would accomplish only one effect and therefore be simple and easy to follow. However, breaking a longer function into smaller functions can potentially create navigation challenges.
In the image above you can see that when a single process is broken across several methods it can require us to navigate back and forth between different lines within the same file. This adds a burden to the engineer as they must continually move back and forth in order to follow the logic for a given process.
Ideally we will not have broken this into too many smaller methods and the burden will be low therefore resulting in less complexity. However, if we have broken it into too many smaller methods we may have made the complexity worse. Again we're striving for a solution that balances the detail and dynamic complexity to result in readable and maintainable code.
When the methods are spread across multiple files this compounds the navigation difficulty in tracing the effect that the overall process is having. Some languages, like those that have inheritance, can have a high level of dynamic complexity. This is because many of the methods in a given class will be spread across many files, some of them way up in a framework or library.
The resulting experience for the engineer is that they end up with many tabs in their editor open and must navigate between files to trace the logic for any given process. A good IDE can make finding these methods simpler but the burden and cognitive load of navigating between methods will still hinder the readability and maintainability of the code. A balance must be struck where we evaluate the trade-offs and make improvements in the parts of the code base that we have control over. Sometimes very little can be done if the complexity is inherited from the language or the framework in which we are working.
Full of different cases/conditions (i.e. switch, if, else)
Methods that are full of different cases and conditions can bring with it both levels of detail and dynamic complexity. As a general rule we must consider the amount of paths introduced by each case or condition. Methods suffering from this could contain either detail or dynamic complexity and sometimes both.
There are tricks to reducing the amount of paths introduced and can therefore lessen the amount of dynamic complexity involved. One trick would be to string together two separate if statements into a single if else condition. Another would be to combine conditions if both are truly required for the process to continue. I won't go into too much detail on these as that will be covered in a later post regarding NPath complexity.
Either way, anytime we go beyond a couple of cases or conditions inside of a single method we should consider that we may be trying to accomplish too much. At which point we must explore breaking the logic up into smaller functions to determine if that is a better option to improve readability and maintainability.
Tackling complexity can be a difficult and advanced engineering concept. It often takes engineers many years of experience to appreciate the need to reduce complexity. The fact that many of the suggestions here must be weighed against each other can make learning the concepts difficult.
If we keep our attention on how easy or difficult it is to navigate the code we will begin to have an impact on complexity. Readability and maintainability of code is more of an art than a science. No two Engineers may agree entirely on what makes code readable or maintainable. However, most will agree that they don't like working on code that is difficult to understand. Consider the trade-offs discussed thus far when you are making decisions about how to structure your code and you will begin to reduce the codes complexity.