Strict vs. Forgiving APIs
Suppose it's the early 1990's and you're James Gosling implementing
String.substring(int, int)
for the first time. What should happen when the index arguments are out-of-range? Should these tests pass? Or throw?
public void testSubstring() {
assertEquals("class", "superclass".substring(5, 32));
assertEquals("super", "superclass".substring(-2, 5));
assertEquals("", "superclass".substring(20, 24));
assertEquals("superclass", "superclass".substring(10, 0));
}
Forgiving APIs
In a forgiving API, these tests pass. The implementation would recognize the out-of-range indices and correct for them. Benefits of forgiving APIs:
- Fault-tolerant. An off-by-one mistake won't bring a production system to its knees.
- Easier to code against. If you don't know what to use for a given argument, just pass
null
and the implementation will do something reasonable.
Strict APIs
In a strict APIs, the out-of-range arguments to
substring
are forbidden and the method throws an
IllegalArgumentException
. Benefits of strict APIs:
- Fail-fast. An off-by-one mistake will be caught in unit tests, if they exist.
- Easier to maintain. By limiting the number of valid inputs, there's less behaviour to maintain and test.
- More Predictable. Mapping invalid inputs to behaviour is an artform. In the example, should
substring(10, 0)
return the empty string? Or "superclass"? What would the caller expect?
For maintainability, I almost always prefer strict APIs. I like to think of the classes in my code as the gears in a fine Swiss watch. Everything fits together tightly, with firm constraints on both the inputs and the outputs. I can refactor with confidence because the system simply won't work if I've introduced problems into it. With a forgiving API, I could introduce bugs and not find out about them until much later.
# posted by Jesse Wilson
on Monday, June 30, 2008
4 comments
post a comment
What's a Hierarchical Injector?
Our application has two implementations for one interface.
EnergySource
is implemented by both
Plutonium
and
LightningBolt
:
class DeLorean {
@Inject TimeCircuits timeCircuits;
@Inject FluxCapacitor fluxCapacitor;
@Inject EnergySource energySource;
}
interface FluxCapacitor {
boolean isFluxing();
}
@Singleton
class RealFluxCapacitor implements FluxCapacitor {
@Inject TimeCircuits timeCircuits;
boolean isFluxing;
public boolean isFluxing() {
return isFluxing;
}
}
class TimeCircuits {
Date whereYouveBeen;
Date whereYouAre;
Date whereYourGoing;
}
interface EnergySource {
void generateOnePointTwentyOneGigawatts();
}
class Plutonium implements EnergySource { ... }
class LightningBolt implements EnergySource { ... }
And to allow for sequels, we assume other implementations of
EnergySource
are possible. We'd like to create an
Injector
immediately and create a Plutonium-powered DeLorean. Shortly thereafter, we'd like to re-use that same
Injector
, but with a lightning bolt for energy.
Option one: Factory classes
We can solve this problem by introducing a
DeLorean.Factory
interface that accepts an
EnergySource
as its only parameter:
class DeLorean {
private final TimeCircuits timeCircuits;
private final FluxCapacitor fluxCapacitor;
private final EnergySource energySource;
DeLorean(TimeCircuits timeCircuits,
FluxCapacitor fluxCapacitor,
EnergySource energySource) {
this.timeCircuits = timeCircuits;
this.fluxCapacitor = fluxCapacitor;
this.energySource = energySource;
}
static class Factory {
@Inject TimeCircuits timeCircuits;
@Inject FluxCapacitor fluxCapacitor;
DeLorean create(EnergySource energySource) {
return new DeLorean(timeCircuits, fluxCapacitor, energySource);
}
}
}
This works for our
specific problem, but in general it's quite awkward:
- It requires a gross amount of boilerplate code.
- It discourages refactoring of the
DeLorean
class. - It increases the complexity of getting an
EnergySource
. - It doesn't work unless
EnergySource
is a direct dependency of the DeLorean
class. Otherwise you need to create lots of little factories that cascade. - And
EnergySource
is no longer in-the-club—it doesn't participate in Guice's injection, AOP, scoping, etc.
Option two: AssistedInject
AssistedInject is a Guice extension that's intended to reduce the boilerplate of option one. Instead of a factory class, we write a factory interface plus annotations:
class DeLorean {
TimeCircuits timeCircuits;
FluxCapacitor fluxCapacitor;
EnergySource energySource;
@AssistedInject
DeLorean(TimeCircuits timeCircuits,
FluxCapacitor fluxCapacitor,
@Assisted EnergySource energySource) {
this.timeCircuits = timeCircuits;
this.fluxCapacitor = fluxCapacitor;
this.energySource = energySource;
}
interface Factory {
DeLorean create(EnergySource energySource);
}
}
This fixes some problems. But the core issue still remains: getting an instance of
EnergySource
is difficult. Unlike regular Guice (
@Inject
is the new
new
), you need to change all callers if you add a dependency on
EnergySource
.
Option three: Hierarchical Injectors
The premise is simple.
@Inject
anything, even stuff you don't know at injector-creation time. So our
DeLorean
class would look exactly as it would if
EnergySource
was constant:
class DeLorean {
TimeCircuits timeCircuits;
FluxCapacitor fluxCapacitor;
EnergySource energySource;
@Inject
DeLorean(TimeCircuits timeCircuits,
FluxCapacitor fluxCapacitor,
EnergySource energySource) {
this.timeCircuits = timeCircuits;
this.fluxCapacitor = fluxCapacitor;
this.energySource = energySource;
}
}
To use it, we start with an
Injector
that had bindings for everything
except for
EnergySource
. Next, we create a second injector that extends the first, and binds either
Plutonium
or
LightningBolt
. This second injector fills in its missing binding.
The injectors share singletons, so we don't have to worry about having multiple
TimeCircuits
. Static analysis is applied to both injectors as a whole, where complete information is known. And all objects are
in-the-club and get Guice value-adds like injection, scoping and AOP.
This is the solution to the mystical
Robot Legs problem, wherein we have a
RobotLeg
class, that needs be injected with either a
LeftFoot
or a
RightFoot
, depending on where the leg will ultimately be used.
Criticism of Hierarchical Injectors
They suggest competing bindings. One
parent injector could have relations with multiple
child injectors. In our example, the parent injector binds
DeLorean
and
TimeCircuits
, and each child binds a different
EnergySource
.
They require abstract Injectors. The parent injector in our example wouldn't be able to create an instance of
DeLorean
, since it doesn't have all of the prerequisite bindings. This is just weird.
They're complex. Guice was born out of making code simpler. Does the conceptual weight of hierarchical injectors justify their inclusion?
Going forward
Today's Guice includes a simplified implementation of hierarchical injectors written by Dan Halem. It doesn't cover the interesting (but complex) case where the parent injector cannot fulfill all of its bindings. I'm studying the use cases, trying to come up with a balance between ease-of-use and power.
For example, one idea is to require users to explicitly call-out bindings that child injectors will provide:
public void configure() {
bind(EnergySource.class).throughChildInjector();
}
I'd also like to do something similar to AssistedInject's factory interfaces. This way the second injector would be created, used and discarded transparently, so the user never needs to see it. From the user's perspective, this would just be like AssistedInject, but the assisted parameters could be injected anywhere.
If you have suggested use-cases or ideas, I'd love to hear 'em.
# posted by Jesse Wilson
on Monday, June 23, 2008
2 comments
post a comment
Wanted: Guice Injector Graphing
One of the nice new features of Guice 2.0 is the new introspection API. It's the equivalent of
java.lang.reflect
for Guice - it lets you inspect your application at runtime. Our goal is to make it easy to write rich tools for Guice. A natural use case is visualizing an application. The right graph can reveal the structure of your application. I've opened a feature request for this,
Issue 213. I created a proof-of-concept to drum-up excitement for this idea.
Example Graph: Application Code
class DeLorean {
@Inject TimeCircuits timeCircuits;
@Inject FluxCapacitor fluxCapacitor;
@Inject EnergySource energySource;
}
class FluxCapacitor {
@Inject TimeCircuits timeCircuits;
}
class TimeCircuits {
Date whereYouveBeen;
Date whereYouAre;
Date whereYourGoing;
}
interface EnergySource {
String generateOnePointTwentyOneGigawatts();
}
class Plutonium implements EnergySource {
public String generateOnePointTwentyOneGigawatts() {
return "newk-you-ler";
}
}
Example Graph: Guice Configuration
Injector injector = Guice.createInjector(new AbstractModule() {
protected void configure() {
bind(EnergySource.class).to(Plutonium.class);
bind(FluxCapacitor.class);
bind(DeLorean.class);
}
});
.dot File
My
Grapher
code takes the above Injector and outputs a
.dot
file that describes a graph:
digraph injector {
"FluxCapacitor" -> "FluxCapacitor.()" [arrowhead=onormal];
"FluxCapacitor" -> "TimeCircuits" [label=timeCircuits]
"DeLorean" -> "DeLorean.()" [arrowhead=onormal];
"DeLorean" -> "TimeCircuits" [label=timeCircuits]
"DeLorean" -> "FluxCapacitor" [label=fluxCapacitor]
"DeLorean" -> "EnergySource" [label=energySource]
"EnergySource" -> "Plutonium" [arrowhead=onormal];
}
The Rendered Graph
Finally,
Graphviz renders the
.dot
file to a pretty picture:
This graph is a good start, but there's a long way to go. Unfortunately, I don't have the bandwidth to take this project to completion and am seeking a contributor. If you're interested, post a note on
the issue. Coding is its own reward!
# posted by Jesse Wilson
on Monday, June 23, 2008
0 comments
post a comment
Integer.class and int.class as Guice Keys
Shortly after
fixing arrays, I've found another
multiple representations bug. This problem is probably familiar - I'm confusing primitive types (like
int
) with wrapper types (like
Integer
).
It's one binding
The critical question: should these tests pass?
assertEquals(Key.get(int.class), Key.get(Integer.class));
assertEquals(TypeLiteral.get(int.class), TypeLiteral.get(Integer.class));
Currently these are non-equal, so Guice has special cases so that they both work. But some special cases are missing! Consider issue
116:
Injector injector = Guice.createInjector(new AbstractModule() {
protected void configure() {
bind(int.class).toInstance(1984);
}
});
assertEquals(1984, (int) injector.getInstance(int.class)); /* passes */
assertEquals(1984, (int) injector.getInstance(Integer.class)); /* passes */
assertEquals(1984, (int) injector.getInstance(Key.get(int.class))); /* passes */
assertEquals(1984, (int) injector.getInstance(Key.get(Integer.class))); /* passes */
assertNotNull(injector.getBinding(Key.get(int.class))); /* passes */
assertNotNull(injector.getBinding(Key.get(Integer.class))); /* fails! */
Should Key be fixed?
Yes. I think I'll change
Key
so that it always uses
Integer.class
, regardless of whether it was created with
int.class
or
Integer.class
. Otherwise, this stuff is just too prone to bugs. For example, our new Binder SPI can return
Provider<int>, even though that's not a valid type.
Should TypeLiteral be fixed?
Probably not. I'm leaning towards leaving it as-is. Consider an interface with these methods:
public boolean remove(Integer value)
public Integer remove(int index);
If I change
TypeLiteral
, then the "remove" method is ambiguous when I know the
TypeLiteral
of the parameter type. But as a side effect of being inconsistent with
Key
, this test will always fail:
TypeLiteral<Integer> primitive = TypeLiteral.get(int.class);
TypeLiteral<Integer> wrapper = new TypeLiteral<Integer>() {};
assertEquals(Key.get(primitive), Key.get(wrapper));
assertEquals(primitive, wrapper);
I think it's a fair compromise.
Fixing the right thing
By making my changes to
Key
and
TypeLiteral
, I can make Guice behave consistently throughout all of its APIs. I won't have to worry about users who bind both
int.class
and
Integer.class
. And I should be able to rip out some special cases both Guice and its extensions.
If there's a
Guice issue that's you'd like fixed, this is a great time to get your feelings heard. I'm spending a lot of time on the issues list, trying to decide what will make the cut for 2.0.
# posted by Jesse Wilson
on Sunday, June 01, 2008
0 comments
post a comment