Dependency Injection via Guice is great, but it does what you tell it. Sometimes, we need to do things that are counter to instinctive programming ideals when building agile, testable mobile applications to work around the constrained resources of mobile environments. Spotting these problems in code reviews isn’t always easy, because most of them cross the boundary layer of a single class. Their impact is measured on the cost of injecting a new instance, or the cost of constructing the injector itself.
Android has two major (and, generally, obvious) limitations that everyone using Guice should be aware of: Call stack depth isn’t infinite, and Guice’s implementation is stack-hungry. Here are some things to avoid if you’re looking to keep your stack footprint low:
- Long constructor chains
Foo needs a bar to construct. Bar needs a Baz. Baz needs a Bat. Guice is going to eat a big chunk of call stack to find a constructor, find the parameters to that constructor, locate constructors for the parameters, etc… Before you know it, you’ve blown your stack and thrown an error.
Break these up by using field or method injection in place of constructor injection to shrink the stack depth, or using provider methods. Where they make sense. The problem with this, of course, is that the resulting fields are non-final, and that makes my eyes bleed to read.
- Large object graphs
I’m not suggesting composition is bad. In fact, composition is almost always better, especially when it comes to anything remotely complex. But think carefully when you compose, and try to find ways to use the composed instances lazily, by injecting a provider of the instance in place of the instance.
Think about whether or not the class you have is just plain doing too much orchestration; consider doing a bit of functional decomposition of the problem domain.
- Costly instance initialization
The cost of creating an instance can be high, too - and not purely due to constructor chains being long. Consider delaying work outside of the constructor. Think about whether the instances you’re creating in your constructor should be delayed until the method is called, and use providers of instances to move that cost.
- Costly class initialization
Arguably, one of the whole points of using injection is getting away from hand-built singletons and static references to critical objects. If you’re doing this, you should think long and hard about whether you really want all that construction to live in the classloader, rather than when you needed it.
If you’re looking to solve the robot legs problem, do it with annotated types. @Left Leg and @Right Leg are better than private static final Leg LEFT and RIGHT, and push the job of construction down to its use, and out of the classloader window.
If you’re looking to solve a singleton problem, use a @Singleton. Avoid it where it makes sense to do so.
Last, if you’re trying to build complex maps, consider Guava’s MapMaker implementation, which can build maps of complex types by supplying a generator function at the time of lookup, rather than at the time of wrapper class creation.
- Contexts and Quasi-Singletons
If you’ve got an object with a strange lifecycle, create that lifecycle - @Request scope is a great example in Servlet-land, and you’ll doubtlessly find others if you look around carefully. It separates the lifecycle from the code that depends on the data, so that you’re not littering your code with details about what needs to live when and where, or creating messy context objects that might better have been represented by a scope.
- Singletons and Static Injection
A well-placed singleton, along with a bit of constructor chaining and lots of tight coupling, can load your whole object graph when your app launches, stopping up your startup. Think long and hard about what really needs to be a singleton; inject providers instead of instances where possible to reduce the cost of that singleton.
When using static injection, think about how you’d have written your code to avoid it, and then do that. When you can’t, use providers of instances in place of instances to decouple where possible.
Guice won’t magically make your application lazily load its required classes and instances. You can burn memory and stack by building highly dependent object graphs - Guice isn’t afraid to throw a verbose stack trace, as you’ve probably encountered previously; what it does is complex, and while it’s possible to inline all that code, it wouldn’t leave Guice very maintainable.
Constructors should ideally be slim and svelte. They should ensure they have exactly what they need to be constructed with, and those things which can sensibly be delayed until first use should be.
Ultimately, Guice is doing what you ask it to; if you ask it to build you a tightly coupled object graph, it will do so. You have the tools to implement lazily-dependent object graphs, should you choose to do so - and in mobile environments, where latency is critical, you should make every effort to do so in critical areas of your codebase. Record the amount of time it takes to perform critical operations; you can even report that back to centrally through metrics gathering like the Google Analytics client, which will give you a feel for your performance profile on different devices and operating system revisions.
Make metric-driven decisions. Don’t prematurely optimize, and when it comes time to do so, make careful decisions about decoupling. Gather metrics from your deployed applications to help you locate problems and improve quality.
I wish you luck.