Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/qlibs/di

C++20 Dependency Injection library
https://github.com/qlibs/di

cpp20 depedency-injection

Last synced: 7 days ago
JSON representation

C++20 Dependency Injection library

Awesome Lists containing this project

README

        

//
[Overview](#Overview) / [Examples](#Examples) / [API](#API) / [FAQ](#FAQ)

## DI: Dependency Injection library

[![MIT Licence](http://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/license/mit)
[![Version](https://img.shields.io/github/v/release/qlibs/di)](https://github.com/qlibs/di/releases)
[![Build](https://img.shields.io/badge/build-green.svg)](https://godbolt.org/z/fKEcojqze)
[![Try it online](https://img.shields.io/badge/try%20it-online-blue.svg)](https://godbolt.org/z/xrzsYG1bj)

> https://en.wikipedia.org/wiki/Dependency_injection (for additional info see [FAQ](#faq))

### Features

- Single header (https://raw.githubusercontent.com/qlibs/di/main/di - for integration see [FAQ](#faq))
- Verifies itself upon include (can be disabled with `-DNTEST` - see [FAQ](#faq))
- Minimal [API](#api)
- Unified way for different polymorphism styles (`inheritance, type erasure, variant, ...`)
- [Generic factories](https://en.wikipedia.org/wiki/Factory_method_pattern)
- Constructor deduction for classes and aggregates
- Constructor order and types changes agnostic (simplifies integration with `third party` libraries)
- Testing (different bindigns for `production` and `testing`, `faking` some parameters with `assisted` injection)
- Policies (APIs with `checked` requirements)
- Logging/Profiling/Serialization/... (via iteration over all `created` objects)

### Requirements

- C++20 ([clang++13+, g++11+](https://en.cppreference.com/w/cpp/compiler_support))

---

### Overview

> API (https://godbolt.org/z/xrzsYG1bj)

```cpp
struct aggregate1 { int i1{}; int i2{}; };
struct aggregate2 { int i2{}; int i1{}; };
struct aggregate { aggregate1 a1{}; aggregate2 a2{}; };

// di::make (basic)
{
static_assert(42 == di::make(42));
static_assert(aggregate1{1, 2} == di::make(1, 2));
}

// di::make (generic)
{
auto a = di::make(di::overload{
[](di::trait auto) { return 42; }
});

assert(a.i1 == 42);
assert(a.i2 == 42);
}

// di::make (assisted)
{
struct assisted {
constexpr assisted(int i, aggregate a, float f) : i{i}, a{a}, f{f} { }
int i{};
aggregate a{};
float f{};
};

auto fakeit = [](auto t) { return decltype(t.type()){}; };
auto a = di::make(999, di::make(fakeit), 4.2f);

assert(a.i == 999);
assert(a.a.a1.i1 == 0);
assert(a.a.a1.i2 == 0);
assert(a.a.a2.i1 == 0);
assert(a.a.a2.i2 == 0);
assert(a.f == 4.2f);
}

// di::make (with names)
{
auto a = di::make(di::overload{
[](di::is auto t) requires (t.name() == "i1") { return 4; },
[](di::is auto t) requires (t.name() == "i2") { return 2; },
});

assert(a.i1 == 4);
assert(a.i2 == 2);
}

// di::make (with names) - reverse order
{
auto a = di::make(di::overload{
[](di::is auto t) requires (t.name() == "i1") { return 4; },
[](di::is auto t) requires (t.name() == "i2") { return 2; },
});

assert(a.i1 == 4);
assert(a.i2 == 2);
}

// di::make (with names, context and compound types)
{
auto a = di::make(di::overload{
// custom bindigs
[](di::trait auto t)
requires (t.name() == "i1" and
&typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; },
[](di::trait auto) { return 42; },

// generic bindings
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
});

assert(a.a1.i1 == 99);
assert(a.a1.i2 == 42);
assert(a.a2.i1 == 42);
assert(a.a2.i2 == 42);
}

constexpr auto generic = di::overload{
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
};

// di::make (seperate overloads)
{
constexpr auto custom = di::overload {
[](di::trait auto t)
requires (t.name() == "i1" and
&typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; },
[](di::trait auto t) { return decltype(t.type()){}; },
};

auto a = di::make(di::overload{custom, generic});

assert(a.a1.i1 == 99);
assert(a.a1.i2 == 0);
assert(a.a2.i1 == 0);
assert(a.a2.i2 == 0);
}

// di::make (polymorphism, scopes)
{
struct interface {
constexpr virtual ~interface() noexcept = default;
constexpr virtual auto fn() const -> int = 0;
};
struct implementation : interface {
constexpr implementation(int i) : i{i} { }
constexpr auto fn() const -> int override final { return i; }
int i{};
};

struct example {
example(
aggregate& a,
const std::shared_ptr& sp
) : a{a}, sp{sp} { }
aggregate a{};
std::shared_ptr sp{};
};

auto i = 123;

auto bindings = di::overload{
generic,

[](di::is auto t) { return di::make(t); },
[&](di::is auto) -> decltype(auto) { return i; }, // instance

// scopes
[](di::trait auto t) -> decltype(auto) {
using type = decltype(t.type());
static auto singleton{di::make>(t)};
return (singleton);
},
};

auto e = di::make(bindings);

assert(123 == e.sp->fn());
assert(123 == e.a.a1.i1);
assert(123 == e.a.a1.i2);
assert(123 == e.a.a2.i1);
assert(123 == e.a.a2.i2);

// testing (override bindings)
{
auto testing = di::overload{
[](di::trait auto) { return 1000; }, // priority
[bindings](auto t) -> decltype(auto) { return bindings(t); }, // otherwise
};

auto e = di::make(testing);

assert(1000 == e.sp->fn());
assert(1000 == e.a.a1.i1);
assert(1000 == e.a.a1.i2);
assert(1000 == e.a.a2.i1);
assert(1000 == e.a.a2.i2);
}

// logging
{
constexpr auto logger = [root = false](
di::provider&& t) mutable -> decltype(auto) {
if constexpr (constexpr auto is_root =
di::provider::size() == 1u; is_root) {
if (not std::exchange(root, true)) {
std::clog << reflect::type_name() << '\n';
}
}
for (auto i = 0u; i < di::provider::size(); ++i) {
std::clog << ' ';
}
if constexpr (di::is_smart_ptr>) {
std::clog << reflect::type_name() << '<'
<< reflect::type_name<
typename std::remove_cvref_t::element_type>() << '>';
} else {
std::clog << reflect::type_name();
}
if constexpr (not di::is_smart_ptr> and
requires { std::clog << std::declval(); }) {
std::clog << ':' << t(t);
}
std::clog << '\n';

return t(t);
};

(void)di::make(di::overload{logger, bindings});
// example
// aggregate
// aggregate1
// int:123
// int:123
// aggregate2
// int:123
// int:123
// shared_ptr -> implmentation
// int:123
}
}

// policies
{
struct policy {
constexpr policy(int*) { }
};

[[maybe_unused]] auto p = di::make(di::overload{
[]([[maybe_unused]] di::trait auto t) {
static_assert(not sizeof(t), "raw pointers are not allowed!");
},
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
}); // error
}

// errors
{
(void)di::make(di::overload{
// [](di::is auto) { return 42; }, // missing binding
[](auto t) { return di::make(t); },
}); // di::error
}

// and more (see API)...
```

---

### Examples

> DIY - Dependency Injection Yourself (https://godbolt.org/z/acE3rYar5)

```cpp
namespace di {
inline constexpr auto injector = [](auto&&... ts) {
return di::overload{
std::forward(ts)...,
[](di::trait auto t) -> decltype(auto) {
using type = decltype(t.type());
static auto singleton{di::make>(t)};
return (singleton);
},
[](auto t) { return di::make(t); },
};
};
template
inline constexpr auto bind = [] {
if constexpr (std::is_void_v) {
return [](T&& to) {
return [&](di::is auto) -> decltype(auto) {
return std::forward(to);
};
};
} else {
return [](di::is auto t) { return di::make(t); };
}
}();
} // namespace di
```

```cpp
int main() {
auto injector = di::injector(
di::bind,
di::bind(42)
);

auto e = di::make(injector);

assert(42 == e.sp->fn());
assert(42 == e.a.a1.i1);
assert(42 == e.a.a1.i2);
assert(42 == e.a.a2.i1);
assert(42 == e.a.a2.i2);
}
```

> Standard Template Library (https://godbolt.org/z/jjbnffKne)

```cpp
struct STL {
STL(std::vector vector,
std::shared_ptr shared_ptr,
std::unique_ptr unique_ptr,
std::array array,
std::string string)
: vector(vector)
, shared_ptr(shared_ptr)
, unique_ptr(std::move(unique_ptr))
, array(array)
, string(string)
{ }

std::vector vector;
std::shared_ptr shared_ptr;
std::unique_ptr unique_ptr;
std::array array;
std::string string;
};

int main() {
auto stl = di::make(
di::overload{
[](di::is> auto) { return std::vector{1, 2, 3}; },
[](di::is> auto) { return std::make_shared(1); },
[](di::is> auto) { return std::make_unique(2); },
[](di::is> auto) { return std::array{3}; },
[](di::is auto) { return std::string{"di"}; },
[](auto t) { return di::make(t); },
}
);

assert(3u == stl.vector.size());
assert(1 == stl.vector[0]);
assert(2 == stl.vector[1]);
assert(3 == stl.vector[2]);
assert(1 == *static_cast(stl.shared_ptr.get()));
assert(2 == *stl.unique_ptr);
assert(3 == stl.array[0]);
assert("di" == stl.string);
}
```

> `is_structural` - https://eel.is/c++draft/temp.param#def:type,structural (https://godbolt.org/z/1Mrxfbaqb)

```cpp
template;
if constexpr (requires { type{}; }) {
return type{};
} else {
return di::make(t);
}
}
> concept is_structural = requires { [](cfg)>{}(); };

static_assert(is_structural);
static_assert(not is_structural>);

struct s { s() = delete; };
static_assert(not is_structural);

struct y { int i; };
static_assert(is_structural);

struct n { private: int i; };
static_assert(not is_structural);

struct c1 { constexpr c1(int) {} };
static_assert(is_structural);

struct c2 { constexpr c2(int, double) {} };
static_assert(is_structural);

struct c3 { constexpr c3(std::optional) {} };
static_assert(not is_structural);

struct c4 { constexpr c4(auto...) {} };
static_assert(is_structural);

struct c5 { private: constexpr c5(auto...) {} };
static_assert(not is_structural);
```

----

### API

```cpp
namespace di::inline v1_0_5 {
/**
* @code
* struct c1 { c1(int) { } };
* static_assert(std::is_same_v, di::ctor_traits::type>);
* #endcode
*/
template struct ctor_traits {
template struct type_list{};
using type = type_list* T constructor parameters (size = N..0)`)*/>;
[[nodiscard]] constexpr auto operator()(auto&&...) const -> T;
};

/**
* static_assert(di::invocable);
* static_assert(di::invocable);
*/
template concept invocable;

/**
* @code
* static_assert(not di::is);
* static_assert(di::is);
* @endcode
*/
template concept is;

/**
* @code
* static_assert(not di::is_a);
* static_assert(di::is_a, std::shared_ptr>);
*/
template class R> concept is_a;

/**
* @code
* static_assert(not di::is_smart_ptr);
* static_assert(di::is_smart_ptr>);
*/
template concept is_smart_ptr;

/**
* @code
* static_assert(not di::trait);
* static_assert(di::trait);
*/
template class Trait> concept trait;

/**
* @code
* static_assert(42 == di::overload{
* [](int i) { return i; },
* [](auto a) { return a; }
* }(42));
* @endcode
*/
template struct overload;

/**
* Injection context
*/
template
struct provider {
using value_type = T;
using parent_type = TParent;

static constexpr auto index() -> std::size_t; // index of parent constructor
static constexpr auto parent() -> parent_type; // callee provider
static constexpr auto type() -> value_type; // underlying type
static constexpr auto size() -> std::size_t; // size of parents
#if defined(REFLECT)
static constexpr auto name() -> std::string_view; // member name
#endif
};

/**
* @code
* static_assert(42 == di::make(42));
* static_assert(42 == di::make(
* di::overload{
* [](di::is auto) { return 42; }
* }
* ));
* @endcode
*/
template
[[nodiscard]] constexpr auto make(auto&&...);
} // namespace di
```

---

### FAQ

- Dependency Injection?

> Dependency Injection (DI) - https://en.wikipedia.org/wiki/Dependency_injection - it's a technique focusing on producing loosely coupled code.

```cpp
struct no_di {
constexpr no_di() { } // No DI

private:
int data = 42; // coupled
};

struct di {
constexpr di(int data) : data{data} { } // DI

private:
int data{};
};
```

- In a very simplistic view, DI is about passing objects/types/etc via constructors and/or other forms of parameter propagating techniques instead of coupling values/types directly (`Hollywood Principle - Don't call us we'll call you`).
- The main goal of DI is the flexibility of changing what's being injected. It's important though, what and how is being injected as that influences how good (`ETC - Easy To Change`) the design will be - more about it here - https://www.youtube.com/watch?v=yVogS4NbL6U.

- Manual vs Automatic Dependency Injection?

> Depedency Injection doesnt imply using a library.
Automatic DI requires a library and makes more sense for larger projects as it helps limitting the wiring mess and the maintenance burden assosiated with it.

```cpp
struct coffee_maker {
coffee_maker(); // No DI

private:
basic_heater heater{}; // coupled
basic_pump pump{}; // coupled
};

struct coffee_maker_v1 {
coffee_maker(iheater&, ipump& pump); // DI

private:
iheater& heater; // not coupled
ipump& pump; // not coupled
};

struct coffee_maker_v2 {
coffee_maker(std::shared_ptr, std::unique_ptr); // DI

private:
std::shared_ptr pump; // not coupled
std::unique_ptr heater; // not coupled
};

int main() {
// Manual Dependency Injection
{
basic_heater heater{};
basic_pump pump{};
coffe_maker_v1 cm{heater, pump};
}
{
auto pump = std::make_shared();
auto heater = std::make_unique();
coffe_maker_v2 cm{pump, std::move(heater)}; // different wiring
}

// Automatic Dependency Injection
auto wiring = di::overload{
[](di::is auto) { return make(); },
[](di::is auto) { return make(); },
};
{
auto cm = di::make(wiring);
}
{
auto cm = di::make(wiring); // same wiring
}
}
```

> The main goal of automatic is to **avoid design compromises** in order to reduce the boilerplate code/minimize maintance burden/simplify testing.

- How does it work?

> `DI` works by deducing constructor parameters and calling appropriate overload to handle them by leavaring concepts - https://eel.is/c++draft/temp.constr.order#def:constraint,subsumption.
The following represents the most important parts of the library design.

```cpp
template
concept copy_or_move = std::is_same_v>;

template struct any {
template requires (not copy_or_move)
operator T() noexcept(noexcept(bind, T>{}));
template requires (not copy_or_move)
operator T&() const noexcept(noexcept(bind, T&>{}));
template requires (not copy_or_move)
operator const T&() const noexcept(noexcept(bind, const T&>{}));
template requires (not copy_or_move)
operator T&&() const noexcept(noexcept(bind, T&&>{}));
};
```

```cpp
template constexpr auto ctor_traits() {
return [](std::index_sequence) {
if constexpr (requires { T{any{}...}; }) {
return type_list{}))::value_type...>{};
} else if constexpr (sizeof...(Ns)) {
return ctor_traits();
} else {
return type_list{};
}
}(std::make_index_sequence{});
}
```

```cpp
template struct overload : Ts... { using Ts::operator()...; };
template overload(Ts...) -> overload;
```

```cpp
template auto error(auto&&...) -> T;
template constexpr auto make(invocable auto&& t) {
return [&] class TList, class... Ts>(TList) {
if constexpr (requires { T{t(provider(t)...); }; }) {
return T{t(provider(t)...};
} else {
return error(t);
}
}(ctor_traits());
};
```

- How to disable running tests at compile-time?

> When `-DNTEST` is defined static_asserts tests wont be executed upon include.
Note: Use with caution as disabling tests means that there are no gurantees upon include that given compiler/env combination works as expected.

- How to integrate with [CMake.FetchContent](https://cmake.org/cmake/help/latest/module/FetchContent.html)?

```
include(FetchContent)

FetchContent_Declare(
qlibs.di
GIT_REPOSITORY https://github.com/qlibs/di
GIT_TAG v1.0.5
)

FetchContent_MakeAvailable(qlibs.di)
```

```
target_link_libraries(${PROJECT_NAME} PUBLIC qlibs.di);
```

- Acknowledgments
> - ["Dependency Injection - a 25-dollar term for a 5-cent concept"](https://www.youtube.com/watch?v=yVogS4NbL6U) (video)
> - ["Law of Demeter: A Practical Guide to Loose Coupling"](https://www.youtube.com/watch?v=QZkVpZlbM4U) (video)
> - ["Clean Code: A Handbook of Agile Software Craftsmanship"](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) (book)
> - ["The Pragmatic Programmer"](https://www.amazon.com/Pragmatic-Programmer-journey-mastery-Anniversary/dp/0135957052/ref=pd_sbs_d_sccl_3_1/140-7224166-5387863?pd_rd_w=muV95&content-id=amzn1.sym.156274ff-6322-443d-8bbf-ab3ed87e382f&pf_rd_p=156274ff-6322-443d-8bbf-ab3ed87e382f&pf_rd_r=ECRZDQ02XA134FQJJQDE&pd_rd_wg=ELPtc&pd_rd_r=49d9e9b3-a8b1-4532-b3b3-16711496a3d3&pd_rd_i=0135957052&psc=1) (book)
> - ["Design Patterns"](https://www.amazon.com/Design-Patterns-Object-Oriented-Addison-Wesley-Professional-ebook/dp/B000SEIBB8) (book)
> - ["Test Driven Development: By Example"](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530) (book)

- Similar projects?
> [boost-ext.di](https://github.com/boost-ext/di), [google.fruit](https://github.com/google/fruit), [kangaru](https://github.com/gracicot/kangaru), [wallaroo](https://wallaroolib.sourceforge.net), [hypodermic](https://github.com/ybainier/Hypodermic), [dingo](https://github.com/romanpauk/dingo)