How Software Abstraction Made a Complicated Project More Manageable

My team works with a specific product that is hard to handle for several reasons.  First, it has high visibility with the client because it is a large-volume selling product with worldwide usage.  Secondly, deprecation of an older OS requires a new OS to replace it, along with different hardware.  Older product is already on the field and it still has software updates to support.  Lastly, the application code is very similar between the old and new product.

But wait, shouldn’t similar application code make things easier and not more difficult? Well, yes, but only if it is managed correctly because there are things that simply will not be the same, namely the OS, hardware, and toolchain (compiler, etc.).  The biggest challenge we faced was how do we separate the things that cannot be reused, like those named previously, and the modules that are similar enough that they could be shared.

To illustrate, consider the following pseudocode as a simplified example:

Here, we can see that ProcessOutOfRangeError obtains the feedback current and then determines whether there’s an out-of-range error in the system. If you notice, other than the call to Get_FeedbackCurrent, the ProcessOutOfRangeError function does not use anything specific to the OS, the hardware, or the compiler, so this function is a good candidate to be abstracted out of the drivers file into an application file that can then be shared between as many similar products as we’d like, provided we can agree on what the interface looks like for Get_FeedbackCurrent.  This is, in essence, software abstraction.

Inspecting the pseudocode further, we can determine that Get_FeedbackCurrent serves as an API for this driver, as it provides meaningful information to other modules, such as the feedback current as an actual value with a specific unit, but ConvertRawValueToCurrent need not be an API.  In fact, ConvertRawValueToCurrent should not be an API, but a static/private function instead, because modules outside of this current driver do not need to know how raw data is converted into an actual value.

Utilizing this way of thinking, we identified 8 out of 12 modules that could be trivially shared and the remaining 4 would heavily benefit from abstraction. These remaining 4 could be split into 4 shared modules and 4 much smaller ones on each platform, totaling 20 modules overall for both products.  Note that the original number of modules was 24 (12 on each hardware/OS), so it may look like reducing to 20 is not that big of a deal, but 8 out of the 20 are minimal in size because they were stripped of any code that was not specific to the platform, and another 8 required little to no effort to become shareable.

By creating well-defined interfaces for each module, we can interconnect them in many ways and create scalable architectures and software products.  For us, this meant that what once was two separate repositories was now moved to a single repo, enabling us to reuse application code almost immediately just by separating functionality more carefully. Now that’s how you create a big win and how we made a complicated project much more manageable!