9

I'm refactoring a project I did for my work and I'm trying to apply the SOLID principles to make the architecture cleaner. I've run into an issue with the Dependency Inversion Principle that I can't seem to solve.

The Dependency Inversion Principle states that higher level modules shouldn't depend on lower level ones, but both should depend on abstractions. In the context of my program, I have an App class that runs the program, and a class for the MLX90614 infrared Sensor that I'm using to take object and ambient temperatures. In the initial release, the app simply depended directly on the MLX90614 driver class. But this violates the DIP.

//MLX90614.h
class MLX90614
{
    public:
    readTemperature();
}

//app.h
#include "mlx90614.h"
class App
{
   public:
   //do stuff
   private:
   MLX90614 sensor;
}

To fix this, I decided to create an abstract ISensor interface to be consumed by App and implemented by MLX90614.

//sensor.h
class ISensor
{
    public:
    virtual public readTemperature() = 0;
}

class MLX90614 : public ISensor
{
    //implements ISensor
}

//app.h
#include "sensor.h"
class App
{
    private:
    ISensor *sensor;
}

This works to remove the concrete dependency from App, but I find a few problems with this method that no tutorial on DIP seems to cover.

The first is that now MLX90614 has new introduced dependencies. This means that if I ever want to re-use the driver in a completely different application, which is highly likely, I'll have to drag the entire interface system along with it. This seems like a bit of the issue of "wanting a banana but getting the gorilla and the forest with it." If I were to re-use this code and release it as a separate library to the public there would be no use for the application specific ISensor interface, which just seems like bad package design to me.

To make matters worse, many tutorials specifically state that to fully adhere to DIP, the interface must be packaged with the higher level component (this satisfies the second rule). This makes things a whole lot more convoluted and messy, as now MLX90614 is tightly coupled with the higher level App module, and in order to re-use the driver I have to drag not only the interface, but the entire application along with it! This seems like a programming nightmare!

//mlx90614.h
#include "app.h"
class MLX90614 : public ISensor
{
    //Implements ISensor, but now is completely dependent on the App module,
    //which we definitely don't want.
}

//app.h
class ISensor
{
    //do stuff
}

class App
{
    private:
    ISensor *sensor;
}

Because of this, I fail to see how dependency inversion does more good than harm to my system. If I want the lower level modules to be highly re-useable then it seems odd to make them dependent on an entire higher-level system that's completely irrelevant to their implementation. And considering how in most systems I've programmed the lower level libraries are always far more re-useable than the upper level implementations, I fail to see how dependency inversion is useful at all.

4
  • 1
    While I respect you wanting to learn the DIP, I do not know if it is necessary for your application. If you only have one sensor and one app, what do you gain by using interfaces? If you have more than one app or more than one sensor, then this becomes important.
    – Mike
    Mar 29, 2018 at 14:34
  • I'm thinking that in the future the sensor could be replaced with another sensor that uses its own package. Or if I want to test the rest of the application without having a sensor I can create a mock "test sensor" that can be used instead that just returns a random double value for App to use. Mar 29, 2018 at 14:55
  • What I've seen is that interface live in their own package. Take for example Java's JDBC interfaces. They live in the java.sql package. Specific implementations like the PostgreSQL JDBC driver live in their own package, and the app that connects to PostgreSQL using JDBC imports both packages. Mar 23, 2021 at 16:06
  • "I'm thinking that in the future the sensor could be replaced with another sensor" - How sure are you about this? If its a general feeling then I say ignore it under the YAGNI principle. If its an actual requirement that you see in a backlog somewhere, then ask yourself if the new sensor could conform to the interface you have written. If it can, then proceed with the interface, but it its just 'shaped' differently and not compatible w/ the interface then just leave the directly-applied dependencies in there.
    – GHP
    Mar 24, 2021 at 13:24

2 Answers 2

14

I fail to see how dependency inversion is useful at all.

That's because you've failed to use it for anything yet.

You're bemoaning the fact that DIP is work, adds complexity, and does nothing for you. All of which are true at the moment. What you're failing to understand is that DIP isn't about now. It's about change.

You're predicting that using DIP will mean you can't reuse your sensor code without dragging stuff along with it. Here you have the relationship backwards.

You don't create the interface ISensor for MLX90614. You create it for the App that depends on having a sensor and doesn't care if it's MLX90614 or something else. Where before your app knew about MLX90614 directly now it is simply publishing an interface detailing what it needs from it's sensor. Whatever it is. That's a good thing. The app may live longer than MLX90614 does.

When it comes time to use MLX90614 in another app you should use a different interface. One that shows what the other app needs. Not everything MLX90614 does. That's the interface segregation principle.

For some reason you insist that MLX90614 needs to be able to be moved from app to app untouched by the experience. Maybe you're thinking of putting it in a library. If so I'd consider the Object Adapter Pattern to keep from getting it's stubbornness all over my app.

enter image description here

Do what you like with your banana. But the gorilla is the one that decides what it wants to eat and how to peel it.

7
  • Let's say I want to release the code for the MLX90614 as a standalone library for others to use with their devices and projects. I probably don't want to be depending on the application specific interfaces for the standalone library. In that case, would it be better to create the standalone class first, then create an Adapter when it's time to insert it into my application? Or is this adding an unneeded complexity? Mar 29, 2018 at 7:59
  • I would hope that the interface would be just as reusable as the MLX90614 itself. The interface will describe what a sensor does. That should be true whatever application you're using sensors in.
    – Simon B
    Mar 29, 2018 at 8:26
  • 4
    You don't create the interface ISensor for MLX90614. You create it for the App that depends on having a sensor and doesn't care if it's MLX90614 or something else. explains everything. Mar 29, 2018 at 8:54
  • Since MLX90614 is a standalone library and is unaware of the application specific interface(s), I think OP is asking how it can possibly implement said interface(s). Which strikes me as a very good question.
    – user949300
    Mar 29, 2018 at 11:11
  • 1
    @user949300 better? Mar 29, 2018 at 21:35
3

If you want to have ISensor implemented by multiple sensors and consumed by multiple applications, then you have something that looks like this:

enter image description here

At this point, you have two choices:

1) Package ISensor together with Sensor 1 and Sensor 2. All applications consuming one or more sensors now reference that package.

2) Package ISensor by itself. Now all applications and all sensor packages reference the package for ISensor. Applications also must reference any concrete implementations that they want to use.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.