神刀安全网

DIY: Build Your Own Dependency Injection Library

We’ve heard a thousand times why Dependency Injection is good. We know how to use it: put an @Inject here, an @Singleton there, and now we’re DI knights! But when the bits hit the fan, may the force be with us: we have no idea how these black magic libraries work. We move annotations around, rebuild, restart, revert, and finally tweet to Jake Wharton. DIY: let’s build our own DI lib in 5 minutes, and iterate on it to rebuild key features of Guice and Dagger. No more black magic, not even advanced science. It turns out, anyone can write their own DI lib.

See the discussion on Hacker News .

Transcription below provided by Realm: a replacement for SQLite that you can use in Java or Kotlin. Check out the docs!

Don’t worry, the audio gets awesome again at 2:25!

Get new videos & tutorials — we won’t email you for any other reason, ever.

About the Speaker: Pierre-Yves Ricau-

Pierre-Yves Ricau is an Android Baker at Square who enjoys good wine and low entropy code.

@piwai

Understanding What’s Beneath the Hood of Dependency Injection

public class VisitHandler {   private final Counter counter;   private final Logger logger;    public VisitHandler(Counter counter, Logger logger) {     this.counter = counter;     this.logger = logger;   }    public void visit() {    counter.increment();    logger.log("Visits increased: " + counter.getCount());   }  }

The goal is to understand what’s going on underneath the hood. So we have a class that has a visit handler, it has a counter that can be incremented, and a logger. We now write a Java program that will use this:

public class Counter {  private int count;   void increment() {   count++;  }   int getCount() {   return count;  } }

This isn’t very interesting, but we’re looking at the pattern of dependency injection. Writing all that code which is pretty simple here can be very complex in a big app. What’s even harder is that the order in which you need to create things is not always clear. Dependency injection libraries solve that for you, and we do that by creating an object graph.

Creating Object Graphs

An object graph is something that can create instances for you. It’s configured – so you pass it a key, and you say I want the thing that is identified with this key X, and it gives you an instance. So here, for simplification purposes, our key here is actually going to be a class literal. And then it’s going to return an instance of that class literal. Okay. So what is object graph? Well object graph is a class that you pass it a key, it returns an instance of that key.

public class ObjectGraph {  public<T> T get(Class<T> key) {   throw new UnsupportedOperationException("Not implemented");  } }

Let’s look at how we could implement that. All we need to actually make object graph work is two classes. We need a factory interface and we need a linker. The factory is the thing that can create instances for a given key. And the linker is essentially a map of factories. It associates each key with a factory.

public interface Factory<T> {  T get(Linker linker); }  public class Linker {  private final Map<Class<?>, Factory<?>> factories = new HashMap<>();   public <T> void in }

How is the object graph actually implemented? It has a linker. You pass it the key, and you ask the factory for an instance of the object.

public class ObjectGraph {  private final Linker linker;   public ObjectGraph(Linker linker) {   this.linker = linker;  }   public<T> T get(Class<T> key) {   Factory<T> factory = linker.factorFor(key);   return factory.get(linker);  } }

Coming back to the main , we build the object graph, which means we create a linker, install a bunch of factories on it and then we create an object graph from it.

public static void main(String... args) {  ObjectGraph objectGraph - buildObjectGraph();  VisitHandler visitHandler = objectGraph.get(VisitHandler.class);   visitHandler.visit(); }

Now on to the meat of the configuration:

private static void installFactories(Linker linker) {  linker.install(VisitHandler.class, new Factory<VisitHandler>() {   @Override public VisitHandler get(Linker linker) {    Factory<Counter> counterFactory = linker.factoryFor(Counter.class);    Factory<Logger> loggerFactory = linker.factoryFor(Logger.class);    Counter counter = counterFactory.get(linker);    Logger logger = loggerFactory.get(linker);          return new VisitHandler(counter, logger);   }  });     linker.install(Logger.class, new Factory<Logger>() {      @Override public Logger get(Linker linker) {        Factory<PrintStream> printStreamFactory = linker.factoryFor(PrintStream.class);         return new Logger(printStreamFactory.get(linker));      }    });     linker.install(PrintStream.class, new Factory<PrintStream>() {     @Override public PrintStream get(Linker linker) {    return System.out;   }    });     linker.install(PrintStream.class, ValueFactory.of (System.out)); }

