Framework-driven dependency injection, as practiced by many OO programmers, tends to have considerable and underappreciated drawbacks. This talk goes into detail about why.
3. Problems with DI frameworks
• Cognitive overheard, confusing semantics
• Smear application-level responsibility everywhere
• Makes it too easy to not think about responsibilities
• Makes it harder to reason about code in isolation
• Throws tech at a problem that shouldn’t exist
4. What problems do DI frameworks solve
• “Factories aren’t fun”
• “Think of Guice’s @Inject
as the new new”
Guice Dagger 2
• “App assembly can be 100KLOC”
• ”Replace FactoryFactory classes”
• “Swapping dependencies for
testing”
Spring
• “Initialisation ordering”
7. More constructively:
Actual problem
• Of the arguments we need to pass, some are selected once for the application
and never changed. It would be bad to select them over and over again
Actual solution
• Only one place should know that it is in an ”application”
• Wire up the things that never change here
• Use good software engineering to reduce the number of things
9. Case study – Video Rental API
Video Search
Controller
Payment
Controller
Video
Searcher
Payments
Payments
Gateway
Rental
Controller
Rentals
Video Repo
Video DB
Analytics
Logging
3rd Party
10. What if there were no objects?
handleVideoSearch
Request
handlePayment
Request
findByName
findSimilar
charge
transferFrom
Account
handleRental
Request
rentVideo
runVideoQuery
record
Debug,
warn,
error
11. public class Rentals {
public static
Receipt rentVideo(Video v, VideoStore store, Customer c) {
store.decrementStock(vid);
Payments.charge(c.getCreditCard(), v.getPrice());
List<Movie> recommendations =
VideoSearcher.findSimilar(v);
Logger.debug(LogMessages.VIDEO_RENTED);
return new Receipt(v.getId(), c.getId(), recommendations);
}
}
12. public class Rentals {
public static
Receipt rentVideo(Video v, VideoStore store, Customer c) {
store.decrementStock(vid);
Payments.charge(c.getCreditCard(), v.getPrice());
List<Movie> recommendations =
VideoSearcher.findSimilar(v);
Logger.debug(LogMessages.VIDEO_RENTED);
return new Receipt(v.getId(), c.getId(), recommendations);
}
}
21. Trickling up is very, very bad
1. Punches through abstraction layers
2. Changes ripple
3. Same arguments must be supplied many times to many places, but
for only one reason
28. Objects = 9
handlePayment
Request
Video Repo
Video DB
Video
Searcher
Rentals
Video Search
Controller
Rental
Controller
Analytics
Logging
Payments
Gateway
3rd Party
Payments
29. Objects = 10
Video Repo
Video DB
Video
Searcher
Rentals
Video Search
Controller
Rental
Controller
Analytics
Logging
Payments
Gateway
3rd Party
Payments
Payment
Controller
30. Object creep is real!
…but isn’t that ok?
We’re writing OO software,
after all
38. The place where the wiring happens
var logger = LoggingFramework.newLogger();
var analytics = MediaBubbleMonkeyAnalytics.create();
var conn = new SpecificDatabaseConnection(connString);
var videoRepo = new VideoRepo(conn);
var videoSearcher = new VideoSearcher(videoRepo);
var gateway = new PaymentsGateway(url);
var payments = new Payments(gateway);
var rentals = new Rentals(videoSearcher, payments);
var videoSearchController = new
VideoSearchController(videoSearcher);
var rentalSearchController = new
RentalSearchController(rentals);
var paymentsController = new PaymentsController(payments);
39. Some people say “yeah, I
can manage it myself”… but
in an Android application,
we had 3000 LOC. In a large
server-side app, it flushes
out to about 100k LOC.
Greg Kick, Dagger 2 author
(Google)
40. “I know I am in an application, and what it is for”
var logger = LoggingFramework.newLogger();
var analytics = MediaBubbleMonkeyAnalytics.create();
var conn = new SpecificDatabaseConnection(connString);
var videoRepo = new VideoRepo(conn);
var videoSearcher = new VideoSearcher(videoRepo);
var gateway = new PaymentsGateway(url);
var payments = new Payments(gateway);
var rentals = new Rentals(videoSearcher, payments);
var videoSearchController = new
VideoSearchController(videoSearcher);
var rentalSearchController = new
RentalSearchController(rentals);
var paymentsController = new PaymentsController(payments);
41. DI frameworks help reduce code in that one spot
….but:
“I know I’m in an application” is smooshed
everywhere
42. public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
43. public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
Who am I? What do I
exist for?
44. What is the problem
that makes me say
“Aha! I’ll use Rentals
to solve it”
*** MOST IMPORTANT SLIDE IN THE WHOLE TALK ***
Programmer
45. public class Rentals {
...
public Receipt rentVideo(Video v, VideoStore store,
Customer c) {
...
}
}
Use me if you want
to rent a video.
46. public class Rentals {
...
public Receipt rentVideo(Video v, VideoStore store,
Customer c) {
...
}
}
What’s that you say,
I’m in an “app”? I
have no idea what
this means
47. public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
I don’t care in the
slightest which
VideoSearcher,
Payments or Logger
we use
48. @Component(“Bob”)
public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
@Autowired
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
But…
49. @Component(“Bob”)
public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
@Autowired
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
There’s only one true
Rentals. Create THE
Rentals for the whole app
to use. They can call it
“Bob”.
50. @Component(“Bob”)
public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
@Autowired
public Rentals(VideoSearcher search, Payments p, Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
I want THE VideoSearcher, THE
Payments, and THE Log
defined for the app.
52. @Component(“Bob”)
public class Rentals {
private final VideoSearcher search;
private final Payments p;
private final Logger log;
@Autowired
public Rentals(@Qualifier(“internetSearch”) VideoSearcher search,
@Qualifier(“creditPayments”) Payments p,
@Qualifier(“log4J”) Logger log) {
this.search = search;
this.p = p;
this.log = log;
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
...
}
}
I want a really really specific
VideoSearcher, Payments, and
Log that we get from a global
register somewhere.
I don’t care in the
slightest which
VideoSearcher,
Payments or Logger
we use
?????
53. public class Rentals {
public Receipt rentVideo(Video v, VideoStore store,
Customer c) {
store.decrementStock(vid);
p.charge(c.getCreditCard(), v.getRentalPrice());
List<Movie> recommendations = search.findSimilar(v);
log.debug(LogMessages.VIDEO_RENTED);
return new Receipt(v.getId(), c.getId(),
recommendations);
}
}
Use me if you
want to rent a
video.
THEREFORE:
1.
2.
3.
4.
54. Test that we can rent a video:
For some valid VideoSearcher, Payments, and Logger
1. Check the VideoStore got decremented
2. Check the Payments was charged
3. Check the logger received a LogMessages.VIDEO_RENTED message
4. Check our receipt has the expected data, including the expected
recommended videos
ALIGNS WITH OUR PURPOSE
55. public class RentalsTest {
VideoSearcher search = new FixedResultsSearcher(...);
Payments p = new InMemoryPayments();
InMemoryLogger log = new InMemoryLogger();
@Test
public void stockGetsDecremented() {
var rentals = new Rentals(search, p, log);
var store = new InMemoryVideoStore();
var video = new Video(“Weekend at Bernies”, Price.cents(250));
rentals.rentVideo(video, store.put(video,2), new Customer(...));
assertEquals(1, store.count(video));
}
...
}
56. public class Rentals {
private final StuffThatHappensFirst firstStuff;
...
@Autowired
public Rentals(@Qualifier(“decrementAndPayStuff”) StuffThatHappensFirst
firstStuff,
VideoSearcher search, Logger log) {
...
}
public Receipt rentVideo(Video v, VideoStore store, Customer c) {
firstStuff.doIt(store);
List<Movie> recommendations = search.findSimilar(v);
log.debug(“Video rented!”);
return new Receipt(v.getId(), c.getId(), recommendations);
}
}
Class responsibility has bled into the app!!!
62. class Multiplier {
private final
Adder adder = new Adder();
public int mult(int a, int b) {
int tot = 0;
for (int i = 0; i < a; i++)
tot = adder.add(tot,b);
return tot;
}
}
63. class Multiplier {
private final Adder adder;
public Multiplier(Adder a) {
adder = a;
}
public int mult(int a, int b) {
...
}
}
64. class Multiplier {
private final
Adder adder = new Adder();
public int mult(int a, int b) {
...
}
}
Who am I? What do I
exist for?
65. class Multiplier {
private final
Adder adder = new Adder();
public int mult(int a, int b) {
...
}
}
The point is:
Multiply 2 ints
into a resulting
int
Tool that
helps us do
our job
66. class Multiplier {
private final Adder adder;
public Multiplier(Adder a) {
adder = a;
}
public int mult(int a, int b) {
...
}
}
We do not care in
the slightest how
the ints get
combined
67. class Multiplier {
private final Adder adder;
public Multiplier(Adder a) {
adder = a;
}
public int mult(int a, int b) {
...
}
}
I exist to …
perform an action
on ints a given
number of times?
68. class GstPrice {
private final Price untaxed;
private final GstPolicy policy =
new GstPolicy();
public GstPrice(Price untaxed) {
this.untaxed = untaxed;
}
... // arithmetic, rounding, display, etc
}
69. class GstPrice {
private final Price untaxed;
private final GstPolicy policy =
new GstPolicy();
public GstPrice(Price untaxed) {
this.untaxed = untaxed;
}
... // arithmetic, rounding, display, etc
}
I exist to provide a
price that has
provably had GST
applied
70. class GstPrice {
private final Price untaxed;
private final GstPolicy policy =
new GstPolicy();
public GstPrice(Price untaxed) {
this.untaxed = untaxed;
}
... // arithmetic, rounding, display, etc
}
This is a tool I
need to do the
job. GST is the
point
71. class GstPrice {
private final Price untaxed;
private final GstPolicy policy =
new GstPolicy();
public GstPrice(Price untaxed) {
this.untaxed = untaxed;
}
... // arithmetic, rounding, display, etc
}
I do not care in
the slightest
which untaxed
price I am dealing
with
72. class TaxedPrice {
private final Price untaxed;
private final TaxPolicy policy;
public TaxedPrice(Price untaxed,
TaxPolicy policy) {
this.untaxed = untaxed;
}
... // arithmetic, rounding, display, etc
}
I do not care in
the slightest
which untaxed
price or tax policy
I am dealing with
73. // Mostly we don’t want to vary the policy,
so...
class TaxedPriceFactory {
private final TaxPolicy policy;
// ...Obvious constructor
public
TaxedPrice newTaxedPrice(Price untaxed) {
return new TaxedPrice(untaxed, policy);
}
}
74. • Don’t lazily turn every field into a
dependency; it increases
complexity and object creep.
• Think hard about responsibilities.
• Use new if it supports our reason
for existing.
84. • This is how a codebase goes to
hell
• Each dependency makes reuse,
extension, reasoning & testing
harder
• Frameworks make it too easy to
pretend it’s free
90. • “Setter injection” is a bad idea
• Public interfaces confuses usage & creation
concerns
• Nobody ever wants all that at once
• Mutable, uncertain – what order did things
happen in?
• Frameworks make “setter injection” too easy &
practical
92. public class Payments {
private final Gateway gateway;
public Payments(Gateway gateway) {...}
public Receipt charge(Price price) {...}
}
93. public class Payments {
public static
Receipt charge(Price price, Gateway gateway) {
...
}
}
94. public class Payments {
public static
Function<Gateway, Receipt> charge(Price price) {
...
}
}
95. public class Payments {
public static
NeedsGateway<Receipt> charge(Price price) {
...
}
}
Required flexibility is now
built in to return type; no
coathanger object required
98. Objects = 4 (just the IO points)
handleVideoSearch
Request
handlePayment
Request
charge
handleRental
Request
rentVideo
Video Repo
Video DB
findByName findSimilar
Gateway
Analytics
Logging
The problem has
vanished! There’s almost
nothing left to wire
99. Conclusion
• Passing arguments to things is not very hard
• Wiring together applications is not very hard
• Having a central “I am the application” place and thinking hard about
responsibilities gives you what you want
• DI frameworks are too complex, not necessary, and lead programmers
and teams toward sloppy design habits.