This document discusses effective practices for dependency injection (DI). It begins with a quick DI refresher and then provides guidelines for DI such as: explicitly defining dependencies, injecting exactly what is needed, preferring constructor injection, avoiding work in constructors, and avoiding direct dependencies on the injector. It also discusses testing code using DI, applying DI to existing code, and techniques for migrating code to use DI such as bottom-up or top-down approaches.
2. This talk
1. Quick DI refresher
2. Guidelines for effective DI
3. Composing applications using DI
4. Testing code using DI
5. Applying DI to existing code
4. DI = Dependency Injection
Declare what you need, not how to get it
PaymentProcessor(
CreditCardService creditCardService) {
this.creditCardService = creditCardService;
this.riskAssessor =
new RiskAssessorImpl(...);
}
5. DI = Dependency Injection
+ Loose coupling, facilitate reuse and testing
- Complexity, runtime failures
Many frameworks with many tools
Important to establish good conventions
6. DI Framework
Providers
Bindings
@Inject
Module [= collection of Bindings]
Injector [= collection of Modules]
injector.getInstance(type)
Scopes
8. Explicit > Implicit
Ensure explicit bindings for all injected types
bind(RequestFilter.class);
Implicit bindings dangerous
Removing an explicit binding
Pulling in undesired explicit binding
9. Inject exactly what you need
Bad:
PaymentProcessor(User user) {
this.account = user.getAccount();
}
Better:
PaymentProcessor(Account account) {
this.account = account;
}
10. Prefer constructor injection
private final fields
Immutability, thread-safety
Objects valid post-construction
Match developer expectations
11. Avoid work in constructors
Separation of concerns, facilitates testing,
"standard" constructor
In Guice: runtime ProvisionException
easily propagates up to top level
Initialize after constructing object graph
12. If you must do work in a constructor
interface DataStoreProvider<T> extends CheckedProvider<T> {
T get() throws DataStoreException;
}
Guice-specific
13. If you must do work in a constructor
(cont'd)
@CheckedProvides(DataStoreProvider.class)
Contacts provideContacts(UserId userId, DataStore dataStore)
throws DataStoreException {
return dataStore.readUserContacts(userId);
}
@Override protected void configure() {
install(ThrowingProviderBinder.forModule(this));
}
Guice-specific
14. If you must do work in a constructor
(cont'd)
@Inject ContactsFormatter(
DataStoreProvider<Contacts> contactsProvider) {
this.contactsProvider = contactsProvider;
}
String formatContacts() throws DataStoreException {
Contacts contacts = contactsProvider.get(); // throws
...
}
Guice-specific
15. If you must do work in a constructor
(cont'd)
Problems
More complex, verbose bindings
More setup work for tests
Business logic creep
Viral
Guice-specific
16. Avoid direct deps on Injector
No longer declaring specific deps
Injector allows dep on anything
Easy to start using Injector, hard to stop
17. Avoid binding very general types
Bad
bindConstant().to(8080);
@Inject int port;
Better
bindConstant().annotatedWith(ForHttp.class)
.to(8080);
@Inject @ForHttp int port;
18. Avoid parameterized binding
annotations
At first it seems convenient...
@Named("http") int httpPort;
...but then you do
@Named("HTTP") int httpPort;
No help from the IDE, more runtime errors.
Harder to track down injection sites.
19. Using parameterized binding
annotations safely
Consistently use constants (enums!) for parameters:
@ForPort(Ports.HTTP) int httpPort;
@FeatureEnabled(Features.X) boolean xEnabled;
20. Keep modules fast and side-effect
free
Make it easy to compose modules
Avoid
bind().toInstance(...)
requestStaticInjection(...)
Serial eager initialization of objects
Guice-specific
21. Prefer Null Objects over @Nullable
@Provides AbuseService provideAbuseService(
@ForAbuse String abuseServiceAddress) {
if (abuseServiceAddress.isEmpty()) { return null; }
return new AbuseServiceImpl(abuseServiceAddress);
}
class AbuseChecker {
@Inject AbuseChecker(
@Nullable AbuseService abuseService) { ... }
void doAbuseCheck(...) {
if (abuseService != null) { ... )
}
Bind StubAbuseService instead of null. Simplify
AbuseChecker.
23. Scoping as an alternative to context
public interface PipelineVisitor() {
void visit(TypeToVisit typeToVisit);
}
public class Pipeline() {
private final PipelineVisitor[] visitors = {
new Visitor1(), new Visitor2(), ...
}
public void visit(TypeToVisit typeToVisit) {
...
}
}
24. Requirements creep in, you add
more parameters...
public interface PipelineVisitor() {
void visit(TypeToVisit typeToVisit, User user);
}
public interface PipelineVisitor() {
void visit(TypeToVisit typeToVisit,
User user, Logger logger);
}
public interface PipelineVisitor() {
void visit(TypeToVisit typeToVisit,
User user, Logger logger, RequestParams params);
}
and so on...
25. Ah yes, the Context to the rescue
public interface PipelineVisitor() {
void visit(PipelineContext context);
}
public Class PipelineContext {
TypeToVisit typeToVisit;
User user;
Logging logging;
... and many more ...
}
26. and adapts the Pipeline itself.
public class Pipeline() {
private final PipelineVisitor[] visitors = { ... };
OutputType visit(TypeToVisit in, User user, Logging
log)
{
OutputType out = new OutputType();
PipelineContext context =
new PipelineContext(in, user, log);
for (PipelineVisitor visitor : visitors) {
visitor.visit(context);
}
}
}
what about testing, isolation,...
27. First : remove the context
public interface Visitor {
void visit(TypeToVisit in);
}
public class LoggerVisitor {
private final Logger logger;
@Inject public LoggerVisitor(Logger logger) {...}
void visit(InputType in, Output out) {
logger.log(Level.INFO, "Visiting " + in);
}
public class UserDependentVisitor {
private final User user;
...
}
28. Second : use a Multi-Binder
class VisitorsModule extend AbstractModule {
@Override protected void configure() {
Multibinder<PipelineVisitor> visitorBinder =
Multibinder.newSetBinder(
binder(), PipelineVisitor.class);
visitorBinder.addBinding().to(LoggerVisitor.class);
visitorBinder.addBinding()
.to(UserDependentVisitor.class);
}
Now we can inject a Set<Visitor>
29. Inject into Pipeline
public class Pipeline() {
private final Set<Provider<PipelineVisitor.class>>
visitors
@Inject
Pipeline(Set<Provider<PipelineVisitor.class>> visitors) {
this.visitors = visitors;
}
public void visit(TypeToVisit in) {
for (Provider<PipelineVisitor.class> visitor :
visitors)
{
visitor.get().visit(in);
...
30. Testing Visitors and Pipeline
public void testVisitor() {
Visitor v = new FirstVisitor(...);
v.visit(in);
assert(...)
}
public void testPipeline() {
Visitor v = Mockito.mock(Visitor.class);
Pipeline p = new Pipeline(Sets.of(Providers.of(v)));
TypeToVisit in = new TypeToVisit(...);
p.visit(in);
Mockito.verify(v).visit(eq(in),
any(Output.class));
}
32. Modular Java!
Use a Service-Based Architecture
All services defined as an interface
Implementations package-private
Package-level Module binds
No need for OSGi unless you need:
Runtime extensibility
Runtime versioning
33. Modular Java: Declare API and Impl
API: (interfaces, enums, binding annotations)
public interface SpamChecker {
void checkIsSpam(...);
}
Implementation: (package private)
class SpamCheckerImpl implements SpamChecker {
void checkIsSpam(...) { ... }
}
34. Modular Java: Colocate bindings
In the same package as SpamChecker*:
public class SpamCheckerModule extends AbstractModule {
@Override
public void configure() {
bind(SpamCheckerImpl.class).in(Singleton.class);
bind(SpamChecker.class).to(SpamCheckerImpl.class);
}
}
To use in your app, install SpamCheckerModule
and then inject SpamChecker as needed.
35. Modular Java: Document deps
Document what your module requires:
class SpamCheckerModule ... {
@Override protected void configure() {
requireBinding(Clock.class);
requireBinding(CryptoService.class);
...
}
}
36. Modular Java: Package it all up
1. Maven
a. All public interfaces in -api.jar module
b. All implementations and modules in -impl.jar
c. api module should only be used for compiling
dependent modules, impl module should be used for
assembling the application
2. OSGi
a. Export interfaces and Module only
b. Avoid versioning at runtime
38. Unit testing a class using DI
public class SubjectUnderTest {
@Inject
SubjectUnderTest(Service1 s1, Service2 s2) {
// Assign to fields
}
String doSomething() { ... }
}
39. Unit testing a class using DI (cont'd)
Service1 mock1 = Mockito.mock(Service1.class);
Service2 mock2 = Mockito.mock(Service2.class);
SubjectUnderTest sut = new SubjectUnderTest(
mock1, mock2);
// configure mock
Mockito.when(mock1).service(eq("param"))
.thenReturn("some return value);
// test your service
sut.doSomething();
// verify the results...
40. Testing bindings
Module.override to overcome problematic
bindings for testing:
Module testModule = Modules
.override(new AcmeModule())
.with(new AbstractModule() {
@Override protected void configure() {
bind(AuthChecker.class)
.toInstance(new StubAuthChecker());
}
});
Avoid overriding too many bindings
41. System tests
System (end-to-end) tests a must with DI
Discover runtime failures in tests or your users
will discover them for you
43. Migrating to DI
Rewiring an existing code base to use DI will
take time
Don't need to use everywhere (but consistency
is good)
44. Techniques for migrating to DI
Approaches
Bottom-up, separate injectors for subparts
Top-down, single injector pushed down
Often need to compromise on ideal patterns
Multiple Injectors
Directly reference Injector
Constructors do work
45. Things that make DI difficult: deep
inheritance
class BaseService { }
class MobileService extends BaseService { }
class AndroidService extends MobileService {
@Inject AndroidService(Foo foo, Bar bar) {
super(foo, bar);
}
What if BaseService needs to start depending
on Baz? Prefer composition.
46. Things that make DI difficult: static
methods
class SomeClass {
static void process(Foo foo, Bar bar) {
processFoo(foo); ...
}
static void processFoo(Foo foo) { ... }
}
I want to bind Foo and start injecting it
elsewhere. How do I get to Guice?
50. Avoid @ImplementedBy
@ImplementedBy(MySqlUserStore.class)
interface UserStore { ... }
Guice-ism, provided for convenience
Interface should not have compile-time dep
on specific implementation!
Can be overridden by explicit bindings
Guice-specific
51. Constructor injection promotes
thread-safe code
@ThreadSafe
class Greeter {
private final Greetings greetings;
private final User user;
@Inject
Greeter(Greetings greetings, User user) {
this.greetings = greetings;
this.user = user;
}
String getGreeting(GreetingId greetingId) { ... }
}
52. Minimize Custom Scopes
Adds complexity
Scopes mismatches
e.g. request scoped type into singleton scoped type
More (wrong) choices for developers
Should I put this in scopeA, scopeB, scopeC, ...?
53. Testing providers
If your module contains non-trivial provider
methods, unit test them
class FooModuleTest {
void testBarWhenBarFlagDisabled() {
assertNull(
new FooModule().provideBar(
false /* bar flag */);
}
void testBarWhenBarFlagEnabled() { ... }
}