What working with Dependency Injection and Service Locator leads to over the years

You can check the depth of understanding of a particular library by writing it yourself. Perhaps the newborn solution will be limited and devoid of any audience, but it will forever belong to its author and confirm a real understanding of how the technology works.

This happened to me too. I decided to write a full-fledged functionality for working with dependencies and, since I am an Android developer, adapt it to work in my familiar environment and for my same familiar tasks.

And I share my written creation with the naive thought that it will make someone better technically.

It is important to remember that this article is my work and the similarity with other technologies and approaches is intentional. And also the article is a summary of the essence of the library I wrote, the full code of which can be found on my GitHub.

Formulation of the problem

It all started with setting goals and an approximate path for code development. The path was built something like this:

  • storing dependencies in a container

  • ability to keep dependencies of the same type in a container

  • factories for creating dependencies

  • auto-generated dependencies

  • modules for more convenient filling of containers with dependencies. Android Extensions

  • dependencies between containers

  • collections in a container

  • library publication

Storing Dependencies in a Container

I initially had a clear understanding that, when talking about storing dependencies, we need to immediately consider working with the lifespan of the container and all its dependencies. For this reason, I immediately moved away from statics and created MutableMap for my dependencies in a regular class with the consideration that monitoring the life of a class instance will be easier than fiddling with statics.

interface DiContainer {
   val container: RootContainer
}

class DiContainerImpl : DiContainer {
   override val container: RootContainer = RootContainer()
}

class RootContainer {
   private val dependencies: MutableMap<Class<out Any>, Any> = mutableMapOf()
   ...
}

Above is the foundation of my approach, which allows you to both use a ready-made container implementation and redefine it in the future when RootContainer will begin to accept dependencies and settings from outside.

Filling this container with dependencies is very simple:

fun <T : Any> provide(clazz: Class<out T>, dependency: T) {
   dependencies[clazz] = dependency
}

Ability to keep dependencies of the same type in a container

To solve this problem, I chose the path of creating a separate collection to contain dependencies, where the key to obtaining them will be the annotation with which the corresponding dependency is associated.

class RootContainer {
   private val qualifiedDependencies: MutableMap<Class<out Annotation>, Any> = mutableMapOf()
   ...
}

This task required minor edits to the existing code: I needed to understand whether the class of the passed dependency contained the required annotation, and if so, put it in another collection.

fun <T : Any> provide(clazz: Class<out T>, dependency: T) {
   val qualifiedAnnotation = findQualifiedAnnotation(clazz)
   if (qualifiedAnnotation != null) {
       qualifiedDependencies[qualifiedAnnotation.annotationClass.java] = dependency
   } else {
       dependencies[clazz] = dependency
   }
}

Behind the method findQualifiedAnnotation hidden minor logic for searching for an annotation on a class. Otherwise, the task can be considered completed.

Factories for creating dependencies

Here I had to rack my brains because it was not clear where to store this dependency and whether it would be necessary to create a separate collection. I didn’t want to create a separate one, and the solution was very simple: put a lambda that creates an object in the existing collection, and use the key to specify the object created by the lambda.

data class DependencyFactory<T>(
   val factory: () -> T,
)

fun <T: Any> T.tryFactory(): T {
   return if (this is DependencyFactory<*>) {
       this.factory.invoke() as T
   } else {
       this
   }
}

Populating a container with a factory dependency looks like this:

inline fun <reified T : Any> factory(noinline factory: () -> T) {
   val dependencyFactory = DependencyFactory(factory)
   provide(T::class.java, dependencyFactory)
}

After that, when a dependency is requested, the tryFactory method is called on it, which will optionally call the lambda if the requested dependency turns out to be a factory dependency.

Auto-generated dependencies

Auto-generated dependency is a dependency that can be instantiated based on other dependencies, provided that they are already in the container. That is, if to create a class Z you need to pass it to the constructor A And Bwhich are already in the container, my system should be able to find these A And BI instantiate on their basis Z.

private fun <T> create(constructor: Constructor<T>): T {
   val parameters = constructor.parameters
   val parametersList = mutableListOf<Any>()
   parameters.forEach { parameter ->
       val qualifiedAnnotation = findQualifiedAnnotation(parameter)
       val value = getInternal(
           parameter.type,
           qualifiedAnnotation?.annotationClass?.java,
       )
       parametersList.add(value)
   }
   return constructor.newInstance(*parametersList.toTypedArray()) as T
}

