Tired of outsourcing? Get hired at a top product startup from Silicon Valley 🚀
×Закрыть

Обзор С++ фреймворков для внедрения зависимостей: kangaru и [Boost].DI

Добрый день, уважаемые читатели! Меня зовут Кирилл Пшеничный. Я разработчик C++ в TeamDev. Основной моей задачей является разработка С++ библиотеки для интеграции с open-source проектом. В ходе данного процесса используются различные подходы: от использования функций обратного вызова (callbacks) до межпроцессорного взаимодействия (IPC). В данной статье я хотел бы рассказать о парадигме под названием Dependency Injection, которая призвана, в частности, упрощать взаимодействие и связывание различных частей системы.

Подход Dependency Injection позволяет сделать архитектуру бизнес-приложения гибкой и расширяемой. Этот подход широко известен, и каждый язык имеет множество фреймворков для реализации такого архитектурного решения. Например, Dagger и соответствующие компоненты во фреймворке Spring для Java, Microsoft Unity Framework для C#, Python Inject для Python. Язык С++ не является исключением и имеет ряд библиотек для внедрения зависимостей.

В этой статье я расскажу о специфике использования двух DI-фреймворков — kangaru и [Boost].DI — на примере несложной системы банковского учета.

Семантика Inversion of Control, Dependency Injection, Dependency Container

Основная идея инверсии управления (Inversion of Control, далее — IoC) заключается в том, что логику связывания и вызова различных компонентов системы программист вызывает не напрямую, а посредством использования IoC-контейнера. К такой логике, например, относятся создание объектов, логирование, кеширование, обработка исключений и вызовы доменных операций. В отличие от классического подхода, в котором программист полностью контролирует все вызовы методов и функций, IoC позволяет делегировать выполнение части бизнес-логики third-party-фреймворку.

Подход преследует такие цели:

  • увеличение гибкости системы;
  • повышение атомарности модулей и сущностей;
  • упрощение дальнейшего процесса замены отдельных модулей.

Внедрение зависимостей (Dependency injection, далее — DI) является одним из способов реализации IoC-подхода. Основной принцип DI — выстраивание отношения client — service. Client — это определенный компонент системы (метод, модуль, сущность), которому для реализации логики нужен сторонний компонент — service. При этом client не ищет и не создает необходимый компонент, а получает его извне, из контейнера зависимостей (Dependency Container, далее — DC).

Ниже приведен простой пример двух сущностей. Client завязан на использование определенного функционала Service. Для решения задачи Client сам создает необходимый объект и полностью управляет его жизненным циклом:

struct Service {
       void doSmth() {
          
       }
};

class Client {
      
 Client() : service_(std::make_unique<Service>()) {}     
      
 void delegate() {
     service_->doSmth();
 } 

 std::unique_ptr<Service> service_;

};

Тот же код с использованием DI:

struct Service {
       void doSmth() {
       }
};

class Client {

 Client(Service* service) : service_(service) {}

 void delegate() {}

 Service* service_;

};

Отличие между этими двумя подходами заключается в следующем:

  • Client не создает Service, а получает его извне;
  • Client не контролирует жизненный цикл Service, а только использует его API.

Какие преимущества дает этот подход?

  • уменьшение количества boilerplate code, связанного с конструированием объектов;
  • упрощение процесса тестирования путем замены реальных объектов (драйверов БД, сетевых соединений) «заглушками» (stubs) и mock-объектами;
  • возможность независимой и параллельной разработки разных компонентов системы — достаточно знать только интерфейсы;
  • реализация различных сервисов в зависимости от конфигураций.

Демонстрационный проект

Для практической демонстрации возможностей рассматриваемых фреймворков была разработана несложная программа, моделирующая банковскую систему. Исходный код находится здесь. Для сборки вам понадобится библиотека boost и компилятор C++ с поддержкой стандарта С++14. В репозитории две ветки: master содержит реализацию DI с помощью kangaru, di_dependency использует [Boost].DI.

Система поддерживает три типа банковских депозитов:

  • SavingsDeposit — депозит с низкой процентной ставкой и постоянным обязательным наличием минимальной суммы на счете;
  • FixedDeposit — депозит, процентная ставка которого растет с течением времени; для него запрещены операции пополнения и снятия средств;
  • CurrentDeposit — депозит с нулевой процентной ставкой и овердрафт-лимитом.

