Air traffic disruptions, defensive programming and design by contract

On the 29th of August 2023, major flight delays were reported in the UK news due to an issue occurring with the UK National Air Traffic Services systems. The cause of the issue has since been identified as a rare instance of incorrect flight plan data. While the technical side of this issue was resolved within three hours, the repercussions would impact and flights and airports for several days. Another such event happened on the 6th of September 2023. When I had first heard about this incident, I thought about defensive programming and if applied, how it had essentially failed to account for an error, one reported to be ‘one in 15 million’.

Defensive Programming vs. Design by Contract

When writing code, we typically represent the process of taking input, processing it, and producing output as a subroutine, function, or software component. If the business logic of the component is correct, and if the expected input is also correct, then we should expect a predictable output. What throws a spanner in the works is when the invalid input finds its way in.

The main programming approach for vetting input is called Defensive Programming (DP). Another popular approach is Design by Contract (DbC). The primary goal of these approaches is to verify and validate so that erroneous values are caught before they are used by software the component. The biggest difference between these two approaches is that DP is always active within the code. This is not the case with DbC, as it should only be available at certain stages of the development life cycle.

For example, I have a function that outputs the day of the week when a day number is given. This number must an integer from 1 to 7.

def day_of_week(day):

    weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
    return weekdays[day - 1]

A defensive programming implementation of this function can be seen below.

def day_of_week(day):

    if isinstance(day, str) and float(day).is_integer():
        day = int(day)
    elif not isinstance(day, int):
        raise TypeError('day variable must be an int')
    
    if day < 1 or day > 7:
        raise ValueError('day variable be in range 1-7')
    
    weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
    return weekdays[day - 1]

So here I have to take into consideration what data will likely be used as input, including whether the input falls within the desired parameters needed by the software component. In this case, I check if the input is an integer value, that it could be delivered as a string or integer type, and that it has a value from 1 to 7. If the input does not meet these conditions, then an error is raised.

Next is a design by contract example.

def day_of_week_dbc(day: int):
    
    # preconditions
    assert isinstance(day, int), 'day variable must be an int'
    assert 1 <= day <= 7, 'day variable must be in range 1-7'

    weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
    return weekdays[day - 1]

Here I just care whether the input is an integer and if it is within the range of 1-7. If the input is invalid, then an assertion error is raised.

Defensive programming

Consider the possibility that any input entering a component is somehow invalid or may cause an error. The most obvious course of action would be to check the validity of the input before it is used. Depending on the circumstances, if there is an expectation that the type of input used may alternate to another data type, the developer may take account of such instances within their code. The need to address the uncertainty of data and safeguard a component with code is in essence defensive programming and it’s something that developers do every day.

I was never one for going out to nightclubs in my early 20s… a terrible dancer. I do remember one occasion, many years ago, when I and a group of workmates went for a few drinks after work. After frequenting a few bars in Glasgow, someone had the bright idea of visiting a popular nightclub. To be fair, none of us were dressed for such an occasion, but we thought we would try our luck. After getting in line and approaching the entrance, we were swiftly turned away by the door bouncer. The bouncer had a set of criteria for allowing access to the club, we failed to meet it and so we were denied. Defensive programming should be applied very much like a door bouncer. Once the data has been checked over and validated, it can be moved on. Otherwise, invalid data and errors should be handled appropriately.

An issue with DP (and DbC) is that extra code is generated. This can be problematic when DP is used excessively, resulting in code bloat. Code bloat is never a good thing since the more code there is, the more complex the code base becomes. The harder it is to maintain the code over time. Additionally, overusing DP can impact the program’s performance. Consider what the experience would be when someone entered a nightclub, only to be met and questioned by a bouncer at the bar, dance floor… and even the toilet.

The truth is, that if the developer has confidence that the any input on entering a component will be valid, then there will be no need to apply defensive programming to that component. It is then reasonable to consider concentrating DP at the edges of programs, ensuring that any data originating from external sources are correct and within the required constraints. An external source would be one where there is the possibility of false data being accidentally or purposely entered; for example, APIs, command-line arguments, user-submitted data, reading from files, etc.

The analogy I like to think about is that a program or application should be like a walled city from the Middle Ages. The gates on the wall allow entry into the city, with each having a few guards checking to see which goods and people are allowed in. Once they have access, they should ideally be able to move around the city freely without further checks. Excessive checks only happen at the gates. In the same vein, after DP checks data from the edges of the program that data should full under a set of expected constraints that makes it acceptable for the components within the program to use. Problems will only occur when not all required checks are taken into account. These issues can often be addressed with additional testing. Which is where DbC shines.

Michel Wolgemut, Wilhelm Pleydenwurff (Text: Hartmann Schedel), Public domain, via Wikimedia Commons
Michel Wolgemut, Wilhelm Pleydenwurff (Text: Hartmann Schedel), Public domain, via Wikimedia Commons

Design by contract

At first read DbC appears to be complex with its invariants, pre-conditions, post-conditions, etc. But fundamentally, all it is, is that a function or software component promises to produce predictable results, if and only if valid input is given. The original idea behind DbC was that it was supposed to mimic a business contract. It is as if the component was saying, “If my conditions are met then I will promise the client that I will carry out my role to the fullest and return an expected result.” DbC places great importance on defining pre-conditions, post-conditions and invariants and written contracts often have pre-conditions set out in detail, these are conditions that must be put in place before any work can be done.

