Symptoms and Causes of Complexity

5-minute read
Table of Contents

This article is based on John Ousterhout’s lessons on software design, specifically his book “A Philosophy of Software Design”. He speaks on what can cause a system to be complex and what ways complexity can manifest itself. A software system doesn’t have to be large in order to be complex. It is very much possible to write a very confusing 100 lines of code. It is also possible to have a code base with 1000’s of lines that are easy to pick up and manage.

Causes

There are two (2) causes of complexity in a system:

  • dependencies
  • obscurities

Dependencies

When we change some of our code, do we have to change other sections as a result?

Obscurities

Are there places in our project where we do not understand? Does modifying code involve us making assumptions that are incorrect?

Symptoms

There are three (3) ways to identify a system that is complex:

  • change amplification
  • cognitive load
  • unknown unknowns

Change amplification

Consider the following C code:


double mass_of_object_1 = 1.56;
double mass_of_object_2 = 4.32;
double weight_of_object_1 = mass_of_object_1*10;
double weight_of_object_2 = mass_of_object_2*10;

We have the masses of two objects and we are calculating their weights by multiplying by an acceleration of $10 ms^{-2}$.


What do you think?

1) In how many places would we have to change the code if we decided to use a different value for acceleration due to gravity e.g. $9.81 ms^{-2}$?

  1. 0
  2. 1
  3. 2
  4. There are 2 places where we use 10

2) How many changes would we have to make if we were calculating the weights of 10 objects?

  1. 0
  2. 1
  3. 10
  4. 10 objects means 10 calculations

I hope we can appreciate that simply storing the acceleration as a variable and referencing that variable in the calculation would reduce the number of places we have to change if we had to use a different acceleration:


double g = 9.81; // only this line needs to be changed
double mass_of_object_1 = 1.56;
double mass_of_object_2 = 4.32;
double weight_of_object_1 = mass_of_object_1*g;
double weight_of_object_2 = mass_of_object_2*g;

This demonstrates the issue of change amplification. In how many places must I change my code if I make a different design decision?

The same issue arises when we use inline CSS instead of a stylesheet. You may decide to set the color of a button using inline CSS but what will happen if you want to color all the buttons on each webpage? How many changes will have to be made if you have 4000 buttons across your entire website? One stylesheet can be used across the entire website and the changes will be propagated when the stylesheet is updated.

Cognitive load

The more information that a developer has to keep within their working memory in order to make a change to the system, the more cognitive load is accrued.

Imagine a scenario where you have to add a new feature to your web app. In order to do this, you are required to:

  • navigate in your file system to the server code repository
  • create a function for the page on the server side and a route “/hello”:

@app.route("/hello")
def hello():
    data = {'message':'hello'}
    return data
  • navigate in your file system to the client code repository
  • create a route for the app on the client side that corresponds to the “/hello” route on the server side
  • commission the endpoint in another file:

const routes = [
  { path: '/', component: Home },
  { path: '/hello', component: Hello }
]
  • add code to perform an AJAX request on the client to the server to get the JSON data
  • add client-side logic parse the JSON response and display it as HTML
  • rinse and repeat for every new feature

That’s 8 steps! Compare this to:

  • navigate in your file system to the server code repository
  • create template file in the server code repository:

<h1>My Message App</h1>
<strong>Message: {{message}}</strong>
  • create route and function to return the template file, in the same server code repository:

@app.route("/hello")
def hello():
    message = "hello there"
    return render_template("hello.html",message=message)

Here we use only 3 steps, 1 of them being the navigation to repository. This means that there are only 2 things that a developer needs to understand in order to add new features to the program. This also means that there are only about 2 places where the developer needs to check if the feature does not operate as intended (the template might be incomplete or the function might not be passing the intended message into the template).

We can use the right tools to make steps like the file navigation easier to do - bash aliases and code editor plugins are some of these. What we cannot easily address is the complexity created by having to create code on the client and server every time we have to add a feature.

Cognitive load is necessary to some extent because you have to remember things in order to code. What we should strive to do is reduce the ratio of code added to the functionality achieved. If we have to do 8 actions every time we want to return a new JSON object corresponding to a route then we might be working with a system that mentally overwhelms the developer instead of making it easy for them to write code.

While it is true that cognitive load can be managed more easily by more skilled developers, it is important to note that a system need not become increasingly difficult to modify (thus requiring a higher skill level) over time.

Unknown unknowns

You can’t fix what you don’t understand.

Conclusion

Being mindful of how we manage and remedy complexity in our software systems is an essential skill if we are to have them last for years and treated with care by those who will work on them in future.

Support us via BuyMeACoffee