Клиенты не взаимодействуют с моделью напрямую, а «общаются» с ней посредством сервисов AccountService и DepositService. Взаимодействие со слоем данных происходит через соответствующие интерфейсы репозиториев — AccountRepository и DepositRepository.

И здесь в процесс включается DI. Работая с абстракциями, мы в любой момент можем заменить имплементацию того или иного компонента. Например, во время тестирования можно использовать репозиторий, который управляет предопределенным набором данных, в продакшене — репозиторий для управления реальной базой данных; можно использовать локальную реализацию сервисов при тестировании, gRPC — в продакшене.

На UML-диаграмме показаны сервисы и их зависимости.

kangaru DI Framework

Для внедрения зависимостей с простым и гибким API используют фреймворк kangaru. Структурно он представляет собой набор заголовочных файлов, его функциональность построена на механизме шаблонов. Фреймворк kangaru поддерживает внедрение зависимостей в конструкторы, методы, функции и сеттеры.

Объекты, которыми управляет kangaru, называются сервисами. Они представляют собой обертки для типов, которые участвуют в процессе DI. Такими типами могут быть конкретные классы, абстракции, интерфейсы.

Для применения пользовательских структур в дальнейшем процессе DI необходимо для каждой структуры создать класс сервиса и наследовать его от библиотечного шаблона kgr::service<>. На практике это выглядит следующим образом:

struct Delegate {
       void sayHello() {
           std::cout << "Hello\n";
       }
};


