C++ dependency inversion for system components

Viewed 77

My understanding of the principle until now was quite superficial - "High level modules should not depend on low level modules. All should depend on interfaces." This is present in many books by example where a Button class that is responsible enabling/disabling Lamp should not be depend on it. Instead of using concrete Lamp the Button should provide an interface that Lamp implements. According to one of Martin's books the interface should be exposed by high level class/module and implemented by low level modules. As I can entirely imagine using that principle among a library however applying that to larger system components seem cumbersome. Let's consider following example of wrong design:

----------Library for group of sensors for certain Vendor, let's call it 'sx' that exposes bunch of header files and dynamically linked library:

// SX4000.hpp
class SX4000{
  public:
    uint32_t getTemperature() const;
    uint32_t getBatteryLevel() const;
};

// libsx.so

----------Application using the sensor
// Display.cpp
#include "SX4000.hpp"

void DisplayTemperatureSensorDetails(const SX4000& sensor)
{
    auto temperature = sensor.GetTemperature();
    auto batteryLevel = sensor.GetBatteryLevel();
    // Logic sending values to the screen
    // ...
}

Then the design with suggested approach would be:

// Application using the sensor
// Measurable.hpp
class Measurable
{
  public:
    virtual uint32_t GetTemperature() const = 0;
    virtual uint32_t GetBatteryLevel() const = 0;
};


// Display.cpp
void DisplayTemperatureSensorDetails(const Measurable& sensor)
{
    auto temperature = sensor.GetTemperature();
    auto batteryLevel = sensor.GetBatteryLevel();
    // Logic sending values to the screen
    // ...
}

// Sensor library:
// SX4000.hpp
class SX4000 : public Measurable{
  public:
    uint32_t getTemperature() const;
    uint32_t getBatteryLevel() const;
};
// libsx.so

This however seems like violation of one of basic rules of object oriented programming. If there is low level library why it should be adjusted to where it's used? What if Sensor library have to be used by other component of the system? Does it mean that on every usage there should be new interface added to the Sensor library? I see two solutions for that problem:

  • Sensor library exposes Measurable interface instead of Application
  • Application creates a adapter/wrapper class that implements Measureable and it's injected at entrypoint

Which solution is better, are there any other alternatives?

EDIT. Just to clarify some points that I may have put in wrong way. I totally understand all benefits of polymorphisim in general and I don't question that approach. The only concern I have is about inversion of dependency among libraries with fairly established interfaces. There is high chance of situation in large system that significant part of all low level libraries have already well established interfaces. Then according to that rule I write a component on top of that exposing an interface to the services. Then I'am in a point when I have to update such well established interface of low level library with an abstraction. I'd assume that in such situation the best way would be to stick with such well-established interface of low level library and put an adapter in higher level library. What are prons and cons of such approach? Any other alternatives?

1 Answers

I think you would need to find whether SX4000 is the only Measurable object first.

For instance, if you have another unit, let's say SX5000, which is also a Measurable. When you have the Measurable interface, if you have a function that accept a Measurable, it could accept both SX4000 and SX5000.

Even more, when you wrote that function, you might not know there will be another Measurable object. By doing a interface, your function would be accepting a Measurable object, so that function can automatically accept the new object type without the need of changing any codes for that function.

On the other hand, SX4000 might not only be a Measurable object. It might also be, let's say, a Switch object. By creating SX4000 inheriting both Measurable and Switch, you can get information from both interfaces easily.

Related