Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dagger: inject into different instances of one class

Can I use Dagger to inject different values into several instances of the same class that are deep inside the object graph? I want to avoid passing the values through the containing objects in the graph (so I can change the implementation of the contained objects without affecting their containers).

Here's a contrived example. The object graph is a Top, which contains a Left and Right, which each contain a Show. So there are two instances of Show.

class Top {
  Left left;
  Right right;
  void encodeTwice(String data) {
    left.encode(data);
    right.encode(data.getBytes());
  }
}
class Left {
  Leaf leaf;
  void encode(String data) {
    leaf.write(URLEncoder.encode(data));
  }
}
class Right {
  Leaf leaf;
  void encode(byte[] data) {
    leaf.write(DatatypeConverter.printBase64Binary(data));
  }
}
interface Leaf {
  void write(String data);
}
class Show implements Leaf {
  String label;
  @Override public void write(String data) {
    System.out.println(label + ": " + data);
  }
}
// There might be other classes that implement Leaf.

Can I use Dagger to inject different values into Top.left.leaf.label and Top.right.leaf.label?

Here's one attempt. This is unsatisfactory, because Left and Right depend on the implementation of Leaf. I want to inject Show.label without involving Left or Right.

ObjectGraph.create(new TopModule()).get(Top.class).encodeTwice("Hello!");

@Module(injects = Top.class)
public class TopModule {
  @Provides @Named("Left.leaf.label") String provideLeftLabel() {
    return "URL encoded";
  }
  @Provides @Named("Right.leaf.label") String provideRightLabel() {
    return "Base 64";
  }
}
class Top {
  @Inject Left left;
  @Inject Right right;
  void encodeTwice(String data) {
    left.encode(data);
    right.encode(data.getBytes());
  }
}
class Left {
  Leaf leaf;
  @Inject Left(@Named("Left.leaf.label") String label) {
    leaf = new Show(label);
  }
  void encode(String data) {
    leaf.write(URLEncoder.encode(data));
  }
}
class Right {
  Leaf leaf;
  @Inject Right(@Named("Right.leaf.label") String label) {
    leaf = new Show(label);
  }
  void encode(byte[] data) {
    leaf.write(DatatypeConverter.printBase64Binary(data));
  }
}
interface Leaf {
  void write(String data);
}
class Show implements Leaf {
  String label;
  Show(String label) {
    this.label = label;
  }
  @Override public void write(String data) {
    System.out.println(label + ": " + data);
  }
}
like image 333
John Kristian Avatar asked Dec 26 '22 14:12

John Kristian


1 Answers

So this is hard to reason about with such abstract types because based on what Left, Right, and Top actually are I might change approaches.

There are three approaches to this problem that I can think of: a Leaf factory, differentiation of Leafs by qualifier, differentiation of Leafs by child graphs.

Factory

Hide the Leaf implementation behind a factory that takes its required dependencies.

interface LeafFactory {
  Leaf create(String name);
}

class TopModule {
  // ...

  @Provide @Singleton LeafFactory provideLeafFactory() {
    return new LeafFactory() {
      @Override public Leaf create(String name) {
        return new Show(name);
      }
    };
  }
}

class Left {
  private final Leaf leaf;

  @Inject Left(@Named("..") String name, LeafFactory leafFactory) {
    leaf = leafFactory.create(name);
  }

  // ...
}

Qualifier

Right now you are injecting the label via a qualifier annotation. There's no reason you couldn't do the same thing but with Leaf instances.

class TopModule {
  @Provides @Named("..") Leaf provideLeftLeaf() {
    return new Show("URL encoded");
  }

  @Provides @Named("..") Leaf provideRightLeaf() {
    return new Show("Base64 encoded");
  }
}

class Left {
  @Inject Left(@Named("..") Leaf leftLeaf) { .. }
}

Note: You can still inject the label names as arguments to the provideFooLeaf methods if you need that abstraction as well.

Child Graphs

By extending the graph with a "leaf scope" you can create separate parts of your application (e.g., "right", "left") which only have a single Leaf instance.

@Module(addsTo = TopModule.class)
class LeafModule {
  private final String label;

  LeafModule(String label) {
    this.label = label;
  }

  @Provide @Singleton Leaf provideLeaf() {
    return new Show(label);
  }
}

Now we can .plus() this onto the root object graph to get a scope.

ObjectGraph ogRoot = ObjectGraph.create(new TopModule());
// ...
ObjectGraph ogLeft = ogRoot.plus(new LeafModule("URL encoded"));
Leaf left = ogLeft.get(Leaf.class);
ObjectGraph ogRight = ogRoot.plus(new LeafModule("Base64 encoded"));
Leaf right = ogRight.get(Leaf.class);

Like I said, this is hard to generalize to fit the abstract sample app you've outlined. But I hope you can see how child graphs allow you create a specialized version of the graph which is a superset of the "root" graph. By doing this multiple times you can alter the dependencies which are used to inject your types.

like image 170
Jake Wharton Avatar answered Jan 06 '23 17:01

Jake Wharton