Visit handler needs a counter and a logger, to get those, it needs a factory for the counter and a factory for the logger. It asks the linker, then it’s going to get the two instances and then it’s going to create the visit handler instance. The logger is the exact same thing. Get the factory for the print stream.

It then calls print stream factory that gets and creates a linker from it. What’s going on is whenever you try to create a logger instance, it’s going to call the method, and this is going to call into the linker, which creates the factory for the print stream. So just by creating this binding, this configuration, everything is going to work like magic.

linker.install(Counter.class, new Factory<Counter>() {      @Override public Counter get(Linker linker) {         return new Counter();      }    });

We have a counter instance, and we want to share that counter across the application because we don’t want to have five counters. We wrap the factory into a singleton factory. All it’s doing is caching the call to get after the first time.

It’s very simple, and not we have a singleton factory. We can see that all it’s doing is when you get , it says “Oh, do I have an instance? No, okay I’m going to call the real factory.”

We’re going to get two different instances if we ask for two instances of this visit handler, which is a lot of work. But there is a way we can cache the work so we don’t have to do it twice.

We had this factory interface which takes a linker, asks for all the dependencies and then returns an instance. What we can actually do is split that in two methods. One is going to be link and it’s when you retrieve the factories for the dependencies. And the other one’s going to be get when you actually create instances.

public interface Factory<T> {  T get(Linker linker); }  public abstract class Factory<T> {  protected void link(Linder linker) {   }   public abstract T get(); }

So if we look back at the linker – the way things are actually going to work is that instead of just retrieving from the key when you get a factory (install is the same), we’re going to have two maps. Factories and linked factories. With linked factories, we try to find a factory in the linked factory. If it’s not there, we get it from the non-linked factory.

So a linked factory is a factory that has its dependencies satisfied.

Let’s come back to our example. We had this big install where we would get all the factories and then get the instances and then create the visit handler, well we’re going to split that in two now. Linking is going to be where we retrieve the factories, and creating instances is going to be where we… create instances .

If we add a little bit of logging to our linker – when do we get the factory, when do we link the factory – we see that the first time we do get , link , get , link , etc, but the second time we only get the factory and we’re done. We’ve optimized everything and now it’s faster.

Reducing Boiler Plate

How do we reduce boiler plate? It can be done where we load the factories, so we have the list of linked factories.

public class Linker {  private final Map<Class<?>, Factory<?>> factories = new HashMap<>();  private final Map<Class<?>, Factory<?>> linkedFactories = new HashMap<>();   public <T> void install(Class<T> key, Factory<T> factory) {   factories.put(key, factory);  }   public <T> Factory<T> factoryFor(Class<T> key) {       System.out.println("Get factory for " + key);       Factory<?> factory = linkedFactories.get(key);        if (factory == null) {        System.out.println("Link factory for " + key);        factory = factories.get(key);        factory.link(this);        linkedFactories.put(key, factory);       }       return (Factory<T>) factory;   }  }

Here’s where we introduce the magical annotation @inject , which just adds metadata to the class, to the constructors more specifically. It identifies a specific constructor. If we go back to the code, the load factory, I can look at it at runtime using reflection. I can try to find this @inject constructor on the key class, and if I find it I can create a reflective factory. I can look for @singleton annotations on the class and then wrap the factory in a singleton factory. How do you find an @inject constructor? Well you just get all the constructors and look for the one that’s annotated with @inject .

public class VisitHandler {  private final Counter counter;  private final Logger logger;   @Inject public VisitHandler(Counter counter, Logger logger) {   this.counter = counter;   this.logger = logger;  }   public void visit() {   counter.increment();   logger.log("Visits increased: " + counter.getCount());  } }

public class ReflectiveFactory<T> extends Factory<T> {  private final Constructor<T> constructor;  private final ArrayList<Factory<?>> factories = new ArrayList<>();   public ReflectiveFactory(Constructor<T> constructor) {   this.constructor = constructor;  } }

