Component Engineering for Reuse with Dependency Injection

Michael Antonov
7 min readJun 7, 2019

One aspect of programming I enjoy is building systems with reusable components (be it a VR tracking system, UI or graphics toolkits). When all dependencies are clearly specified and factored out, software can be assembled as if from building blocks — reusing components in different scenarios and OSs without change.

Of course, componentizing isn’t always the right approach — often direct coding to hardware at hand is all that is needed. Abstraction can obfuscate code, so as a rule of thumb it’s good to make sure that it doesn’t add more than 5% of code to the project. Still, today I’ll focus on scenarios where we do have lots of code and it can be made more general with minimum effort.

One pattern which helps reuse is dependency injection. If you’ve specified inputs through an abstract interface to another class — you’ve used it before. Still, I’ve seen good time spent comprehending and refactoring systems that have input dependencies baked in, so wanted to share my thoughts on its benefits.

Component Separation & Reuse

A component is a part of a system that has unique identity and purpose. Components can be as small as individual reusable algorithms (consider search function in STL), or more involved parts of a larger whole:

  • A Video Player App, for example, may be built from a Data streamer, Decoder and Presentation UI components.
  • A Virtual Reality (VR) Tracking System, which tracks a pose of the headset in space, could be built form an image-based Positional Tracker component, a Sensor filter/fusion component, and IMU hardware device abstraction (Here, Sensor filter processes and integrates IMU rotation data with positional tracker poses to a single result).

When describing any software system, one can often break it down into parts drawn as boxes with data flows illustrated by arrows; such parts, however, do not necessarily represent independent components. Instead, component based design manifests itself through interfaces and dependencies in code: when all dependencies go through a clear set of well defined interfaces there is great opportunity for reuse; if, instead, parts access each other and the OS arbitrarily, we get a system that is hard to use in any shape other than a monolithic whole.

So why might we want component separation and reuse? There is a range of reasons:

  • To use them as a library or toolkit — selecting only the parts needed for tailored use cases. This may save memory
  • To supporting alternative implementations for different OSs or platforms
  • To clearly document system boundaries
  • To separate work, enabling different people and teams to work on parts of the system independently

How reusable the component is depends on how its dependencies are managed — so let us consider two possible system designs to illustrate.

Dependency Injection Example

Given my experience in VR, we’ll consider a hypothetical VR Tracking System as an example. Let it consist of two parts:

  • A SensorFilter component, or algorithm that performs IMU filtering and fusion, returning orientation poses based on input, and
  • a DeviceHal, which is a hardware interface to an IMU (inertial measurement unit) that provides packets with rotation data.

After tracking has started, Hal generates IMU packets which are then fed into SensorFilter. Sensor filter integrates them to produce the resulting poses.

Let us consider two possible designs for this system: Design A, where SensorFilter knows about tracking system and calls on it directly to get the IMU packets.

Alternatively, there is Design B, where we still have both components but SensorFilter does not know about DeviceHal specifically. Instead, it relies on an interface that it exposes for feeding the packets in.

It is the job of the user to connect the two, resulting is a usable system.

Which one is more flexible and provides more opportunities for code reuse?

Design A has one benefit: it appears to be easier to use. Users of SensorFilter do not need to know what DeviceHal is — they simply create SensorFilter object and get poses, everything working underneath. Arguably this is a minor win, as similar ease could also be achieved with a wrapper of Design B.

However, what happens if we want to use the SensorFilter with a different implementation of IMU, perhaps on a different OS? Or use it for different inputs simultaneously? In a strict sense of a word we can’t — our design is fixed to one implementation. In C/C++ we can circumvent OS issue by linking in a different implementation; however, it is a detail that the compiler is not aware of.

Design A: Dependencies Not Clearly Defined

One key challenge with Design A is that SensorFilter dependencies are not described explicitly. To understand why this is not ideal, let us consider an example with a more complex hypothetical headset — in addition of head tracking, such DeviceHal abstraction could include eye tracking support, status LED control, plus firmware update methods:

  • getIMUPacket — Returns IMU rotation data
  • setTrackingSampleRate — To configure the rate that IMU runs
  • setStatusLED — To control the LEDs on the device
  • getEyeTrackerImages — To report eye camera bitmaps
  • updateFirmware — To update internal firmware

