Being a programmer and working on complex projects we may spend almost as much time on integrations as we spend on debugging. This is a bit counterintuitive for a young programmer so I will try to explain the details.
As programmers we are a part of a bigger team. Each team member gets a block of a larger design to implement, with description of the block and interfaces that must be supported by the block. Ideally we start from the code that tests the interfaces, and slowly build the implementations of the inner block, making our own “developer” tests as we go. At this point we assume some sort of information we need to recieve and to produce and judge our ability to do this. When we are happy with the results on some inputs and outputs we have, we further optimize the code for more speed, less memory usage, and more readable code. Some time along the way, other team members complete their blocks with their inputs and outputs as they understand them. Now the blocks are ready for integration and testing.
The initial integration is typically very simple. All codes implement some interface that all parties agreed on with minimal deviations that are easy to handle. So the whole porject works well on a single example. Now it is ready for testing. Instead of very few inputs used during the development, the code is activated with many parameters and inputs to see how well it performs. Somewhere along the way the code fails to deliver on some of the inputs. Now you need to decide why and in which block the code failed, so that the owner can fix it. How do you do that?
The first tool in your arsenal is the system log. In some mode, the system should be able to print what step and in what module it is performing. If the system failed before getting to some module, then that module did not generate the failure. However, if the system did get to some module, this does not mean that the information did not get corrupted long before.
The second tool you can use are temporary outputs. You can build each block to output its inputs and its outputs and build an additional tool to validate that these inputs and outputs are reasonable.
Some memory and timing issues will generate problems occasionally in a way that is not repeatable. To fix the issue you need tools to validate which part of the system generates the problem. To this end you need to be able to execute the system with different timing schemes and memory schemes (like release vs debug testing).
Even if you can identify the block that causes the issues, it will be further required to dive in into a submodule within the block. The developer of the block will need to perform the same analysis on his own with extra pressure of all team members waiting for him to finish.
Therefore the key to fast and smooth integration is visualization of anything major that could go wrong and constructing ways to isolate the problem as a part of the original code design and implementation.
There are several standard ways to ensure this:
- Detailed and accurate log with several levels of detail or verbosity switched on and off by some parameter.
- Sequencial execution instead of more complex parallel or branching modes.
- Keeping very simple and not optimized code as a foldback for more complex and highly optimized code.
- Ability to bypass large parts of the code.
- Storage of various intermediate results.
- Support of partial code execution from intermediate results.
To implement properly each of these steps you may visualize the whole project (as a mindmap?), and try to go step by step along the process.
At each step along the way you may ask yourself: what may go wrong and how do I fix this?
Then add the code that implements the fix BEFORE anything went wrong. This way when things do go wrong, you are ready to handle them quickly.