Skip to content

MentalModel

Googler edited this page Oct 13, 2023 · 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 is a map

Fundamentally, Guice helps you create and retrieve objects for your application to use. These objects that your application needs are called dependencies.

You can think of Guice as being a map[^guice-map]. Your application code declares the dependencies it needs, and Guice fetches them for you from its map. Each entry in the "Guice map" has two parts:

  • Guice key: a key in the map which is used to fetch a particular value from the map.
  • Provider: a value in the map which is used to create objects for your application.

Guice keys and Providers are explained below.

[^guice-map]: The actual implementation of Guice is far more complicated, but a map is a reasonable approximation for how Guice behaves.

Guice keys

Guice uses Key to identify a dependency that can be resolved using the "Guice map".

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 Providers

Guice uses Provider to represent factories in the "Guice map" that are capable of creating objects to satisfy dependencies.

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 the Provider interface directly, they implement a Module to tell Guice how to configure the injector. Internally, Guice will create Providers for all the objects 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

Using Guice

There are two parts to using Guice:

  1. Configuration: your application adds things into the "Guice map".
  2. Injection: your application asks Guice to create and retrieve objects from the map.

Configuration and injection are explained below.

Configuration

Guice maps are configured using Guice modules (and Just-In-Time bindings). A Guice module is a unit of configuration logic that adds things into the Guice map. There are two ways to do this:

  • Adding method annotations like @Provides
  • 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()

Injection

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 Guice 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 of its own, 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.

[^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