― Blaise Pascal
Sometimes, you need to write a component initialization method that does many things: initialize hardware (e. g. various sensors), multiple libraries, and so on. Usually, some initialization steps are dependent on the successful execution of previous steps, so if one step fails, you want to skip the rest of the initialization procedure to avoid crashes.
Such initialization methods can easily become 100+ lines long, which is far off from the 5 to 10 lines that Uncle Bob and followers of the Clean Code school of thought recommend. That’s why it’s often a good idea to put the initialization steps into methods of their own, each consisting of only 5 to 10 lines of code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool ImageRecognizer::init() { bool ok = false; if (initCameraDriver(...)) { if (initCamera(...)) { if (initComputerVision(...)) { ... ok = true; } } } return ok; } |
While this achieves the primary goal of reducing the length of the initialization method and works if only three steps are involved, a cascade of ten nested if-statements is not at all pleasant to look at. Clean Coders would certainly frown upon it.
Of course, C++ coders (clean or unclean) have a well-known solution for such cases: exceptions. Put a try/catch block around your code and throw exceptions if an initialization step fails:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
bool ImageRecognizer::init() { bool ok = false; try { initCameraDriver(...); initCamera(...); initComputerVision(...); ... ok = true; } catch (const InitializationError& error) { ... } return ok; } |
But maybe you are not allowed to use exceptions in your project. How come? Some embedded folks abhor the idea of exceptions (for many, sometimes unjustified reasons) and even disable support for exceptions via compiler switches. Or, your language doesn’t support exceptions in the first place, like good ol’ C. So what do you do if you want to have cleaner code, anyway?
How about this technique:
1 2 3 4 5 6 7 8 9 10 |
bool ImageRecognizer::init() { bool ok = true; initCameraDriver(..., ok); initCamera(..., ok); initComputerVision(..., ok); ... return ok; } |
This code is definitely shorter and more readable than the original code as well as the exception-based variant. But how does it work?
You simply pass a reference to a boolean flag (or pointer, if you prefer) to every initialization step routine. If the flag is already false, you skip executing the initialization code; otherwise you execute it and only set the flag to false in case of an error:
1 2 3 4 5 6 7 8 9 10 |
ImageRecognizer::initCamera(..., bool& ok) { if (ok) { ... if (/* error occurred */) { ok = false; } } } |
This approach flattens the nested-if cascade by moving the check for the successful execution of a previous step inside the method implementing the next step.
Usually, I try to avoid detailed error codes as much as possible. I prefer binary (boolean) results which only tell if something executed without failure. Should an error occur, I send detailed error information to a logger. If that’s not sufficient to you, you can use an enum instead of a boolean flag:
1 2 3 4 5 6 7 8 |
enum class Result { ok, unspecified, driverFailed, ... }; |
Instead of passing a reference to a boolean flag to you methods, you would pass a reference to an instance of enum Error. The rest of the code should stay mostly the same.