class Client {
   public:
       explicit Client(Delegate delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate delegate_;

};

struct DelegateService : kgr::service<Delegate> {};
struct ClientService : kgr::service<Client, kgr::dependency<DelegateService>> {};

Здесь у нас два сервиса: DelegateService и ClientService. Зависимость между сущностями в модели представлена в сервисе с помощью шаблонного класса kgr::dependency. В качестве параметров шаблона следует передавать сервисы, которые будут создавать нужные объекты.

Создание нужных объектов происходит в объекте kgr::container:

int main() {
   kgr::container container;
   Client client = container.service<ClientService>();
   client.hello();
   return 0;
}

По умолчанию kgr::service создает новый объект каждый раз при запросе. Если вам нужны глобальные объекты, используйте kgr::single_service.

Вернемся к нашей банковской системе. Как было сказано ранее, нам хотелось бы иметь множество имплементаций, которые мы могли бы использовать в разных конфигурациях. Для декларации сервисов полиморфных объектов kangaru предлагает механизм полиморфных сервисов. Наследование от типа kgr::polymorphic при декларации сервиса сообщает фреймворку, что в дальнейшем такой сервис может быть замещен:

struct Delegate {
       virtual ~Delegate() = default;
       virtual void sayHello() {
           std::cout << "Hello\n";
       }
};

struct PoliteDelegate : public Delegate {
   void sayHello() override {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

class Client {
   public:
       explicit Client(Delegate& delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate& delegate_;

};

struct DelegateService : kgr::single_service<Delegate>, kgr::polymorphic {};
struct PoliteDelegateService :
 kgr::single_service<PoliteDelegate>, 
 kgr::overrides<DelegateService> {};

struct ClientService : kgr::service<Client, kgr::dependency<DelegateService>> {};

Оверрайд сервиса происходит путем наследования от шаблонного типа kgr::overrides. В качестве аргумента шаблона передаем родительский сервис.

В нашем случае клиент оперирует только интерфейсами; для такой задачи предусмотрен механизм абстрактных сервисов. Абстрактные сервисы — это такие сервисы, которые должны быть обязательно заоверрайдены.

Интерфейс Bootstrapper представляет собой центр получения сервисов системы. Именно здесь мы и внедрим наши абстрактные сервисы.

Имплементации Bootstrapper заполнят контейнер зависимостей конкретными реализациями сервисов:

struct AccountRepositoryService : kgr::abstract_service<Repository::AccountRepository> {};
struct DepositRepositoryService : kgr::abstract_service<Repository::DepositRepository> {};

struct AccountServiceService : kgr::abstract_service<Service::AccountService> {};
struct DepositServiceService : kgr::abstract_service<Service::DepositService> {};

class Bootstrapper : public boost::noncopyable {
   public:
       virtual ~Bootstrapper() = default;

       Service::DepositService& getDepositService() {
           return container_.service<DepositServiceService>();
       }

       Service::AccountService& getAccountService() {
           return container_.service<AccountServiceService>();
       }

   private:
       virtual void initRepos() = 0;

       virtual void initServices() = 0;

   protected:
       kgr::container container_;
};

Класс TestContainerBootstrapper представляет тестовую конфигурацию системы. В качестве репозиториев в такой конфигурации используется локальный набор данных, в качестве сервисов — локальные имплементации.

Декларируем конкретные сервисы с определенными имплементациями:

struct LocalAccountRepositoryService :
       kgr::single_service<Repository::LocalRepo::AccountRepositoryImpl>,
       kgr::overrides<DP::AccountRepositoryService> {};

struct LocalDepositRepositoryService :
       kgr::single_service<Repository::LocalRepo::DepositRepositoryImpl>,
       kgr::overrides<DP::DepositRepositoryService> {};

struct LocalAccountServiceService :
       kgr::single_service<Service::Impl::AccountServiceImpl, kgr::dependency<DP::AccountRepositoryService>>,
       kgr::overrides<DP::AccountServiceService> {};

struct LocalDepositServiceService :
       kgr::single_service<Service::Impl::DepositServiceImpl, kgr::dependency<DP::DepositRepositoryService, DP::AccountRepositoryService>>,
       kgr::overrides<DP::DepositServiceService> {};

Затем просто добавляем наши сервисы в контейнер:

void TestContainerBootstrapper::initRepos() {
   container_.emplace<LocalAccountRepositoryService>();
   container_.emplace<LocalDepositRepositoryService>();
}

void TestContainerBootstrapper::initServices() {
   container_.emplace<LocalAccountServiceService>();
   container_.emplace<LocalDepositServiceService>();
}

Важно! В первую очередь следует добавлять в контейнер сервисы без зависимостей (в нашем случае это сервисы репозиториев). Неправильный порядок вызовет исключение при запросе к контейнеру.

Проверим работоспособность с помощью библиотеки Catch2. Тесты должны покрыть следующую функциональность:

  • корректное создание объектов;
  • выброс исключений при подаче некорректных данных (пустые строки, отрицательные и нулевые значения при создании объектов и операциях с ними);
  • выброс исключений предметной области (нарушения семантики операций пополнения и снятия с депозитов для определенных типов депозитов).

Вызов функциональных объектов

Помимо внедрения зависимостей в конструкторы kangaru предоставляет возможность вызова функциональных объектов (функций, лямбда-выражений, методов) с помощью контейнера. Делается это путем вызова шаблонного метода invoke у объекта контейнера. В качестве параметров метод принимает функциональный объект и его аргументы. Аргументами шаблона следует сделать классы необходимых сервисов. На практике это выглядит следующим образом:

struct Delegate {
   virtual void sayHello() {
       std::cout << "Hello\n";
   }
};

struct PoliteDelegate : Delegate {
   void sayHello() override {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

struct DelegateService : kgr::single_service<Delegate>, kgr::polymorphic {};
struct PoliteDelegateService : kgr::single_service<PoliteDelegate>, kgr::overrides<DelegateService> {};

void sayHello(Delegate& delegate) {
   delegate.sayHello();
}

int main() {
   kgr::container container;
   container.invoke<PoliteDelegateService>(sayHello);
   return 0;
}

Функция sayHello ожидает в качестве параметра объект Delegate. Контейнер автоматически инстанциирует нужные объекты и вызовет функциональный объект.

Для упрощения вызовов kangaru предлагает синтаксис отображения сервисов (Map Service). Он позволяет указать, какой сервис необходимо использовать при запросе того или иного аргумента. Выглядит это так:

auto service_map(<parameter>) -> <definition>;
parameter — тип аргумента, definition — соответствующий сервис.

Такой синтаксис позволяет опускать явное указание параметров шаблона при вызове метода invoke.

Важно! Внедрение service_map должно быть в том же пространстве имен, что и аргументы отображения. Иначе будет получена ошибка компиляции.

Теперь давайте позволим клиентам класса Bootstrapper вызывать произвольные функциональные объекты с произвольными сервисами домена.

Внедрим service_map и соответствующий метод:

namespace Service {
   struct AccountService;
   struct DepositService;
   auto service_map(Service::AccountService&) -> Dependency::AccountServiceService;
   auto service_map(Service::DepositService&) -> Dependency::DepositServiceService;
}
template <typename Callable>
void invoke(Callable callback) {
   container_.invoke(callback);
}

Теперь мы можем вызывать произвольные функциональные объекты, указывая в аргументах желаемые сервисы. Контейнер автоматически вызовет функтор с нужными сервисами:

bootstrapper.invoke(
       [](Service::AccountService& accounts, Service::DepositService& deposits){
           REQUIRE(accounts.getAccountsAmount() == deposits.getDepositsAmount());
       })

Альтернативный способ описания зависимостей

Напоследок я хотел бы показать альтернативный синтаксис внедрения зависимостей, который называется autowire. Обычно мы внедряем сервисы, явно декларируя зависимости посредством шаблонного типа kgr::dependency:

struct DelegateService : kgr::single_service<Delegate> {};

struct ClientService : kgr::service<Client, kgr::dependency<DelegateService>> {};

При большом количестве зависимостей снижается читаемость; autowire же позволяет опустить явное указание зависимых сервисов. Для этого, как и в случае с invoke, нужно задекларировать ассоциацию между типом и соответствующим сервисом:

auto service_map(Delegate const&) -> DelegateService;

Затем просто используем тип kgr::autowire вместо kgr::dependency:

struct Delegate {
   void sayHello() {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

struct DelegateService : kgr::single_service<Delegate> {};

auto service_map(Delegate&) -> DelegateService;

int main() {
   kgr::container container;

   container.invoke(
           [](Delegate& delegate){
               delegate.sayHello();
           });

   return 0;

Эту форму можно еще упростить, используя автогенерацию сервисов с помощью псевдонимов. В нашем случае контейнер сам сгенерирует singleton-сервис DelegateService из декларации service_map:

#include <kangaru/kangaru.hpp>
#include <iostream>

struct Delegate {
   void sayHello() {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

auto service_map(Delegate&) -> kgr::single_service<Delegate>;

int main() {
   kgr::container container;

   container.invoke(
           [](Delegate& delegate){
               delegate.sayHello();
           });

   return 0;
}

Другие полезные псевдонимы:

ПсевдонимГенерируемый сервис
kgr::autowire_service<T>kgr::service<T, kgr::autowire>
kgr::autowire_single_service<T>kgr::single_service<T, kgr::autowire>
kgr::autowire_unique_service<T>kgr::unique_service<T, kgr::autowire>
kgr::autowire_shared_service<T>kgr::shared_service<T, kgr::autowire>

[Boost].DI

Один мой коллега говорил: «Если чего-то нет в STL, посмотри в boost». Конечно, boost предоставляет нам фреймворк для эффективного внедрения зависимостей, но пока он не поставляется с официальной сборкой. Как и kangaru, [Boost].DI — header-only-библиотека, и, как утверждают ее авторы, она довольно быстрая по сравнению с аналогичными библиотеками. К особенностям [Boost].DI можно также отнести:

  • возможность явного контроля продолжительности жизни объектов, создаваемых контейнером зависимостей (см. Scopes);
  • управление поведением контейнера, например выбор места аллокации объектов (стек или куча, см. Providers);
  • использование ограничений при наполнении контейнера (см. Concepts).

Как и в случае с kangaru, центральным объектом в [Boost].DI является контейнер зависимостей. В [Boost].DI он называется injector. В отличие от kangaru, его использование лаконичнее: никаких дополнительных деклараций и аннотаций для описания зависимостей не нужно. Базовый пример, описывающий зависимость объекта Client на Delegate, будет выглядеть так:

struct Delegate {
   void sayHello() {
       std::cout << "Hello\n";
   }
};

class Client {
   public:
       explicit Client(Delegate delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate delegate_;
};

int main() {
   auto client = boost::di::make_injector().create<Client>();
   client.hello();
   return 0;
}

Важно! Объект Client будет создан на стеке, а затем скопирован. Из этого следует, что для объектов с нетривиальной логикой копирования следует явно определять конструкторы копий, операторы присвоения и перемещения.


struct Delegate {
   void sayHello() {
       std::cout << "Hello\n";
   }

   explicit Delegate(int size) : size_(size), data_(new int[size_]) {
   }

   ~Delegate() {
       delete [] data_;
   }

   Delegate(Delegate& other) : size_(other.size_) {
       memcpy(data_, other.data_, size_);
       std::cout << "copy constructed\n";
   }

   Delegate& operator= (const Delegate& other) {
       if (this == &other) {
           return *this;
       }

       delete [] data_;

       data_ = new int [other.size_];
       memcpy(data_, other.data_, size_);
       size_ = other.size_;

       std::cout << "copy assigned\n";
       return *this;
   }

   Delegate(Delegate&& other) noexcept {
       std::swap(other.data_, data_);
       size_ = other.size_;

       std::cout << "moved constructed\n";
   }

   Delegate& operator= (Delegate&& other) noexcept {
       if (this == &other) {
           return *this;
       }
       std::swap(other.data_, data_);
       size_ = other.size_;

       std::cout << "moved assigned\n";

       return *this;
   }

   int size_ = 0;
   int* data_ = nullptr;
};

class Client {
   public:
       explicit Client(Delegate delegate) : delegate_(std::move(delegate)) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate delegate_;
};

int main() {
   auto client = boost::di::make_injector(boost::di::bind<int>().to(10)).create<Client>();
   client.hello();
   return 0;

Привязки интерфейсов к реализациям

Любое взаимодействие с системой должно происходить через интерфейсы (AccountService, DepositService, AccountRepository, DepositRepository). Контейнер должен оперировать имплементациями данных интерфейсов. Привязка интерфейсов к конкретным имплементациям делается в момент конструирования контейнера с помощью шаблонной функции bind следующим образом:

struct Delegate {
   virtual ~Delegate() = default;
   virtual void sayHello() = 0;
};

struct PoliteDelegate : public Delegate {
   void sayHello() override
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

class Client {
   public:
       explicit Client(Delegate& delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate& delegate_;
};

int main() {
   using namespace boost;
   auto client = di::make_injector(di::bind<Delegate>.to<PoliteDelegate>()).create<Client>();
   client.hello();
   return 0;
}

Время жизни объектов

В [Boost].DI жизненный цикл объектов, создаваемых фреймворком, контролируют специальные классы — scopes. Существует 4 вида scope:

  • instance scope — означает использование объектов, которые были переданы в контейнер извне, их продолжительность жизни контролирует пользователь;
  • unique scope — при каждом запросе будет создаваться новый объект;
  • singleton scope — разделяемый глобальный инстанс объекта на весь жизненный цикл приложения;
  • deduce scope — один из вышеописанных вариантов, который выберет фреймворк.

Каждый раз при вызове метода create инжектор создает необходимые объекты, исходя из типа параметров создаваемого объекта. По умолчанию контейнер использует deduce scope, который имеет следующую политику (Object type — тип требуемого объекта, Scope — время жизни созданного объекта):

Object typeScope
Tunique
T&singleton
const T&singleton
T*unique
const T*unique
T&&unique
std::unique_ptrunique
std::shared_ptrsingleton
boost::shared_ptrsingleton
std::weak_ptrsingleton
class scopes_deduction {
   scopes_deduction(const int& /*singleton scope*/,
                    std::shared_ptr<int> /*singleton scope*/,
                    std::unique_ptr<int> /*unique scope*/,
                    int /*unique scope*/)
   {}
};

Кроме того можно явно указывать желаемый scope при создании контейнера вызовом метода in:

injector = di::make_injector(di::bind<Delegate>.to<Delegate>().in(di::singleton));

При этом нужно понимать, что указание scope не может нарушать семантических правил языка. Например, нельзя указывать instance scope, если клиент требует ссылки на объект: это вызовет ошибку компилятора. Это связано с тем, что инжектор создаст временный объект, который нельзя передавать в качестве ссылки.

struct Delegate {
   void sayHello() {
       std::cout << "Hello\n";
   }
};

class Client {
   public:
       explicit Client(Delegate& delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate& delegate_;
};

int main() {
   namespace di = boost::di;

   auto injector = di::make_injector(di::bind<Delegate>.to<Delegate>().in(di::unique));


   // Error: cannot bind temporary object to a reference
   auto client = injector.create<Client>();
   client.hello();
   return 0;
}

Также нужно помнить о том, кто владеет объектом и кто его будет удалять. В первую очередь это касается объектов с unique scope и instance scope:

struct Delegate {

   ~Delegate() {
       std::cout << "~Delegate\n";
   }

   void sayHello() {
       std::cout << "Hello\n";
   }
};

class Client {
   public:
       explicit Client(Delegate* delegate) : delegate_(delegate) {}

       ~Client() {
           // Objects with unique scope must be deleted explicitly
           delete delegate_;
       }

       void hello() {
           delegate_->sayHello();
       }

   private:
       Delegate* delegate_;
};

int main() {
   namespace di = boost::di;
  
   auto injector = di::make_injector(di::bind<Delegate>.to<Delegate>());
auto client = injector.create<Client>();
   client.hello();
   return 0;
}

Контейнер для тестовой конфигурации нашей системы будет иметь следующий вид:

using namespace boost;
auto injector = di::make_injector(
       di::bind<Repository::AccountRepository>.to<Repository::LocalRepo::AccountRepositoryImpl>(),
       di::bind<Repository::DepositRepository>.to<Repository::LocalRepo::DepositRepositoryImpl>(),
       di::bind<Service::AccountService>.to<Service::Impl::AccountServiceImpl>(),
       di::bind<Service::DepositService>.to<Service::Impl::DepositServiceImpl>());

Затем просто создаем объект TestContainerBootstrapper, который имеет зависимость в своем конструкторе от сервисов и репозиториев домена:

TestContainerBootstrapper::TestContainerBootstrapper(
       Service::AccountService& accountService,
       Service::DepositService& depositService,
       Repository::DepositRepository& depositRepository,
       Repository::AccountRepository& accountRepository);

injector.create<TestContainerBootstrapper>();

Динамическое создание требуемых объектов

У [Boost].DI есть интересная возможность — динамическое создание необходимых объектов (run-time) во время запросов к контейнеру. Делается это посредством вызова функции make_injector с лямбда-выражением, которое будет вызываться фреймворком для создания запрашиваемых объектов.:

struct Delegate {
   virtual ~Delegate() = default;

   virtual void sayHello() = 0;
};

struct PoliteDelegate : public Delegate {
   void sayHello() override {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

struct EveningDelegate : public Delegate {
   void sayHello() override {
       std::cout << "Good evening, ladies and gentlemen\n";
   }
};

class Client {
   public:
       explicit Client(Delegate& delegate) : delegate_(delegate) {}
       void hello() {
           delegate_.sayHello();
       }

   private:
       Delegate& delegate_;
};

int main() {
   using namespace boost;
   bool is_evening = false;

   auto injector = di::make_injector(
           di::bind<Delegate>().to([&](const auto& injector) -> Delegate& {
               if (is_evening)
                   return injector.template create<PoliteDelegate&>();
               else
                   return injector.template create<EveningDelegate&>();
           })
   );

   injector.create<Client>().hello();
   return 0;
}

Другие типы зависимостей

Помимо зависимостей к пользовательским типам [Boost].DI позволяет создавать зависимости к фундаментальным типам (int, float, double, char):

struct Client {
   Client(int i, double j, char c) { std::cout << i << ' ' << j << ' ' << ' ' << c; }
};

int main() {
   auto injector = boost::di::make_injector(
           boost::di::bind<int>.to(42),
           boost::di::bind<double>.to(3.14),
           boost::di::bind<char>.to('a'));

   injector.create<Client>();

   return 0;
}

Можно создавать зависимости к уже существующим объектам. Это может быть полезным при работе с third-party-библиотеками в случаях, когда логика создания объектов скрыта от пользователя:

struct Delegate {
   void sayHello() {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};

struct Client {
   explicit Client(Delegate& delegate) : delegate_(delegate) {}
   void hello() {
       delegate_.sayHello();
   }

   Delegate& delegate_;
};

int main() {

   Delegate global_instance;

   auto injector = boost::di::make_injector(boost::di::bind<Delegate>.to(global_instance));

   Client first = injector.create<Client>();
   Client second = injector.create<Client>();

   assert(&first.delegate_ == &second.delegate_);

   return 0;
}

Возможно даже указывать зависимости к аргументам шаблонных классов, что является весьма полезным при шаблонном метапрограммировании:

struct Delegate {
   void sayHello() {
       std::cout << "Hello\n";
   }
};

struct PoliteDelegate {
   void sayHello() {
       std::cout << "Hello, ladies and gentlemen\n";
   }
};


template <typename T = class TDelegate>
struct Client {
   void hello() {
       T delegate;
       delegate.sayHello();
   }
};

int main() {

   auto injector = boost::di::make_injector(boost::di::bind<class TDelegate>.to<PoliteDelegate>());
   injector.create<Client>().hello();

   return 0;
}

Заключение

Рассмотренные DI-фреймворки используются в реальных проектах. На мой взгляд, [Boost].DI имеет API, который позволяет лучше управлять контейнером зависимостей. Оба фреймворка дают возможность описания зависимостей без модификации классов сущностей или создания XML-конфигураций. Это преимущество позволяет заменять библиотеки с минимальными затратами в процессе разработки.

Полезные ссылки

LinkedIn

15 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

In one of the examples:

struct DelegateService : kgr::service {};
...
struct DelegateService : kgr::single_service, kgr::polymorphic {};

Redefinition error?

Thanks a lot for pointing that! First declaration is totally wrong. I’ve already requested a change.

Годная статья. Только, черт возьми, давайте заканчивать с избыточной русификацией. Я секунд 5 не мог по заголовку понять, о внедрении чего и куда идет речь, пока не перевел на английский.

Спасибо за отзыв! Учту Ваше пожелание в будующих статьях

Еще одно «не нужно». Прослойки пишутся быстрее, чем в этой хрени разбираться будешь.

Большая проблема не в написанном, а в том, что разные версии либ часто не имеют обратной совместимости или их зависимости конфликтуют с тем, что сейчас стоит.
Вот чего не хватает для C и C++ это чего-то подобного Nix в типичных дистрах и винде, чтобы я мог при сборке приложения просто указывать либы и их версии, а уже ось с этим разрулилась.

Статическая линковка — это прошлый век. Используй динамическую загрузку, и сам решай что загружать.

ну да а потом микросервисы ))

если динамическая загрузка shared objects (а по-другому видимо никак ну разве что снова вариант микросервисов) то зачем не сделать объекты .so уже конфигурируемыми на этапе загрузки что именно грузить?

а стоп так они уже! ))

Да он просто бредил. Просто написал что-то, даже не читая то, на что он отвечал.

Вот чего не хватает для C и C++ это чего-то подобного Nix в типичных дистрах и винде, чтобы я мог при сборке приложения просто указывать либы и их версии, а уже ось с этим разрулилась.

Для питона сейчас выбран винда-вэй через venv — всё свое тяну с собой. С докерами тот же винда-вэй — всё своё тяну с собой.
А вот нормальный путь не курильщика — это через инструменты подобные Nix, но они почти не развиваются. Удобство работы с ними застряло где-то в начале 90-х (странно, что еще не предлагают перфокарты).

А вот нормальный путь не курильщика — это через инструменты подобные Nix, но они почти не развиваются. Удобство работы с ними застряло где-то в начале 90-х (странно, что еще не предлагают перфокарты).

Вот теперь прочитай мой ответ, может наконец-то дойдёт.

Вот чего не хватает для C и C++ это чего-то подобного Nix в типичных дистрах и винде, чтобы я мог при сборке приложения просто указывать либы и их версии, а уже ось с этим разрулилась.

Это только для hello world, если ты пишешь приложение посложнее — ты будешь делать вот так:

Для питона сейчас выбран винда-вэй через venv — всё свое тяну с собой. С докерами тот же винда-вэй — всё своё тяну с собой.

Сам поймёшь или обьяснить? :)

Сам поймёшь или обьяснить? :)

Всегда готов послушать умного человека и обгадить тупого. В этом топике от тебя пока ничего умного не прочитал.

Начнём с того, что у тебя абсолютно неправильное видение библиотек, принадлежащих операционной системе и процесса их использования (наверняка вызванных убунтой и Ко).

Все библиотеки, которые использует операционная система принадлежат ТОЛЬКО операционной системе и их использование в своих проектах возможно ТОЛЬКО на свой страх и риск. Поэтому утверждение

чего-то подобного Nix в типичных дистрах и винде, чтобы я мог при сборке приложения просто указывать либы и их версии, а уже ось с этим разрулилась

Является некорректным в своей основе. Для примера возьмём libjpeg и убунту, как образец рассадника хаоса. В дистрибутиве убунты может со-сосуществовать несколько версий libjpeg, но лишь только потому, что в дистрибутиве есть древние приложения, которые находятся в неподдерживаемом состоянии и которым нужна определенная версия libjpeg. Ты никогда не знаешь, что будет в следующем обновлении, все перейдут на новую версию libjpeg и оставят ли старую, доступную для работы или старую, на которое завязано твоё приложение просто упразднят и ты должен будешь работать только с новой. Запомни простое правило, все библиотеки, идущие в дистрибутиве предназначены для функционирования дистрибутива и только, а не для твоих приложений.

Теперь плавно подходим к тому, что ты должен тащить libjpeg с собой для своих приложений. Везде и всегда. Во-первых, оптимизация. в дистрибутиве могут (и используют) щадящие и общие опции оптимизации, например для i686/x86 и всё, а libjpeg может использовать новомодные плюшки процессоров, мимо которых ты просто пролетаешь. Поэтому ты собираешь libjpeg сам, с опциями, которые тебе нужны и тащишь её с собой. Во-вторых ты можешь иметь несколько сборок libjpeg (например стандартную, turbojpeg или стандартную с кучей оптимизационных патчей, включая те, которые, например, используют Intel VAAPI для аппаратного декодирования JPEG).

Теперь ещё более плавно подходим к тому, что я сказал изначально. Ты не можешь линковаться с libjpeg напрямую, потому что тебе нужно в рантайме выбирать и загружать библиотеку, с которой ты будешь работать. Поэтому утверждение «просто указывать либы и их версии, а уже ось с этим разрулилась» звучит ещё более глупо.

Теперь начинаем ускоряться. Типичная иерархия библиотек: Библиотека А использует Библиотеку B и C. В свою очередь библиотеки B и C используют libjpeg разных версий. Как ты собираешься разруливать данную ситуацию? Ещё 20 лет назад древние программисты вводили систему префиксов функций при компиляции, чтобы динамическому линкёру операционной системы не сносило крышу и при загрузке библиотеки А. Либо статическую линковку libjpeg, чтобы убрать пересекающееся пространство имён функций в библиотеках разных версий, но с одинаковыми функциями. Потом менее древние программисты начали грузить библиотеки динамически в runtime, что позволило прятать пространство имён функций внутри библиотек, не делая эти функции доступными для гравного приложения.

А вот было бы хорошо, чтобы с системе было дерево зависимостей и она уже разруливалась между разными версиями либы.
Когда либ и софта было мало и он был простой и описанный тобой механизм работал, сейчас же он сильно геморный.
Вот тебе пример — разруливаться в нескольких версиях куды ручками можно, но геморно.

А вот было бы хорошо, чтобы с системе было дерево зависимостей и она уже разруливалась между разными версиями либы.

Это будет работать только для системных пакетов, которые идёт с операционной системой. С 3rdparty никто работать не будет.

Вот тебе пример — разруливаться в нескольких версиях куды ручками можно, но геморно.

А у тебя есть альтернатива? Нет, её нет.

Это будет работать только для системных пакетов, которые идёт с операционной системой. С 3rdparty никто работать не будет.

Хотя бы.

Подписаться на комментарии