The problem was solved very elegantly, in my opinion, requiring only to go through the parameters of the constructor and try to get the dependencies of the desired type from the container, and then simply pass all the collected objects to the method newInstance.

At the moment, my method of internally receiving data from the container began to look something like this:

private fun <T : Any> getInternal(
   clazz: Class<T>,
   qualifierClazz: Class<out Annotation>? = null,
): T {
   return getQualifiedDependency<T>(qualifierClazz)?.tryFactory()
       ?: (dependencies[clazz] as? T)?.tryFactory()
       ?: createDependency(clazz, qualifierClazz)
       ?: throw IllegalStateException("...")
}

Modules for more convenient filling of containers with dependencies. Android Extensions

I wanted to immediately solve the problem of changing the configuration, so I wrote a delegate to store an instance of my container inside ViewModel.

class DiContainerStoreViewModel(
   val container: RootContainer,
) : ViewModel()

Next I set up receiving DiContainerStoreViewModel via delegate:

fun ViewModelStoreOwner.retainContainer(
   modules: List<DiModule> = emptyList(),
   overrideViewModelStore: ViewModelStore? = null,
): Lazy<RootContainer>

DiModule is a data class that helps me save all calls to work with dependencies, so that I can later initiate them on a specific container instance.

fun module(module: RootContainer.() -> Unit): DiModule {
   return DiModule(module)
}

data class DiModule(
   val module: RootContainer.() -> Unit,
)

After this step, I could enjoy the ease of working with modules, which could be supplied in any quantity and the dependencies between them could be broken down in any order.

val sampleModule = module {
   provide(SomeDep("hello"))

   factory { FirstInteractorImpl() }
   factory { SecondInteractorImpl() }
}

Dependencies between containers

This task fit perfectly into the existing code and only required passing to the constructor RootContainer another container called within getInternal. Naturally, the last call is to first check for the presence of a dependency in the current container, and only then proceed to search in the dependent one.

Collections in a container

But this point turned out to be not only final, but also very non-trivial. Frankly, I thought about the solution for several days and here’s why – I wanted to work only with the collection Map and separate collection instances based on key and value types.

This means that if I put multiple elements in a collection Map<String, Int>and then I'll put it Map<Int, String>then my container should already hold two collections in the system, and when requesting a collection Map<String, Int>I don't want to receive elements from Map<Int, String>. And this was the whole difficulty, since in runtime it was necessary to pull out the types of generics of the collection Map was not possible.

As a result, I solved the problem by creating another collection, as well as an annotation, in the arguments of which I passed the necessary types.

private val dependencyMaps: MutableMap<DependencyMapIdentifier, MutableMap<Any, Any>> = mutableMapOf()

Data class for storing key and value types for the corresponding Map:

private data class DependencyMapIdentifier(
   val keyClass: Class<out Any>,
   val valueClass: Class<out Any>,
)

Filling the collection with data looks like this:

fun <K : Any, V : Any> intoMap(key: K, value: V) {
   val mapIdentifier = DependencyMapIdentifier(key.javaClass, value.javaClass)
   val existedMap = dependencyMaps[mapIdentifier]
   if (existedMap != null) {
       existedMap[key] = value
   } else {
       val newMap: MutableMap<Any, Any> = mutableMapOf(key to value)
       dependencyMaps[mapIdentifier] = newMap
   }
}

At the stage of filling the collection, a data class was created that stored the values ​​of the classes and used it as a key.

And to request a dependency, the following construction was required:

class AutoCreatedDependency @Inject constructor(
   @MapDependency(String::class, String::class) stringMap: Map<String, String>,
   @MapDependency(String::class, Boolean::class) booleanMap: Map<String, Boolean>,
)

There was a slight problem finding the collection because the primitive classes were transformed from wrapper classes such as java.lang.Boolean V booleanwhich required this method of obtaining from KClass wrapper class: mapDependencyAnnotation.keyClass.javaObjectType.

Library publishing

To publish, I used git commands:

git tag 1.0
git push --tags

After that, I opened a tab on GitHub Reseases -> Draft a new releaseselected the desired tag and clicked Publish release. Almost immediately my library was found in jitpackwhere, in addition to links and versions, you can find a line for badge within GitHub to insert into README.md and see in it always the current version of the published library.

Bottom line

I wrote my first full-fledged library and went through problems that were interesting to me, which gave me a deeper understanding of working with dependencies and also brought me pleasure. All library code can be found in GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *