Injecting objects constructed via factory without modifying constructor signature?
leakybits opened this issue · 3 comments
I've been reading the tutorial/reference on the wiki over and over again and am really struggling to make something work. I'd appreciate any guidance you can offer!
I'm trying to set up a system consisting of four parts:
Backend
implementsIBackend
and requires anIWorker*
Worker
implementsIWorker
and requires anapi::IClient*
and aservice::IClient*
api::Client
implementsapi::IClient
and requires anASSISTED(std::string)
service::Client
implementsservice::IClient
and requires anASSISTED(service::Channel)
Essentially, Backend
has a Worker
which has an api::Client
and a service::Client
.
api::Client
and service::Client
are constructed via a factory due to their assisted injection. That makes sense -- for example, fruit gives me a std::function<std::unique_ptr<api::IClient>(std::string)>
so that I can construct API clients for as many different std::string
inputs as I want.
But I want the Worker
and Backend
objects to have simple constructors of the form
Worker(api::IClient*, service::IClient*);
Backend(IWorker*);
In my opinion, they shouldn't care that the two Client
s are constructed via factory, parameterised by std::string
/service::Channel
. I should for example be able to inject a mock version of the clients with a different concrete implementation and different dependencies.
But that's exactly what I'm struggling with -- from what I've read on the wiki, it seems my constructors need to be of the form:
Worker(
api::ClientFactory apiFactory,
ASSISTED(std::string) url,
service::ClientFactory serviceFactory,
ASSISTED(service::Channel) channel
) : mAPIClient{apiFactory(url)}, mServiceClient{serviceFactory(channel)} {}
Backend(WorkerFactory factory, ASSISTED(std::string) url, ASSISTED(service::Channel) channel)
: mWorker{factory(url, channel)} {}
While this works, it does seem to kind of ruin the point of dependency injection, because now the implementation details of the concrete clients (i.e. that they depend on strings/channels) are leaking through to the Worker/Backend classes, which should ideally only work with client interfaces.
At the end of the day, what I want to be able to do is something along the lines of this:
Component<std::function<std::unique_ptr<IBackend>(std::string, service::Channel)>>
getBackendComponent() {
return createComponent()
.bind<IBackend, Backend>()
.install(getWorkerComponent)
.install(api::getClientComponent)
.install(service::getClientComponent);
}
but while keeping the constructors of Worker
and Backend
fully implementation-agnostic of their dependencies, such that I could swap them for something that doesn't depend on strings/channels. Is there a way to achieve that?
Hope my question makes sense and that you can send me in the right direction!
That makes sense -- for example, fruit gives me a std::function<std::unique_ptrapi::IClient(std::string)> so that I can construct API clients for as many different std::string inputs as I want.
Is it a requirement in your system that you might need multiple clients with different string values within 1 system?
Or are you just trying to make the string configurable at a different level?
Factories give you a lot of freedom in terms of how to pass the string param, but there's a cost: since you can have multiple client instances and they're not managed by Fruit, then Fruit can't just construct e.g. Backend for you since it doesn't know what string/client you want.
You could have factories all the way up, but it gets messy. Unless you need multiple instances of the client within a single injector / system you should probably not use factories for this.
Is it a requirement in your system that you might need multiple clients with different string values within 1 system?
And if you can have multiple, then would you just have 1 Backend?
Or 1 Backend instance for each client?
Or ...?
Many thanks for the response. You were right -- I realised that at any given point in time there should only be one of each client instantiated. A factory is therefore overkill.
I rearchitected slightly so that the concrete client implementations receive a "configuration" object providing the necessary settings (server host std::string or grpc channel). I create these config objects in main()
so they stay alive throughout the whole program and I use bindInstance
to bind them to the concrete client implementations.
I'm much happier with this solution now. Thanks!