Ask for the factory for each parameter of the constructors. So all you need to do is call them in order and then pass, create an area of the arguments and pass it to the constructors here.

So with that, this is our new configuration. The only thing left is to bind PrintStream to System.out so there’s no annotation involved here. This is basically Google Guice. There’s a lot more to Google Guice, but this is the same basic way that it works.

The problem is with this, as we saw in the reflective factory is using reflection to detect the dependencies and also to create instances. So if we look at how we originally installed the manual factories, with anonymous classes. This anonymous class here could become a real class that we’re going to generate at compile time. And it’s exactly the same code except we’re going to generate it.

Generating a Real Class at Compile Time

We add $$factory to the name. You’ll notice we need to handle the singletons. So what we’re going to do is we’re going to have a parent superclass that has a singleton constructors param. And then in the generated class, we’re going to pass true or false which is going to say whether this is a singleton or not. And all of that is going to be generated at compile time by looking at the annotations on the class.

public class Logger$$Factory extends Factory<Logger> {  Factory<PrintStream> printStreamFactory;   @Override public void link(Linker linker) {   printStreamFactory = linker.factoryFor(PrintStream.class);  }   @Override public Logger get()  {   return new Logger(printStreamFactory.get());  } }

Loading the Generated Factory

All you need to do is you have the key, which is the name of the class you’re looking for and append $$factory . The one which is the one we generated. We look for that class. We wrap it into a singleton factory if it’s a singleton and then we return it. Which means all we need to change in load factory is to look for the generated factory by reflection.

public abstract class GeneratedFactory<T> extends Factory<T> {  public static <T> Factory<T> loadFactory(Class<T> key) {   String generatedClass = key.getName() + "$$Factory";   try {    Class<GeneratedFactory<T>> factoryClass = (Class<GeneratedFactory<T>>) Class.forName(generatedClass);    GeneratedFactory<T> factory = factoryClass.newInstance();    if (factory.singleton) {     return SingletonFactory.of(factory);    } else {     return factory;    }   } catch (Exception e) {    return null;   }  } }

This is basically Dagger 0.5, but not 1.0 yet.

Why is it not Dagger 1? There are a few bits missing.

Let’s look at another example. Suppose I have a database manager which needs a database. So I come back to my main and ask the object graph, “Give me my database manager.” But when I try to run this code it’s not happy – it’s saying “Oh no, I can’t find a factory for database. The database is not annotated with anything. I don’t know what to do with it.”

You forgot the @inject annotation. It’s only when you actually ask for the database manager instance that you’re going to realize that something’s missing. Imagine you have a server or an app and then it’s only when you get into this screen of the app that it’s going to realize that something’s missing.

So let’s imagine that we had a validate method on the object of operate where we say, “Hey, I actually don’t want an instance but I want to make sure that this will work when I ask for it, and otherwise please throw an exception.” All we need to do to validate is ask for the factory of that key.

public class TransactionHandler {  private final Analytics analytics;  private final TaxCache taxCache;   private Payment paymentInFlight;  private Order order;   @Inject TransactionHandler(Analytics analytics, TaxCache taxCache) {   this.analytics = analytics;   this.taxCache = taxCache;  } }

The singleton factory or singleton binding here has a reference to the binding and also keeps cache reference of the transaction handler, because transaction handler is a singleton. This means that as long as I keep the object graph in memory, all of my singletons are going to be kept in memory, which is the point.

If I don’t want the singleton anymore, I want to recycle it.

If you have an object graph that hauls through a transaction handler, the only way to make sure the transaction handler gets recycled is to recycle the object graph. So if it has a reference to a print handler but you didn’t want to get rid of the print handler, now it’s bad because you’re removing the whole thing. What you need to do is have one object graph for the transaction handler, and one object graph for the print handler. That’s great.

It turns out most of the time you actually don’t want two object graphs. You want a hierarchy of object graphs. And as soon as you leave that part of the app, you de-reference the object graph and then all of the instances get garbage collected. We need to update our vocabulary because the things that are in here are not singletons anymore – they are scoped instances, meaning a singleton is supposed to be only one instance ever. But here, if we destroy this object graph, and then create a new one and create a new transaction handler, we actually have two instances of transaction handler. So scoped instances is the new singleton.

