When implementing software, it is essential to apply a consistent design to the logic throughout an application.  When examining any function, a developer or tester has to understand the logic that was being applied.  This examination requires understanding the nuances of the individual function, appreciating the corner cases, and determining if the correct logic was applied.  A code review can be greatly simplified by uniformly using principles to the design of logic. 

Utilization of design principles within a design allows for the tester or maintainer to intuit a great deal more about the code.  Upon entering a new function, specific information about the function can be (safely) assumed.  Accurate assumptions, based on understanding the design principles, help with the immediate understanding of the code. 

Principle: “Test for Truth”

At Hellebore, we continually look for the principles we use to guide software engineering practidces. This principle is designed to help the human in the loop, not necessarily produce the most efficient code for the computer.  As such, it makes it easier for us as engineers to reason about the software and (hopefully) make fewer errors when implementing complex logic. Don’t check if something is “not false” and don’t check for “failure”. Check if something is “true” and if a function returned “success”.

Functions will often perform some operation.  The result of the operation is verified through some logical test.  If successful, the execution continues. Typical logical tests include checking for null pointers, checking for valid inputs to an algorithm, performing an algorithm and testing for valid output, and returning a valid result. 

This progression makes perfect sense.  Let’s examine an ad-hoc implementation of this type of function:

Naïve Implementation

int ComputeFoo(int x, int y)
{
	const auto gcd = GetGCD(x, y);

	if(gcd <= 0)
	{
		// Failure.
		return -1;
	}
	else if(gcd > 10)
	{
		x *= y;
	}

	if(gcd > 100)
	{
		// Failure.
		return -1;
	}
	else
	{
		x += y;
	}

	if(x * y < 0)
	{
		x *= x;
	}
	else 
	{
		// Failure.
		return -1;
	}

	// Success.
	return (x * y) / gcd;
}

While, in this trivial example, it is generally easy to follow what the coder intended.  However, complex cases cause several problems.  Firstly, when the debugger drops you off in the middle of this function, the tester will first have to scroll to the top of the function and see what work has been done to get to where the error occurred.  Secondly, it is not apparent what the correct path is through the function.  By applying the principle of “Testing for Truth” to function design, these problems are eliminated. 

Refactoring to Test for Truth

Here is the same function has been re-written after the application of the principle of “Testing for Truth.” The deeper we go into the function, the more tests have passed and the closer we are to returning the result:

int ComputeFoo(const int x, const int y)
{
	const auto gcd = GetGCD(x, y);

	if((gcd > 0) && (gcd <= 100))
	{
		if(gcd > 10)
		{
			x *= y;
		}

		x += y;

		if(x * y < 0)
		{
			x *= x;

			// Success.
			return (x * y) / gcd;
		}
	}

	// Failure.
	return -1;
}

Now, it is trivial to understand what tests have passed and one the correct path is through the function.  A cursory examination can divine several traits out about the function based on knowledge of the way it was designed.

There is precisely one successful return location from this function and exactly one failure return location from this function. 

This simplicity is the result of a successful application of the “Testing for Truth” principle.  This simplified and well-structured code leads to better function design, fewer bugs, lower costs of maintenance, and a more natural understanding of the logical flow. Consider a similar principle when implementing new code or maintaining an existing code base.

Related posts