Принцип «Инверсия зависимостей» на современном C++
Инверсия зависимостей – еще один принцип из SOLID, который поможет нам улучшить абстракцию. Применяя этот подход, мы отделяем определение интерфейса от фактической реализации. Чтобы объяснить этот паттерн, приведем классический пример с лампочкой и выключателем. Если предположим, что мы хотим разработать реализацию, ориентированную на классы, нам нужно разработать класс для лампочки и класс для выключателя.
#include <iostream>
class LightBulb
{
public:
void TurnOn()
{
std::cout << "Light bulb on..." << std::endl;
}
void TurnOff()
{
std::cout << "Light bulb off..." << std::endl;
}
};
и
#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_;
};
Когда дело доходит до использования, это будет выглядеть так:
#include "electric_power_switch_plain.h"
int main()
{
LightBulb light_bulb{};
ElectricPowerSwitch power_switch{light_bulb};
power_switch.press();
power_switch.press();
return 0;
}
Всё работает, но это то, что нам нужно? Нет, явно нет. Выключатель сильно зависит от лампочки; с точки зрения непрофессионала, этот выключатель может работать только с конкретной лампочкой. Внутри выключателя мы вызываем методы включения и выключения лампочки. Что, если бы выключатель не зависел от лампочки? Что, если бы выключатель мог включать и выключать всё, что можно включить/выключить: лампочку, вентилятор, кофемашину? А для этого нам нужно сначала определить абстрактный класс включаемого/выключаемого устройства или, лучше, интерфейс. В этом интерфейсе мы указываем так называемые связи, которых должно придерживаться каждое выключаемое устройство. У него есть два метода: включение и выключение (плюс виртуальный деструктор).
class Switchable
{
public:
virtual void TurnOn() = 0;
virtual void TurnOff() = 0;
virtual ~Switchable() = default;
};
Наш выключатель должен зависеть только от него. Как же изменить класс выключателя? Легко.
#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_;
};
Теперь нам нужно сделать лампочку подклассом интерфейса Switchable
.
#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;
}
};
Теперь, когда дело доходит до использования, это будет выглядеть так:
#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;
}
И вы можете увидеть, что есть также вентилятор, который можно включать, и у нашего класса выключателя нет никаких проблем с управлением им.
#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;
}
};
Таким образом, используя инверсию зависимостей, вместо того, чтобы иметь классы, которые напрямую связаны, мы разделяем их с помощью интерфейса!