Dependency Injection with C++20 Concepts and the Service Provider Pattern
In this article I explore a system of using C++20 Concepts for Dependency Injection
Linux /g++ 11.1.0
In this article I explore a system of using C++20 Concepts for Dependency Injection. A working example can be found here
For each service you will need:
- an interface Concept
- a provider Concept
- concrete implementation
Optionally you could also create a mock implementation using the testing framework of your choice.
End Game
In the sample code I have several functions (all, two, onlyLogger, onlyPersist, onlyCalculator) that have different injection requirements. I have covered requirements for all, a sample/mix (two), and a single service. All functions take a provider object as their only parameter.
int main() {
auto provider = ServiceProviderImpl{};
all(provider);
two(provider);
onlyLogger(provider);
onlyPersist(provider);
onlyCalculator(provider);
return 0;
}
Here are the functions and their requirements:
- all (logger, persistence, calculator)
- two (logger, persistence)
- onlyLogger (logger)
- onlyPersist (persistence)
- onlyCalculator (calculator)
void all(ServiceProviderInterface auto provider) {
provider.logger.info("Hello Logger DI: all");
std::cout << "add (all) = " << provider.calculator.add(1,2) << std::endl;
}
template<typename T>
concept TwoProvider =
LoggerServiceProvider<T> &&
DataPersistenceProvider<T>;
void two(TwoProvider auto provider) {
provider.logger.info("Hello Logger DI: two");
}
void onlyLogger(LoggerServiceProvider auto provider) {
provider.logger.info("Hello Logger DI: onlyLogger");
}
void onlyPersist(DataPersistenceProvider auto provider) {
provider.persistence.write();
}
void onlyCalculator(CalculatorProvider auto provider) {
std::cout << "add (onlyCalculator) = " << provider.calculator.add(3,4) << std::endl;
}
All requirements can use the ServiceProviderInterface directly, single requirements can use the individual provider directly. The more useful case is a mix of services which requires a new concept to be used. Luckily the mixed concepts are easy to write by using &&
with the services you require.
template<typename T>
concept TwoProvider =
LoggerServiceProvider<T> &&
DataPersistenceProvider<T>;
Service Provider
template<typename T>
concept ServiceProviderInterface =
LoggerServiceProvider<T> &&
DataPersistenceProvider<T> &&
CalculatorProvider<T>;
struct ServiceProviderImpl{
LoggerServiceImpl logger;
DataPersistenceImpl persistence;
CalculatorImpl calculator;
};
auto provider = ServiceProviderImpl{};
Creating the Services
This is the Calculator implementation. It's simply a struct with 2 functions.
struct CalculatorImpl {
int add(int a, int b){ return a + b; }
int subtract(int a, int b){ return a - b; }
};
We can then create a Concept for Calculator that enforces the interface at compile time.
template<typename T>
concept CalculatorInterface = requires(T calculator, int value) {
{calculator.add(value, value)} -> std::same_as<int>;
{calculator.subtract(value, value)} -> std::same_as<int>;
};
The provider allows us to simplify the Service Provide Concept and enforce the variable name of this service (in this case calculator
).
template<typename T>
concept CalculatorProvider = requires(T provider){
{provider.calculator} -> CalculatorInterface;
};