Good coding makes for tough debugging

Paul Johnston 23 May 2018

I've recently needed to debug some tricky issues in a large, complex Java application. The application is generally well coded: it follows SOLID principles, has a comprehensive test suite and a clean, test-driven architecture. But still, users hit issues in production that need to be diagnosed and fixed.

Personally, I find code tracing - reading code to figure out what it does - to be an invaluable part of debugging. This may be a questionnable choice; more on that later.

In the past I've done a lot of work with Python and developed my code tracing skills a lot. Python code tends to follow a style of relatively few classes and relatively long methods. Java code in general, and this app in particular, follows the single responsibility principle - the S in SOLID. This results in lots of classes with very tightly defined responsibility, and relatively short methods. The style offers a lot of benefits from a developer's point of view, especially that classes can be tested individually, which is great for test-driven development.

However, having lots of small classes and methods makes code tracing harder. With Python, I could read through one or two methods to understand what it's doing at a low level. But with lots of small Java classes I need to jump around the code to get the same view. A good IDE helps, but doesn't change the fact that the task is inherently harder.

Another coding pattern that frustrates debugging is the observer pattern. The pattern makes for loosely coupled code with classes that can be tested individually. But it adds another layer of indirection when debugging. Perhaps after some investigation we've determined that when ListModel calls notifyListeners() something is trying to obtain a lock and causing a deadlock. There's another bit of work to figure out what observers could be active. Again, a good IDE helps - particularly the find implementations feature - but it's harder than debugging a tightly coupled component that explicitly notifies another class.

The app I've been working on doesn't use dependency injection. If it did I think this would create similar issues to the observer pattern: it would be extra work to determine which implementation was active.

This leads to the question: what can be done about this? The single responsibility principle and observer pattern greatly benefit developers, and I'm not aware of alternatives that are easier to debug.

One possibility is to rely less on code tracing and use a debugger more. Single-stepping through execution automatically jumps between methods. And at run time it's much easier to see what observers are active: we can inspect the list of registered listeners. There are a number of excellent debuggers for Java, and learning to use them well is an important skill. But this approach generally relies on being able to reproduce a bug when running in the debugger, which isn't always easy. And sometimes the presence of the debugger causes heisenbugs to disappear.

The different ways that coding patterns are used affects debugging. The concept of single responsibility should be interpreted pragmatically, to avoid excessive code fragmentation. And where possible, observers should use a specific interface. Rather than using a general EventListener interface with dozens of implementations, each observable class can use something more specific with fewer implementations. Also, if naming is consistently good, you can make informed decisions about whether a particular methods is relevant to a bug without having to read the implementation.

As already mentioned, a good IDE helps a great deal. I think there's potential for even smarter IDE plugins to assist tracing further. I've just noticed that IntelliJ has some Dataflow Analysis features that I will experiment with soon, and I'd be interested in other suggestions.

I hope you found this interesting. If you have any ideas for how debugging can be done better, please join the discussion. And if you're a developer who's previously not thought much about debugging production code: bear this in mind in future!