I enjoy programming. Unfortunately, my skull is too small to hold a lot of stuff at the same time. Because of this, I have to keep my eyes on the target and I have to take small steps. If I don’t, I risk introducing bugs and unneccessary cruft. Perhaps you’re different, but this is how my head works.
A technique I sometimes use is pseudocode. I write my intended code out as simple sentences first, to see if it makes sense to me. If it doesn’t, I rewrite and edit more. When I’m happy with the story, I translate it into real code.
Bonus: When I do this, I end up with well commented code “for free”: I can simply choose how much of the original pseudocode I want to retain as comments.
Let’s do a simple example.
The ErrorTracker
I have an app that relies on an important network operation. I want feedback once a certain number of network requests start to fail: for instance, if 2 out of my last 10 background network operations fail. In that case, the app should visually communicate to the user that something is wrong.
Public api
Let’s first describe the contract that this thing should offer to the rest of my system.
// The ErrorTracker tracks and reacts to error rates over a sliding window
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
This already feels like it could be some sort of queue data structure… but let’s not get ahead of ourselves. Let’s sketch out the api first, and worry about the internals later.
How about this?
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
}
We need to feed it with data when things go right and wrong:
enum class Entry { SUCCESS, FAILURE }
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
fun add(entry: Entry) {
}
}
This is how it’ll be used from the outside world:
val errorTracker: ErrorTracker = ErrorTracker(2, 10) {
println("Got more than 2 errors in 10 operations!")
}
// ... every time the network operation goes wrong
errorTracker.add(Entry.FAILURE)
// ... every time the network operation goes right
errorTracker.add(Entry.SUCCESS)
Internals
Ok, let’s go back and implement the internals of our ErrorTracker. Let’s write it out as a story first.
enum class Entry { SUCCESS, FAILURE }
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
fun add(entry: Entry) {
// Update the current list of errors: only track the last N entries
// Is the number of errors in the list above maxErrors...?
// then run the onError... function
}
}
This seem straight forward enough.
Except: while we stay above the maxErrors… threshold, this will trigger the onError… function every time we add an entry.
We only want the function to fire once every time we cross that threshold. Let’s adjust a bit.
enum class Entry { SUCCESS, FAILURE }
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
fun add(entry: Entry) {
// Check if we are above the error threshold already
// Update the current list of errors: only track the last N entries
// Is the number of errors in the list above maxErrors...?
// If we hadn't already crossed above the error threshold,
// then run the onError... function now
}
}
Ok, I think I’m happy with how this looks now. Time to translate into code.
I like to implement each chunk of code below its original pseudocode. And look, we end up with well documented code out of the box!
enum class Entry { SUCCESS, FAILURE }
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
val entries: MutableList<Entry> = mutableListOf()
fun add(entry: Entry) {
// check if we are above the error threshold before adding the latest entry
val aboveThresholdBeforeThisEntry = isAboveErrorThreshold();
// Update the current list of errors: only track the last N entries
entries.add(entry)
if (entries.size > windowSize) {
entries.removeAt(0)
}
// Is the number of errors in the list above maxErrors...?
// If we haven't already crossed above the error threshold,
// then run the onError... function
val aboveThresholdAfterThisEntry = isAboveErrorThreshold();
if (!aboveThresholdBeforeThisEntry && aboveThresholdAfterThisEntry ) {
onErrorCrossingAboveThreshold()
}
}
fun isAboveErrorThreshold(): Boolean {
val errorCount = entries.count { it == Entry.FAILURE }
return errorCount > maxErrorsInsideWindow
}
}
This looks simple enough that I can drop the pseudocode: the comments actually feel a bit excessive.
If the code was more complex I would feel more of a need to keep the comments in there for clarification. However: my goal is to make the code itself readable enough that an imagined future reader won’t need those comments.
In this case, let’s try to let the code stand on its own.
enum class Entry { SUCCESS, FAILURE }
// Tracks and reacts to error rates over a sliding window.
// It will react *once* if more than X out of Y operations are errors.
// It will react again the next time the error rate rises above that threshold.
class ErrorTracker constructor(val maxErrorsInsideWindow: Int,
val windowSize: Int,
val onErrorCrossingAboveThreshold: () -> Unit) {
val entries: MutableList<Entry> = mutableListOf()
fun add(entry: Entry) {
val aboveThresholdBeforeThisEntry = isAboveErrorThreshold();
entries.add(entry)
if (entries.size > windowSize) {
entries.removeAt(0)
}
val aboveThresholdAfterThisEntry = isAboveErrorThreshold();
if (!aboveThresholdBeforeThisEntry && aboveThresholdAfterThisEntry ) {
onErrorCrossingAboveThreshold()
}
}
fun isAboveErrorThreshold(): Boolean {
val errorCount = entries.count { it == Entry.FAILURE }
return errorCount > maxErrorsInsideWindow
}
}
That seems readable enough to me. I hope the person who maintains this code in the future agrees with me!
Take small steps
Pseudocode is one way to do it. Unit testing is another approach. My point is that it pays to do careful work in small increments (and take care that your code reads well to the next person who will maintain it).