Isolation
The problem is caused by insufficient isolation of various sub-systems.
From a sub-system S
point of view, type X
has changed its contract over time. In the past, the type gave S
ability to control all of its instances. But with integrations, control of instances was lost. Type X
changed its behavior in an incompatible way.
Sub-system S
should have been more isolated. It should not have used types that are potentially in conflict with other systems. It should have used its own private types instead, there'd be no issue then.
Now let's say in our hypothetical problem, there is a Database
sub-system. It uses timeout when issuing network calls. It wants an instance of Timeout
that has the value of 100 millis. EmailSender
sub-system expects to be slower. Its Timeout
has to equal 5 seconds. The systems conflict when integrated.
One approach: private wrapper types
// EmailSender-private wrapper. Class is not public on purpose.
class EmailSenderTimeout {
final Timeout timeout;
EmailSenderTimeout(Timeout t) { this.timeout = t; }
}
// In the module
bind(EmailSenderTimeout.class)
.toInstance(new EmailSenderTimeout(Timeout.seconds(5));
// In the service
@Inject
EmailSendingService(EmailSenderTimeout timeout) {
long millis = timeout.timeout.millis();
}
Walla! Should anyone ever go and bind Timeout
to whatever their heart desires, we the EmailSender
still have our 5 seconds!
We achieved through isolation. We are still sharing the Timeout
type, but we are no longer sharing instances.
Guicier: binding annotations
This mechanism is Guice's answer to our exact problem.
// Define this annotation once in the sub-system somewhere.
// Perhaps even directly in the Module class.
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
@interface ForEmail { }
// EmailModule
protected void configure() {
bind(Timeout.class).annotatedWith(ForEmail.class)
.toInstance(Timeout.seconds(5);
}
// Service
class EmailSendingService {
@Inject
EmailServiceImpl(@ForEmail Timeout timeout) {
long millis = timeout.millis();
}
}
You can reuse the annotation for other shared types, too:
class EmailServiceImpl {
@Inject
EmailServiceImpl(@ForEmail Timeout timeout,
@ForEmail RemoteAddress remoteAddress,
@ForEmail Protocol protocol) {
}
}
Each sub-system would declare its own private binding annotation and use it throughout.
In absolute, no two sub-systems should bind the same types, whether or not they are integrated today.
Simplistic mental model of Guice
There must never be duplicates in bindings
:
class Guice {
HashMap<Key, Provider> bindings;
}
// Combines 3 things: Class, Generic Types, and Annotation
class Key {
Class<?> actualClass;
@Nullable Class<?> annotationClass;
@Nullable Type genericTypes;
}
More details: Key.java, TypeLiteral.java