Managing Complexity

If you haven’t read the incredible book Code Complete by Steve Mcconnell, well, stop reading this amateur blog post and read something worthwhile instead. Having a lot of down time recently, I was able to read through it front to back in about 2 weeks. There is so much timeless, worthwhile content in the book and I suggest you buy it, as nothing you read here will be able to fully sum up all the information in the book.

If there’s one thing that Steve wants his readers to take away from his book it’s that we as humans are inferior to computers to the point where we can’t even fully comprehend the software we write for them. Well…at least not all at the same time. This is one of the main arguments for why it is then essential to limit complexity in software projects. The book offers endless ways to do this for numerous different aspects of development, but they all boil down to the same main end goal. At a level higher than the source code there are a number of different concepts that are touched on throughout the book that come together to reach this lofty goal. Each post in this series will focus on one of these concepts.

Part 1 - Working Within the Problem Domain

Let’s say you’re working on a program that calculates how much someone spends on different types of things throughout the month and that data is stored in an array of purchase objects that have a field indicating what type of purchase it is. If you wanted to get the total amount spent on groceries in the month of February your code might look something like this:

//get groceries February total
var februaryGroceryTotal = 0
purchases.filter { it.type == 3 && it.month == 1 }
.forEach { purchase ->
	februaryGroceryTotal += purchase.amount
}

This is an example of working, but hard to understand code. Why? Because it takes too long to fully understand what’s going on and how the solution relates to the problem it’s solving. At a glance it seems that there might be an error. Does 1 represent January or February in the month comparison? And what type does 3 represent? Both of these values would need to be cross-referenced with some sort of documentation before we even knew if we were making the correct comparison. And on top of that, your brain is still required to understand the contents of the purchases array. Here’s a better example:

fun getTotal(month: Month, purchaseType: PurchaseType): Float {
	var total = 0
    
	purchases.filter { it.month == month && it.type == purchaseType }
    .forEach { purchase ->
		total += purchase.amount
	}
    
	return total
}

var februaryGroceryTotal = getTotal(Month.February, PurchaseType.Grocery)

In the second example, getTotal would be part of the interface of a class that managed a user’s purchases. In this way, all of the low-level details are encapsulated and the exposed interface is concise and easy to understand. The routine’s implementation has also been improved by converting the arbitrary numbers used in the first example into concrete, problem domain values. It’s a lot easier to know what getTotal(Month.February, PurchaseType.Grocery) is doing vs. getTotal(1, 3). Code written in the problem domain is also often self-documenting code. In the first example, if we wanted to make the code more clear we could comment that type 3 is grocery and that month 1 is February. In the second example that isn’t necessary because we already know that information from the type names.

If done correctly, requirement definitions that proceed any code you write should also be defined within the problem domain. In this way it makes little sense to define your code differently than your requirements. A routine called getTotal with month and purchase type parameters maps to the requirement…

The user should be able to see how much they spend on different types of purchases by month.

…much better than an unclear array filter and loop summation does. In general, when trying to convert high-level requirements into code, its a good idea to try to define real-world objects that relate to those requirements first. Once you have this skeleton defined the problem domain solutions at the routine level will come more naturally.

Up Next

Hopefully this read was short enough to easily get through, but long enough to help keep the problem domain in the front of your head when constructing your next class or routine. Encapsulation and abstraction, which were lightly touched on here, are key aspects of managing complexity and will be expanded upon in the next part of this series.