droidcon SF 2018 - Motif - An Opinionated Dependency Injection API for Deep Graphs

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
all right everyone I'm Leland I do Android development on Hoover's mobile platform team and I'm also the author of motif which is our dependency injection library that we're open sourcing I thought about starting out this talk with kind of an overview of dependency injection and what it is and why it's important why it's useful but I think there's a lot of resources out there already there's a lot of presentations that have covered this topic so I'm gonna spare you all the dependency injection 101 and jump right into some specifics on motif itself and specifically let's dive into a question that I think is kind of the elephant in the room which is you know why do we need a new dependency injection system and first we'll talk about dagger versus motif and the first thing I'll say actually is that motif is actually an abstraction on top of dagger it offers a completely separate slimmed down simpler API but under the hood it's actually generating dagger but with that said let's take a look at dagger and motif API and some of the high-level concepts to keep in mind as we go through this presentation and as you consider whether or not motif is a right choice for you in your applications so let's take a look at dagger first dagger as we know is the industry standard it's been around for a while it's been proven in production in many different types of applications so we're bus framework that we all trust right and part of the reason why daggers gain this level of pop popularity is because of its flexibility and that is to say that dagger can satisfy a lot of different types of applications a lot of different structures of dependency injection graphs but with this flexibility daggers API is kind of grown and grown and as new use cases crop up the API is kind of added more and more feature features and again as a generic framework this is kind of the right decision for dagger because dagger needs to play that role of a kind of bass di framework that it can satisfy all the different use cases whether it be a small individual application developer or you know enterprise company building out an app dagger can satisfy all those use cases but ultimately that means that dagger has a lot of concepts to understand to spy all these different use cases so that will result in a higher or steeper learning curve for developers and also may take a toll on developer productivity all right so those are some of the points to keep in mind let's take a look at motif now motif is pre-release we've open sourced it a few months ago I think now we are using in production at uber so we've converted about a hundred of our dagger components to use motif and we haven't seen any problems in production so we're pretty confident in the robustness and the correctness of motif but the API might change slightly because it is still a pre-release library a second point on motif is that as opposed to daggers or in contrast to daggers flexibility motif is an opinionated framework so we have targeted a kind of narrow set of use cases as opposed to trying to solve all the all the patterns Under the Sun we've targeted if a few narrow use cases and because of that we've been able to design an API that's highly optimized for those use cases so what that results in is many fewer concepts to understand by the developer because we're not trying to solve all these cases that dagger does for instance and just a slimmer and more intuitive API okay so with that out of the way I think the next thing what we'll do is go into some of the background in why we actually got came to the decision of you know we need to build a new dependency injection framework because that's a very heavy-handed solution so there must be a good reason why we did that so take a look at that now and we'll start by just talking a little bit about mobile architectures in general and here I mean MVC MVP and vvm viper ribs but no matter what you're using out of these frameworks they're all trying to solve a similar problem and that is managing code complexity we know that mobile applications don't have to deal with the same sort of scaling issues that say a back-end service would back-end services might need to deal with things like requests per second a mobile application on the other hand deals with different types of scaling issues right we're we're we need to deal with things like scaling up number of developers work in the same codebase and dealing with code complexity scale and lines of code so what all these mobile architectures try to solve is dealing with those problems right and what they do is they solve that in kind of similar ways by breaking down that complexity into composable business units and well just from here on out call these scopes and depending on the framework you're using or the patterns are using you might be calling this something differently but we'll just use the generic scopes as a term that we'll use in this presentation so here's an example of how you might break down some application into scopes at the top you might have a root scope and if you're using MVC maybe this is a root controller with Associated views and models and when the user logs in maybe attach a logged in scope and with MVC again this might be a logged in controller with associated views and models and rib world that might be logged in router interactor and a builder but you keep on doing this and breaking this down and what this allows you to do is develop it allows you to isolate features so if you have many developers working the same codebase it allows developers to iterate and move quickly within their own feature without fear of breaking other parts of the application so that's a benefit of scoping your application in this way at uber we've taken this kind of to the extreme and we have over a thousand scopes in our code base and again this is because we have a large complicated application with the many lines of codes code and a lot of developers working on our code base we have over we have around 200 developers Android developers working on our app right so what all does this have to do with dependency injection if we're talking about dagger we could structure our di graph in many different ways here we could say there's only one component for the entire application so let's take that scenario if we have one component for the whole application there are some problems here the first problem I see is that no matter what scope you're working in all these dependencies have a global visibility so when we're when we're asking yourself what dependencies are available in my scope you actually have to consider every single dependency that's declared in your application second problem with this aside from visibility is life cycle and what I mean by that is that if you have only a single component for your entire application not all the dependencies that you would like to be on the dependency graph are available at app startup when you need to create that component so that doesn't really work that well either and you end up with a lot of stateful nullable dependencies right so no so ok so let's try to solve those problems maybe we can have several components for our application and kind of mitigate that issue a little bit the problem our sorry the solution that we've found to be the most effective is actually having a component and a module per each of these scopes and this gives us the highest level the most control over again dependency visibility and dependency lifecycle one of the conversations that comes up in design discussions around how do we structure our feature and how do we break our feature down is where do we put our dependency what's go at what scope should we place our dependency and oftentimes that dependency ends up being in the same scope that is consumed at and this is if that scope is the only scope consuming that dependency but let's say we have many scopes consuming the same dependency then our rule is that that dependency should live at the lowest common ancestor amongst all the scopes that are consuming that dependency but in this way we can have our dependency live in a place that limits the visibility and lifecycle of that dependency and the way we can do that is with this pattern having a component and a module per each of these scopes and you might be thinking now that while that sounds pretty painful to have a component and a module per every one of these over a thousand scopes that you have in your codebase and that's true and that's one of the reasons why we built motif but let's take a look at the actual code that's that is defined by each of these scopes for a dependency injection and this is a bare minimum for any of our scopes this is the just the basics that we use here and you can see right away that it's actually it's a little bit difficult to scan over this and understand exactly what's going on and of course advanced tagger users would be able to go line by line understand each of these concepts but just glancing over this it doesn't give you a good sense of the structure of what it's trying to represent so to help us out we'll just highlight the important bits so these are the bits that developers would often care about when they're reading this code or writing this code these are the pieces that don't change between scopes you can see that the signal-to-noise ratio here is pretty low there's a lot of surrounding boilerplate and I'm claiming that we don't necessarily care about that when reading this code given our patterns and we'll talk a little bit more about that later but first let's let's just go through each of these and talk about what they represent so of course the top level this is just the component interface definition tells us what scope we're in contains the all of the dependency injection logic right and the first method on that component is a component provision method so this says I need to be able to create a controller and when I call this all the code generated code please resolve all the dependencies and instantiate a route controller for me and return it to me and this is required because what we this is required so that some other code can have an instance of the component and then request to pull dependencies off of that component right so that's important to be able to do this next method is also very important this is what actually gives us that scope graph structure so that diagram we were looking at earlier this is what defines those edges it defines the parent-child relationship and this is a component sub component factory method that's returning a sub component builder right and as saying that logged in is a child of route moving into the component builder this methods a little bit more nuanced but it's essentially what it's saying that we are able to inject a V group into the child dependency graph another canonical example here would be an auth token so let's say you're in the root scope and you retrieve the auth token over the network and you want that auth token to be available in the child scope but it's obviously you can't provide that statically from the parent since it was retrieved over the network this is a way to inject that into the graph and have it be available on the dependency injection graph from logged in down to any of the descendents okay and finally this is a provides method in the module and this is probably the most familiar to us this is just telling dagger how to create a route view right you just call this method and pass in a via group as a dependency of route view now each of those concepts and all those concepts are not not very complicated and yet there's a lot of code involved to actually define and Express what we want to express here in Daggers world and this is the problem that we were seeing and godess starting a starting thinking about how we can actually abstract this repeated complexity and make our lives a little bit easier and in addition to the boilerplate in terms of lines of code there's also a lot of concepts here that we have to understand on the left-hand side here these are all the concepts that we use just in that example that we just looked at I'm not going to go through all of them because there's a lot on the right hand side we have some decisions that we have to make when defining our patterns at a platform level so should we be using components or component dependency sub components or component dependencies should we be using component builders explicitly defined by our developers or should we use dagger just a lot of dagger to generate that interface for us so these types of decisions we have to make when designing our patterns but the decisions we make don't change from scope to scope we define them once and then we just have developers implement that pattern for the scope that they're they're working in right but of course dagger doesn't know what our patterns are and doesn't know about the fact that we have a lot of scopes and have them deeply nested within each other so we have to tell dagger every we define a scope exactly what we want so this is one of the reasons why one of the things that adds to the repeated complexity that we're trying to avoid so how do we solve the fact that we have all this repeated configure dependency injection configuration well normally we've all kind of internalized strategies for factoring out duplicated logic so maybe we'll I you know immediately identify a a method that can be factored out of some duplicated code that we see or some helper method that might be useful in a certain scenario but what we're talking about here is a little bit different you can't factor out this sort of repeated patterns in the same way that you would for just brought Java logic right so we needed another strategy and that's exactly what motif is motif is an annotation processor that generates dagger code under the hood and in fact it's generating code that's very similar to that code that we would would have written manually and will tell we'll take a look at motifs API in a little bit but first I'll just point out a couple other things about motif the second point is that it offers full interoperability with dagger so we'll take a look at exactly what we mean by that in a little bit and also motif infers scope dependencies so just like sub sub components would in the dagger world if you define a scope that needs dependencies from the parent motif we'll actually be able to infer that without you having to explicitly define it so very much like sub components and dagger world and finally the driving principle behind motifs API design is just API simplicity so you'll see that when we get into some of the code samples for motif but this is something that we valued highly when we were designing motif okay so what is our di setup look like now that we're using motif instead of that single component in single module per scope we now have a single node scope per each of these scopes and again it's an abstraction on top of dagger so under the hood we're still generating the component and the module but we're dealing with a much much simpler API and a higher abstraction so here's a code that we use for motif and this is equivalent to the dagger code that we saw earlier and you can tell that it's obviously much cleaner and much shorter in terms of lines of code and more importantly there's many less fewer concepts that you need to learn to understand what's going on here so we'll do the same exercise where we highlight the important bits and the signal to noise ratio is obviously higher here right like when the code that you write out explicitly is almost all the code that's actually committed to source control and that's really important to us so we'll go through again each of these and kind of define what it's doing at the top level we have the root scope and this is analogous to the dagger component this is just the top-level interface that defines the container for all the dependency injection logic and motif does a very similar thing to what dagger does in that it will generate the implementation code generate the implementation of this root scope interface right and the first method here is exactly the same as the dagger component provision method we call it a motif access method but it does the same thing and has the same syntax it allows you to pull off of the root scope a root controller and internally the code generated implementation will know how to resolve all the root controller's dependencies and instantiate the root controller if you think back to the dagger example this is the sub component factory method that we were talking about and in the dagger world we returned a sub component builder here we're just returning another scope so we've kind of removed a layer of abstraction there and one difference that we'll see is that this actually takes in parameters and this is so that we don't need to use the binds instance annotation that we had in the dagger example in order to pass in those dynamic dependencies that we were talking about that auth token we just need to define a parameter to this child method and we can just pass it in when when calling this method okay moving into the objects class here this is analogous to the dagger module and this is where we define or we tell motif how to instantiate different dependency types so this first method tells us how to instantiate the route controller and you'll remember that there actually wasn't an analogous analogous method in the module for a dagger version and that's because in dagger this this type was constructor injected and dagger was able to infer that by looking at the constructor of this object and seeing that I was annotated with that inject so why didn't we do do that and leverage the same API for motif well we think it's really powerful to be able to look at the objects class and know that every every method that's defined there represents all of the dependencies that this scope can provide and we think this is really useful because in the dagger world not all the dependencies that are provided by a scope are actually listed in one place it could be scattered across your code base with constructor injected objects and finally this is the analogous of the view method that we were looking at in the dagger module and this is just saying in order to instantiate the route for you call this view method and pass in the V group as a dependency so the same thing just - some of the annotations that we had to declare in dagger all right so that's it that's it in terms of the motif API so let's look at the concepts that we went over this is a list of the object this list of the concepts that we used in that previous example and although that example is pretty basic this is almost all of motifs API so again driving home that idea of API simplicity just the basic example it covers most of the API there's maybe a few other concepts to learn but you can tell by just looking at these concepts that onboarding and more importantly long term understand ability and readability will be greatly increased if you are if you commit to the sort of patterns that motif can handle right okay so I'm gonna switch gears a little bit here and about something I mentioned a little earlier that some of you might be interested in which is dagger interoperability so we stripped out a lot of the other methods in this route scope and we've just left the child skip the logged in method here and again just to recap this defines logged in scope as a child or root scope here so when we talk about dagger interoperability we're gonna we're going to keep in mind two questions and the first question is what if logged in scope is a dagger component so can we have a dagger component be a child of a motif scope so this scenario would be something like if you had a by and large motif based application can you touch can you attach a dagger component as a child to that some feature that's already using motif and the second the converse is what a fruit scope is a dagger component so can we have a dagger component that's a parent of a motif scope so this is this would be maybe probably the more common use case which is if you have a application that's already using dagger can you attach a motif scope just to try it out or begin migrating so both of these are possible so we'll take a look at each scenario the first scenario of being dagger is a child of motif scope and the first thing you do is define your component as you normally would and define your dependencies as you normally would except instead of passing in a component class here you pass in a motif class a motif scope rather and the reason why this works is because motif scopes and dagger components share the same syntax for these provision methods and that's what dagger looks for on this dependencies definition so when you go to instantiate the component all you need to do is pass in an instance of the motif scope now and that's it so it's a pretty seamless integration and in fact dagger treats the motif scope exactly in the same way that it would treat a component dependency so the second scenario is dagger is a parent of motif scope and this is almost just as simple we said that motif generates an implement in implementation of the motif of the scope interface that you define and along with that code generated implementation it generates this dependencies interface and this lists out all of the dependencies that are required from the scopes parent so now what we can do is have the component directly satisfy all this dependencies by extending this dependencies interface and when we new up the scope we can just pass in an instance of the component as the dependencies and now we've set up dagger as a parent of motif scope so using these two strategies you can opt in or opt out of using motif however you want and this is really useful for instance just trying out motif or doing an incremental migration if you wanted to do that okay so that's kind of the end of the API portion of this and the comparison with dagger and I wanted to get into some architecture pieces because I think it enables some really cool things the first thing we'll look at in motifs architecture is the IRS this is the intermediate representation that abstracts away the representation of the java AST and we've implemented this both in the annotation processing world and also in the IntelliJ plugin world and we'll see what that looks like in a little bit but what's great about this is that the subsequent logic here and the subsequent flow doesn't know whether or not we're in the compiler or the IntelliJ world so we'll take a look at this IR for a second these methods are these interfaces on the Left are satisfied in two different worlds again which that with the java X dot link model namespace and this is the annotation processing models so the annotation processing representation of the java AST and the calm down in IntelliJ dot psi models and this is Intelligence version of the java AST in that this is the way IntelliJ represents Java source code right and diving a little bit more into that here the actual definitions of the compiler and the intelligence of these models so once we've implemented the IR the next step we need to do is take that representation of Java source code and transform it into motif aware models and these models would be things like access method child method factoring method objects class scope and along the way we can catch things like maybe we've defined a method on a scope that's it might be a valid java method but it doesn't make sense in motif world well we would catch that here and we'd throw a parsing error let's say so once we've got these motif models kind of these motif primitives we process it into validated graph because before this process graph step we didn't know what the dependency graph looks like so that's where we what we construct here we construct an in-memory representation of the dependency and the scope graph and this is also where we catch things like missing dependencies so we've defined a scope that has that requires dependencies that isn't satisfied by any of its parents or ancestors then we'll throw a missing dependencies error in this step here we'll also catch things like dependency cycles or scope cycles which aren't allowed but once we have that validated graph this in memory representation of the dependency graph will hand it off now to either the compiler to code generate our implementation of the scope interface that we define or we'll update the UI in some way and we'll take a look at that use case in a little bit and that's this piece here so again most of motifs logic is actually unaware of whether it's operating in the annotation processing world or in an intelligent plugin and that's really powerful and we'll see why here this is still under development but this is kind of what we envision for the future of IDE integration for a motif once we have a full graph understanding inside of an intelligent plugin we can do really cool things like real time graph validations we can all we all have run into that dreaded missing dependencies are missing bindings error in dagger world and we have to fix that in our source code and recompile to figure out whether or not that passes so with full integration with the IDE we can do this much quicker and have a much faster iteration cycle here and feedback loop and it behaves much more like a linter right either feedback is instantaneous and that's because the IntelliJ plug-in has full understanding of the graph we can also do things like visualizing the scope hierarchy inside the IDE and again we can trust that just as much as we trust the compiler because they share the same understanding of the graph in the same core logic another thing we can do is navigate to who provides a given dependency and that's really powerful when you're trying to track down which one of your ancestors actually gave me this dependency right so this is something we're really excited about it's not fully developed yet but it's it's definitely in the works and it's enabled and made possible by the fact that we built out that I are upfront to abstract away the java AST so that's all I have for you guys today and if you're interested to learn more check out this github repo and you know let us know what you think alright thank you you
Info
Channel: droidcon SF
Views: 934
Rating: undefined out of 5
Keywords:
Id: Y45MqYNjts0
Channel Id: undefined
Length: 28min 22sec (1702 seconds)
Published: Sat Dec 01 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.