The issue is that most of these methods, besides the first one, are not necessary for SensorFilter code!.. yet it is not obvious that they are not used. In fact, the only way to know would be to scan SensorFilter source code for the dependency. Furthermore, even if you found out that the reference is not there, you wouldn’t be sure that will be so in the next revision.

This means that if we are implementing new DeviceHal to support SensorFilter, we have to implement all these methods. Furthermore, we still have no way to support multiple Hal types simultaneously, such as separate Hal for controllers.

Design B: Dependency Injection

Let us consider how Design B addresses this:

  1. It provides an input interface (illustrated by a circle) that specifies exactly what SensorFilter needs — and nothing else. Methods such as getStatusLED or getEyeTrackerImages would have no place here.
  2. It leaves it to end user code to set up the binding to the DeviceHal.

Both of these elements together add a lot of re-usability to SensorFilter:

  • Users are free to implement simple DeviceHal analogs with minimum effort because they know exactly what SensorFilter needs
  • They can create multiple implementations of DeviceHal that work together, providing support for simultaneous tracking of multiple devices, such as controllers, that have different HW implementations.

There is a name for this pattern: Dependency injection.

From Wikipedia: “ In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state.[1] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

While I have a certain level of allergy to design patterns (I’ve seen them abused and introduce complexity), the concept of clearly specifying dependencies through an interface is both trivial and sound. I suggest that you consider this one when building a reusable system past a certain level of complexity.

Besides reuse, there is a significant added benefit for dependency injection — it forces us to clearly document interfaces, which is a big plus for understandable design. It also puts compiler on your side, helping ensure that the right methods are implemented.

Code Sample

In case some of the diagrams weren’t clear, let me illustrate the concept in C++ code. Here, we assume existence of a DeviceHal class implements the getImuPacket function.

class SensorFilterInput {
public:
virtual bool getImuPacket(const IMUPacket&) = 0;
}
class SensorFilter {
public:
SensorFilter(SensorFilterInput*) {}
// return current pose.
Pose getPose();
};
// --------------------------------------------------------
// User-side implementation of the interface above
class SensorFilterInputImpl : public SensorFilterInput {
DeviceHal* pDeviceHal;
public:
SensorFilterInputImpl(DeviceHal *hal) : pDeviceHal(hal) {}
virtual bool getImuPacket(const IMUPacket& packet) {
return pDeviceHal->getImuPacket(packet);
}
}
int main() {
DeviceHal hal;
SensorFilterInputImpl input(&hal);
SensorFilter sf(&input);
...
while (!quit) {
Pose pose = sf.getPose();
// ... do work
}
}

Here, SensorFilter implementation relies on SensorFilterInput to receive input instead of direct access to the Hal; the input data piping is instead configured by the user code in main. This allows SensorFilter to potentially work with different Hals and offers other benefits described earlier.

Reuse Principles

1. Expose component’s dependencies through interfaces that include the absolute minimum functionality required, and are ideally custom to the component at hand — this is the key guiding principle.

2. Let the higher level application code (or end developer) combine the system out of the smaller component blocks, wiring up dependencies and necessary customization. Keep such dependencies out of the components themselves.

3. When in doubt, avoid combining methods needed by different components in into one shared interface.

4. Use the “5% rule” to combat any abstraction complexity when it’s not needed. Abstraction shouldn’t be adding more than 5% of total code. This is a constraint on all of the above — to keep things simple and avoid over-design.

For C++, there are a couple of extra guidelines I suggest:

  • Avoid factories that instantiate things dynamically, unless such dynamic lookup is truly, really needed — let compiler and linker pull in the dependencies based on user references instead. Factories have become popular over the years, yet I’ve seen many cases where it would’ve been clearer to directly instantiate the target objects instead.
  • Consider placing larger subsystems code, say over 10K lines, into dedicated namespaces along with their dependency interfaces, plus avoid the “using” clause. This will make undesired dependencies easier to spot, as they will have to explicitly reference the relevant namespace.

There are no black-and-whites in software, yet following these rules will move your code significantly towards reuse.

--

--

Michael Antonov

Investor @ formic.vc, software architect, serial entrepreneur and co-founder of Oculus. Interested in biotechnology and hardest challenges facing humanity.