Skip to content

MentalModel

Googler edited this page Apr 21, 2021 · 11 revisions

Guice Mental Model

Learn about Key, Provider and how Guice is just a map

When you are reading about Guice, you often see many buzzwords ("Inversion of control", "Hollywood principle", "injection") that make it sound confusing. But underneath the jargon of dependency injection, the concepts aren't very complicated. In fact, you might have written something very similar already! This page walks through a simplified model of Guice's implementation, which should make it easier to think about how it works.

Note: While this page doesn't assume any prior knowledge of Guice or dependency injection, it does assume a prior working knowledge of Java, including modern Java syntax with annotations, method references, and lambdas, as well as knowledge of common object-oriented programming principles and patterns.

Guice keys

Guice uses Key to identify a dependency that can be resolved.

The Greeter class used in the Getting Started Guice declares two dependencies in its constructor and those dependencies are represented as Key in Guice:

  • @Message String --> Key<String>
  • @Count int --> Key<Integer>

The simplest form of a Key represents a type in Java:

// Identifies a dependency that is an instance of String.
Key<String> databaseKey = Key.get(String.class);

However, applications often have dependencies that are of the same type:

final class MultilingualGreeter {
  private String englishGreeting;
  private String spanishGreeting;

  MultilingualGreeter(String englishGreeting, String spanishGreeting) {
    this.englishGreeting = englishGreeting;
    this.spanishGreeting = spanishGreeting;
  }
}

Guice uses binding annotations to distinguish dependencies that are of the same type, that is to make the type more specific:

final class MultilingualGreeter {
  private String englishGreeting;
  private String spanishGreeting;

  @Inject
  MultilingualGreeter(
      @English String englishGreeting, @Spanish String spanishGreeting) {
    this.englishGreeting = englishGreeting;
    this.spanishGreeting = spanishGreeting;
  }
}

Key with binding annotations can be created as:

Key<String> englishGreetingKey = Key.get(String.class, English.class);
Key<String> spanishGreetingKey = Key.get(String.class, Spanish.class);

When an application calls injector.getInstance(MultilingualGreeter.class) to create an instance of MultilingualGreeter. This is the equivalent of doing:

// Guice internally does this for you so you don't have to wire up those
// dependencies manually.
String english = injector.getInstance(Key.get(String.class, English.class));
String spanish = injector.getInstance(Key.get(String.class, Spanish.class));
MultilingualGreeter greeter = new MultilingualGreeter(english, spanish);

To summarize: Guice Key is a type combined with an optional binding annotation used to identify dependencies.

Guice Provider

Guice uses Provider to represent factories that are capable of creating objects to satisfy dependency requirements.

Provider is an interface with a single method:

interface Provider<T> {
  /** Provides an instance of T.**/
  T get();
}

Each class that implements Provider is a bit of code that knows how to give you an instance of T. It could call new T(), it could construct T in some other way, or it could return you a precomputed instance from a cache.

Most applications do not implement Provider interface directly, they use Module to configure Guice injector and Guice injector internally creates Providers for all the object it knows how to create.

For example, the following Guice module creates two Providers:

class DemoModule extends AbstractModule {
  @Provides
  @Count
  static Integer provideCount() {
    return 3;
  }

  @Provides
  @Message
  static String provideMessage() {
    return "hello world";
  }
}
  • Provider<String> that calls the provideMessage method and returns "hello world"
  • Provider<Integer> that calls the provideCount method and returns 3

Guice mental model

A good way to think about Guice is to imagine it as a big map of Map<Key<?>, Provider<?>> (Of course, that's not type safe because the two wildcard generics won't match. This is a simplified mental model, not a rigorous implementation.)

Like Providers, most applications do not create Key directly but rather use Modules to configure the mapping from Key to Provider.

Modules add things into the map

Modules exist as bundles of configuration logic that just add things into the Guice map. There are two ways to do this: either using method annotations like @Provides, or by using the Guice Domain Specific Language (DSL).

Conceptually, these APIs simply provide ways to manipulate the Guice map. The manipulations they do are pretty straightforward. Here are some example translations, shown using Java 8 syntax for brevity and clarity:

Guice DSL syntax Mental model
bind(key).toInstance(value) map.put(key, () -> value)
(instance binding)
bind(key).toProvider(provider) map.put(key, provider)
(provider binding)
bind(key).to(anotherKey) map.put(key, map.get(anotherKey))
(linked binding)
@Provides Foo provideFoo() {...} map.put(Key.get(Foo.class), module::provideFoo)
(provider method binding)

DemoModule adds two entries into the Guice map:

  • @Message String --> () -> DemoModule.provideMessage()
  • @Count Integer --> () -> DemoModule.provideCount()

You don't pull things out of a map, you declare that you need them

This is the essence of dependency injection. If you need something, you don't go out and get it from somewhere, or even ask a class to return you something. Instead, you simply declare that you can't do your work without it, and rely on someone else to give you what you need.

This model is backwards from how most people think about code: it's a more declarative model rather than an imperative one. This is why dependency injection is often described as a kind of inversion of control (IoC).

Some ways of declaring that you need something:

  1. An argument to an @Inject constructor:

    class Foo {
      private Database database;
    
      @Inject
      Foo(Database database) {  // We need a database, from somewhere
        this.database = database;
      }
    }
  2. An argument to a @Provides method:

    @Provides
    Database provideDatabase(
        // We need the @DatabasePath String before we can construct a Database
        @DatabasePath String databasePath) {
      return new Database(databasePath);
    }

This example is intentionally the same as the example Foo class from Getting Started Guide, adding only the @Inject annotation on the constructor, which marks the constructor as being available for Guice to use.

Dependencies form a graph

When injecting a thing that has dependencies, Guice recursively injects the dependencies. You can imagine that in order to inject an instance of Foo as shown above, Guice creates Provider implementations that look like these:

class FooProvider implements Provider<Foo> {
  @Override
  public Foo get() {
    Provider<Database> databaseProvider = guiceMap.get(Key.get(Database.class));
    Database database = databaseProvider.get();
    return new Foo(database);
  }
}

class ProvideDatabaseProvider implements Provider<Database> {
  @Override
  public Database get() {
    Provider<String> databasePathProvider =
        guiceMap.get(Key.get(String.class, DatabasePath.class));
    String databasePath = databasePathProvider.get();
    return module.provideDatabase(databasePath);
  }
}

Dependencies form a directed graph, and injection works by doing a depth-first traversal of the graph from the object you want up through all its dependencies.

A Guice Injector object represents the entire dependency graph. To create an Injector, Guice needs to validate that the entire graph works. There can't be any "dangling" nodes where a dependency is needed but not provided.[^3] If the graph is invalid for any reason, Guice throws a CreationException that describes what went wrong.

Usually, a CreationException just means that you're trying to use something (like a Database), but you forgot to include the module that provides it. Other times, it can mean that you mistyped something.

[^3]: The reverse case is not an error: it's fine to provide something even if nothing ever uses it—it's just dead code in that case. That said, just like any dead code, it's best to delete providers if nobody uses them anymore.

What's next?

Learn how to use Scopes to manage the lifecycle of objects created by Guice and the many different ways to add entries into the Guice map.

Clone this wiki locally