Dependency inversion is one more pattern that is going to help us improve abstraction. We mean that by applying this approach, we separate the definition of an interface from the actual implementation. To explain the pattern, there is a classic example with the light bulb and the switch. If we assume that we want to develop a class-oriented implementation, we need to design a class for the light bulb and a class for the power switch.
#include <iostream> class LightBulb { public: void TurnOn() { std::cout << "Light bulb on..." << std::endl; } void TurnOff() { std::cout << "Light bulb off..." << std::endl; } };
and
#include "light_bulb_plain.h" class ElectricPowerSwitch { public: ElectricPowerSwitch(LightBulb light_bulb) : light_bulb_{light_bulb}, on_{ false} {} void press() { if (on_) { light_bulb_.TurnOff(); on_ = false; } else { light_bulb_.TurnOn(); on_ = true; } } private: LightBulb light_bulb_; bool on_; };
When it comes to the point of the utilisation, it will look like:
#include "electric_power_switch_plain.h" int main() { LightBulb light_bulb{}; ElectricPowerSwitch power_switch{light_bulb}; power_switch.press(); power_switch.press(); return 0; }
The truth is that it works, but is what we want? No, clearly no. The power switch contains a strong dependency on the light bulb, in layman’s terms this switch can only work with the specific bulb. Inside the switch, we call the turn on and turn off methods of the bulb. What if the power switch was independent of the light bulb? What if the light switch was able to turn on and off everything that is switchable, a light bulb, a fan, a coffee machine? Yes, that is the way and for that, we need first to define the switchable abstract class or better interface. In that interface, we set up the so-called contact that every switchable device should adhere. It mush have two methods, the turn on and turn off methods (plus the virtual destructor).
class Switchable { public: virtual void TurnOn() = 0; virtual void TurnOff() = 0; virtual ~Switchable() = default; };
Then our power switch should depend only on that. How are we going to reformat it? Hmm, I think is easy.
#include "switchable.h" class ElectricPowerSwitch { public: ElectricPowerSwitch(Switchable &switchable) : switchable_{switchable}, on_{false} {} void press() { if (on_) { switchable_.TurnOff(); on_ = false; } else { switchable_.TurnOn(); on_ = true; } } private: bool on_; Switchable &switchable_; };
Now, we need to make the light bulb subclass of the switchable interface.
#include "switchable.h" #include <iostream> class LightBulb final : public Switchable { void TurnOn() override { std::cout << "Turn on light bulb" << std::endl; } void TurnOff() override { std::cout << "Turn off light bulb" << std::endl; } };
Now, when it comes to using it, it will look like this:
#include "electric_power_switch.h" #include "light_bulb.h" #include "fan.h" int main() { LightBulb light_bulb{}; Fan fan{}; ElectricPowerSwitch switch1{light_bulb}; switch1.press(); switch1.press(); ElectricPowerSwitch switch2{fan}; switch2.press(); switch2.press(); return 0; }
and you can see, there is also a fan, which is switchable, and our power switch doesn’t have any problem operating on it.
#include "switchable.h" #include <iostream> class Fan final : public Switchable { void TurnOn() override { std::cout << "Turn on fan" << std::endl; } void TurnOff() override { std::cout << "Turn off fan" << std::endl; } };
So using dependency inversion, instead of having classes that are directly coupled, we decouple them using an interface!
As I have said in the past, this series of patterns posts was inspired by Arjan Egges and his great youtube video tutorials about Software Design in Python
References: