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?