In code DbC is comparable to DP. Where it tries to ensure that inputs adhere to a set of constraints. Yet DbC differs in some regards. Firstly, as mentioned, DbC can also check for expected post-conditions. DbC checking does not occur in production code. DbC is only concerned that conditions are met, it does not mitigate or provide contingencies when it encounters invalid data or errors. So when conditions are not met, DbC should cause a hard failure.

If I go back to the walled city analogy, think of DbC as placing temporary guards in front of city buildings. They check the ingoings and outgoings of buildings. These checks will naturally slow traffic within the city but if by chance a problem arises then the guards will notify the ruler (the programmer). After various test runs, the guards can be recalled and traffic within the city can flow unabated.

Applying design by contract and defensive programming within code

As stated, it is my view that DP is best used on the boundaries of a program, and if used within, used sparingly so that it does not impact on performance or add to code bloat. If DP is not applied completely, that is, if all eventualities are not considered, then following Murphy’s law “Anything that can go wrong will go wrong”, some erroneous data will make its way into the system. Mitigating issues caused by invalid data can be greatly improved by utilising DbC.

DbC support is largely dependent on the programming language used. In the cases where there is a lack of support, options may be limited, sometimes support is provided by third-party libraries. Yet even if there is no support, DbC concepts can still be applied to code, but this takes some creativity on the developer’s part. DbC will add to the size of the code base but this code is often removed when released.

DbC vs TDD

The thought occurred to me if TDD (Test Driven Development) can be considered an alternative to DbC. Both practices attempt to test code before the code enters the wild. For those who are not informed, TDD is a development philosophy/technique where the tests are written before the implementation. The initial test will always fail but then the developer provides the minimum implementation needed to make that test pass. Then there may be an additional aspect or constraints of the software component that must be considered. A new test is created, it runs, fails, and then the developer implements an improvement to get all tests to pass. An iterative refinement process takes the place of testing, running, failing, and implementing to the point that all possible tests should be accounted for. Notice that TDD is heavily focused on passing tests, tests which are identified by the developer and there lies the fault.

With this in mind, I do not believe that TDD is an alternative to DbC. TDD is beneficial in many ways but TDD can give the developer a false sense of security. In theory, if all tests pass then there should be no failures. However, human beings are fallible and while developers are usually good at writing tests for most cases, sometimes they miss one or two. Could be something as simple as forgetting a bounds-checking test, or maybe ensuring that a zero-division error is accounted for. Like DP, the practice of TDD cannot account for all eventualities.

I like to think of TDD as lab testing, where each component or unit is tested individually under a set of ideal conditions. These components should work without issue in a program or application. But DbC is more like field testing, testing how components work together and flagging errors when something unexpected happens. Thus both testing methodologies are complementary to each other.

DbC is no silver bullet either, the developer can mess up here too. This is why there exists bug reporting. If the bug can be replicated with code that has DbC enabled, then in many cases it will likely aid in the debugging process. Regardless, no matter how thorough developers are at trying to eliminate errors, they will arise somewhere out of unknowns.

Applying DbC to Python and JavaScript

While there is no formal language support for DbC in Python and JS. It can be achieved in both languages, to some degree, when using assert. An assertion tests a condition, if it is false it will raise an error and write out a message. There are some advantages to using assertions in code.

One advantage is the ability to enforce type-checking. Both languages are loosely typed, meaning that any type of data can be submitted as a component argument. Typescript, a superset of JS, enables type-checking. An interesting point to make is that Typescript compiles to JS code and this code will not be type-checked. Similar type-checking functionality can be added to normal JS using assert. There is a catch though, which I will elaborate on soon. Python does not have type checking but does have type inference, think of it as syntax which hints that a software component should be using a certain type. While the type is inferred it is not enforced, so any data type can be submitted without error. Asserts can be used to enforce type-checking.

# Python
def increment_int(x: int):
    assert isinstance(x, int), 'x must be an int'
    return x + 1

// JavaScript
function incrementInt(x) {
    console.assert(typeof x === 'number', 'x must be a number');
    console.assert(Number.isInteger(x), 'x must be an int');
    return x + 1;
}
Js console.alert example
Expected output in a JS console

Remember DbC is not needed in the final code, but both of these languages are scripting languages, so code is usually read as is by the interpreter. However, some extra steps can be taken to remove the assert statements from both languages.

The Python interpreter provides a command line option -O. When this is enabled the interpreter will skip over assert statements, providing performance gains. The developer just needs to remember to enable this option.

python3 -O script.py

JavaScript requires a little bit more effort. Firstly assertions are raised with a console method, so it does not result in hard fails. When releasing code many JS developers tend to minify it, reducing the file transfer size, and the tools used to achieve this (bundlers/optimisers) often have the option to remove console notifications, so there is one solution. Another technique albeit a hackish one is to simply overwrite the assert method using an empty function in JS, while not ideal this will result in better performance than allowing the assert statement to remain.

// JavaScript DbC hack
const DEBUG = false;

if (!DEBUG) {
    console.assert = function() {};
}

function incrementInt(x) {
    console.assert(typeof x === 'number', 'x must be a number');
    console.assert(Number.isInteger(x), 'x must be an int');
    return x + 1;
}
JS DbC hack