google/fruit

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 implements IBackend and requires an IWorker*
  • Worker implements IWorker and requires an api::IClient* and a service::IClient*
  • api::Client implements api::IClient and requires an ASSISTED(std::string)
  • service::Client implements service::IClient and requires an ASSISTED(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 Clients 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!