We now have a hierarchy. Which means we’re introducing the notion of a parent object graph. And when you ask for something, it’s actually going to ask the parent first. It’s going to ask the parent object graph, “Can you provide me with that instance?” If it can’t then it’s going to ask the current. So you can imagine that when you are at the leaf graph, it’s going to go all the way up, then it goes down and provides that instance.

But if we look at how we implemented linker, our linker doesn’t actually have the ability to say “No, I can’t provide that.” It just tries to load the instance, and that’s not great because we want some singletons to be in the leaf instances and some of them to be in the root instances.

To do this, at compile time we actually have access to all the information, because we have these entry points, the things are on @moduleinjecticle .

We have these entry points, and we can go through all the dependencies. Which means we know exactly in which module/scope an object is supposed to live. So when we create a sub scope, a sub object graph, we’re actually going to go through the list of injects for the parent of the graph and we’re going to try to load the factory for each of them, which means they’re going to be cached in the link factories. And then we prevent the linked factories from ever being updated again. This means when a child asks for something that the parent has never seen before, the parent is not going to try to load it.

We got rid of the linker. We generate the right factory phrase. And then we need one central place where we are going to assemble everything together and generate the code that is going to be responsible for creating the logger factory, creating the print stream factory. So this was our module before. If you remember at runtime we have a bunch of modules and we passed the modules in the constructor of the object graph, and we created the object graph from that. So instead of doing that, we’re actually going to replace the object graph with a class that’s going to be generated at compile time. And that’s a component.

Implementing MainComponent

public class Dagger_MainComponent implements MainComponent {  Factory<PrintStream> printStreamFactory;  Factory<Logger> loggerFactory;  Factory<DatabaseManager> databaseManagerFactory;   Dagger_MainComponent() {   final MainModule mainModule = new MainModule();   printStreamFactory = new Factory<PrintStream>() {    @Override public PrintStream get() {     return mainModule.providePrintStream();    }   };

What’s interesting here is how you implement MainComponent . This is all generated at compile time again. Dagger main component is a generated class that’s going to implement MainComponent . When we create it, what we actually do is we create an instance of the module. Then we can create a printStreamFactory that delegates to the provider method of the module. So now we have a field that’s printStreamFactory . The we can send the logger factory field to the generated logger factory class, and just have it reference to printStreamFactory which is also filled in this class. Same thing for the database manager factory. And lastly, you can see that when we want to get a database manager instance, we just call the factory.

};     loggerFactory = new Logger$$Factory(printStreamFactory);  databaseManagerFactory = new DatabaseManager$$Factory(loggerFactory); }  @Override public DatabaseManager databaseManager() {  return databaseManagerFactory.get(); }

So all of this is pretty much what the Dagger 2 generates in terms of code. Which means we got rid of the linker, we got rid of the object graph, and everything is much faster. And there is no linking anymore. It’s basically when the component is created, all the factories are created with the right dependencies immediately.

Conclusion

So that’s Dagger 2. I was scared of dependency injection libraries before, and now I’m not anymore. I wanted to show that anyone can really implement their own dependency injection library.

Q&A

  • So you’re in a multi-threaded environment, should you, as a developer, take any precautions when using a dependency injection library?

You should absolutely. The way we ignore that problem is by doing all the injection on the main thread. So anytime we require an instance from the object graph, it’s always on the main thread for us on Android. If you’re doing server work, it might be a little bit more tricky. In Dagger 1.0 the generated code for singletons is thread-safe, meaning if two threads try to get a singleton instance at the same time it’s going to do the right thing: The first one that hits it is going to cache it, and the second one that hits it is going to reach into the cache. So that is thread-safe.

See the discussion on Hacker News .

Get new videos & tutorials — we won’t email you for any other reason, ever.

Realm: a replacement for SQLite that you can use in Java or Kotlin.Check out the docs!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » DIY: Build Your Own Dependency Injection Library

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址