I am building a client SDK for a web API and trying to apply dependency injection via guice. This Java client will be used by third parties as a way of accessing our API.
I want to be able to inject my external dependencies(The HTTP client used etc.) and give a way for developers to inject different versions of those dependencies if they wanted or if I ever wanted to change the implementation myself (a good case for dependency injection right?).
However in order to wire the dependencies, I have to make the user of my library create an injector etc. something like so:
Injector injector = Guice.createInjector(new MyAPIClientModule(url, username, password));
this.service = injector.getInstance(MyAPIService.class);
I don't want to push this up to the user of my library, but I still want to give users the ability to choose a different implementation or underlying HTTP library etc.
Am I missing the point of guice or DI here somehow? Is this standard practice when using guice?
Or should I wrap this in another class that does the injection and present the third party user with just a sample Java object?
I want to be able to inject my external dependencies(The http client used etc.) and give a way for developers to inject different versions of those dependencies if they wanted or if I ever wanted to change the implementation myself(a good case for dependency injection right?).
It is highly arguable that this is a good case for DI. External dependencies like HTTP clients usually have concrete interface that is implemented by no one except exactly that dependency. Personally I can't imagine how your program is written given that swapping underlying HTTP client won't affect its architecture, that is, unless you provide your own facade for it, something like
public interface HttpClient {
HttpResponse send(HttpRequest request);
}
where HttpRequest
and HttpResponse
are also custom classes/interfaces. But providing such kind of extension point to the end user is rarely appropriate, especially if you don't have some reference implementation (this means that the user will have to create this facade for the dependency he/she wants). It is appropriate in rare cases, but chances are this is not your situation.
Different versions of the same dependency is also usually not the case for DI because swapping versions can be done at build/assembly time.
If you want to expose an ability for the user to provide their own implementations of some of your library "moving parts", then first you have to define strict interface for all these moving parts. In other words, provide a set of interface
s which your user must extend and which are injected in your classes.
Then you create your "binding space" consisting of your Guice modules, and in these modules you declare requirements on these interfaces:
public class SomeModule extends AbstractModule {
@Override
protected void configure() {
requireBinding(SomeUserAPI.class);
// Other bindings which probably use SomeUserAPI in implementations
}
}
By stating required bindings you ensure that no one will be able to mix in your module unless they provide some implementation of the given class. Of course, Guice will fail anyway if it can't find the binding, but when you require it explicitly you obtain more concrete error message, as well as clear interface of your modules.
And then you create special "entry point" to your library, sole responsibility of which is to create the injector and provide the user with instances of your classes. This class accepts Guice module from the user and integrates it into the injector.
public class Library {
private final Injector injector;
private Library(Module userModule) {
// SomeModule and AnotherModule are modules defined in the library
// and they are not the part of public interface of your library
this.injector = Guice.createInjector(userModule, new SomeModule(), new AnotherModule());
}
public static Library create(Module userModule) {
return new Library(userModule);
}
public MyAPIService myAPIService() {
return injector.getInstance(MyAPIService.class);
}
}
Then the user uses it like this:
Library library = Library.create(new AbstractModule() {
@Override
protected void configure() {
// recall requireBinding(SomeUserAPI.class) statement we wrote previously,
// here we are "implementing" it
bind(SomeUserAPI.class).to(SomeUserAPIImpl.class);
// other bindings for your exposed interfaces
}
});
MyAPIService service = library.myAPIService();
In this approach you allow the user to extend your library using Guice DI in a neat and controlled way.
You still have to expose Guice to your users, however (because the users have to implement Module
interface). I don't think you can avoid that completely unless you do something bizarre like
Library.create(SomeUserAPIImpl.class, SomeUserAPI2Impl.class, ...)
that is, accept class objects representing implementations of extension points (and then bind them in some internal module). But I don't think that eliminating Guice from the library interface really worth it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With