I recently finished reading a book called The Art of Readable Code by Boswell and Foucher. Overall, it was a good book on programming style – touching on many of the timeless pearls of wisdom that Steve McConnell wrote about in Code Complete using a more modern flair. One chapter that really piqued my interest was Chapter 10 – Extracting Unrelated Subproblems. This chapter touched on some really key points for enhancing your code’s readability: identifying unrelated subproblems, techniques for extraction, decoupling, watching out for when you inadvertently take things too far, etc.
All in all, the purpose of extracting unrelated subproblems is, as Steve McConnell put it, to reduce complexity – Software’s Primary Technical Imperative (i.e. Keep it Simple). Techniques used for achieving this are things like using proper layers of abstraction, ensuring a method has proper cohesion, Domain-Driven Design, and of course, as discussed by Boswell and Foucher, extracting unrelated subproblems, amongst other things.
For me, personally, the problem of needless complexity is often encountered during code reviews (CRs). One thing that I often struggle with is reverse engineering what the code is supposed to be doing based on the code as it is written. This can be really frustrating if the author isn’t immediately available, if there is a lot of code with no obvious design doc, and when high-level methods are riddled with needless low-level details that obfuscate the code’s intent.
To combat these types of issues such as the obfuscation of the code’s intent, unrelated subproblems, lack of method cohesion, and in general, needless complexity, there is one technique that I frequently use which really helps me: Use Breadth-First programming instead of Depth-First programming.
By breadth-first programming and depth-first programming, I’m not talking about using the breadth-first and depth-first traversal algorithms throughout my code. đ Instead, I’m talking about an approach to how we actually think about and write the code that we write into our IDEs.
For example, suppose I am tasked with writing a web service API for a legacy client that validates whether or not a booking should be accepted as legitimate. We’ve met with the stakeholders, done our design, and have come with the following sequential tasks to be performed by our “Verification Gateway”:
- Transform the request XML payload into a POJO data model.
- Enhance the POJOs with extra information – useful for downstream processing.
- Call a downstream Verification Service for with our enhanced POJOs for further processing – used to determine whether the booking is truly legitimate.
- Marshall the response back into a XML payload that the caller expects.
Regardless of how contrived this example is, this is our high-level description of what we want to do. To make our system as simple as possible, it would be great if we used the Breadth-First programming technique and wrote a simple high-level function that looked something approximating the following code:
public class VerificationGateway {
public String verify(String xmlRequest) {
BookingRequest bookingRequest = generateBookingRequestFromXml(xmlRequest);
enhanceBookingRequest(bookingRequest);
BookingResponse bookingResponse = verifyBooking(bookingRequest);
String xmlResponse = generateXmlFromBookingResponse(bookingResponse);
return xmlRespoinse;
}
}
The above code has advantages that include the following:
- It’s easy to see what the high-level intention of the code for an engineer who has little context when asked to do a CR.
- It’s cohesive – having a consistent level of abstraction.
- It keeps things simple.
- It has extracted and identified the keyproblems subproblems to be solved – like generating a POJO form of the XML payload, enhancing the payload, verifying the booking, and converting the response back into an XML payload again.
For me, I tend to write high-level logic only when I force myself to think about the problem at a high-level, and focussing with pain on managing my own shiny object syndrome. Ironically, I find it hard to write simple high-level code.
Instead, I find that my natural unfortunate tendency is to write code like the following:
public class VerificationGateway {
public String verify(String xmlRequest) {
// Thought 1: I need a method that can parse an XML request and hydrate
// a request. I'll write a method that does that first. Thus, I start writing
// the `generateBookingRequestFromXml()` method without even finishing the
// `verify()` method first.
BookingRequest bookingRequest = generateBookingRequestFromXml(xmlRequest);
// Thought 3: I need functionality that can add datum1 to the booking request.
// Let's write that now.
// functionality that adds datum2 to the booking request
// Thought 4: I need functionality that can add datum2, ..., datum_n to the booking request.
// Let's write that now.
// functionality that adds datum2 to the booking request
// Thought 5: Now that all that's done, I know that I need to call the Verification Service,
// so let's write that now.
// messy code that calls the Verification Service, most of which is within the scope of the
// top-level `verify()` method.
// Thought 6: I need a method that can parse out the XML payload as a string from my current
// `BookingResponse` instance. That's clearly low-level logic that isn't business logic
// related, so let's extract that method, a simple call.
String xmlResponse = generateXmlFromBookingResponse(bookingResponse);
return xmlRespoinse;
}
private BookingRequest generateBookingRequestFromXml(xmlRequest) {
// Thought 2: Implement this method before I finish the `verify()` method, leaving a broken
// `verify()` method until the very end of my implementation.
}
private String generateXmlFromBookingResponse(bookingResponse) {
// Thought 7: Implement this method.
}
}
As you can see, I added comments that allow you to gain a small glimpse into how certain undisciplined thinking leads to this kind of crap code. It’s also the kind of code that both myself, and engineers that I respect have written before. IMO, this type of code comes from depth-first thinking and Shiny Object Syndrome. In this second code sample immediately above, my comments delineate how I’m guilty of being distracted by the next low-level problem each time I encounter one. My verify()
method is never finished until almost the very end of the implementation. Consequently, it’s really difficult to discern the requirements from this code. Most of the key logic is now in a bloated verify()
method that really ought to just be a simple high-level method that delegates each of sub-problems to a downstream method.
But, when I force myself to think in a disciplined manner about my problem, and forcing myself to think in a high-level bread-first manner about a problem, I end up writing code that is pretty maintainable and simple. It also makes things easier of the engineers doing the CR because it’s easy to see what I’m trying to do. Sure there are probably errors in the lower-level problems, but the verify()
method is now so simple that it becomes easy to reason that there are no errors in the high-level problem is being solved.
In sum, based purely on my own personal, anecdotal experience, I have that using a breadth-first approach to programming leads to better code that’s simpler and more maintainable. I hope you find the same. Thanks for reading.