Dependency Injection Simplified

Balraj Singh - Feb 9 '23 - - Dev Community

In our last session, we have talked about Functional ways to do error handling and looked at how this is a better way to handle errors. To achieve this we introduced some new Data Types like Optional, Try, Either. These are Functional Data Types and they helped to solve Exception/ Error handling issues. Today we will talk about simplifying DI in our app.

What is Dependency Injection?

DI is a concept which makes a class independent of its dependency management. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion and single responsibility principles.
To achieve this in Swift one of the most appreciated library is Swinject. This library not only provides DI but also have many other features like:-

  • Constructor/ Property/ Method Injection

  • Object Scopes as None (Transient), Graph, Container (Singleton) and Hierarchy

  • Container Hierarchy

  • Modular Components

  • Thread Safety (but the container is not thread-safe)

So we can see that this framework like many other frameworks is fully loaded. Below is a sample on how to use this library:-

// 1. Creating a IOC container & Object registeration
// This container is responsible to hold the logic to
// create an object and maintain its lifecycle

let container = Container()
container.register(Animal.self) { _ in Cat(name: “Mimi”) }
container.register(Person.self) { r in
 PetOwner(pet: r.resolve(Animal.self)!)
}

//2. To use the registered object we can resolve the instances
// using the same instance of container

let person = container.resolve(Person.self)!
person.play() // prints “I’m playing with Mimi.”
Enter fullscreen mode Exit fullscreen mode

With all this boiler-plates it makes sense in a complex project where one finds the usability of all or most of the features. But for starter or simpler project we can look into some other approaches too. Let’s see what Functional concepts has to offer to solve the above problem of DI.

Reader Monads

A Reader, sometimes called the environment monad, treats functions as values in a context. Loosely speaking, it allows you to build a computation that is a function of some context (configuration, session, database connection, etc.), rather than passing the context as an argument to the function.
Reader Monad differs the execution of a function to a moment when we can provide a proper execution context for it.
A simpler implementation of Reader Monad can be like this:-

struct Reader<E, A> {

 let f: (E) -> A

static func unit<E, A>(_ a: A) -> Reader<E, A> {
 return Reader<E, A>{_ in a}
 }

func run(_ e: E) -> A {
 return f(e)
 }

func map<B>(_ g: [@escaping](http://twitter.com/escaping) (A) -> B) -> Reader<E, B> {
 return Reader<E, B> { e in g(self.run(e)) }
 } 

 func flatMap<B>(_ g: [@escaping](http://twitter.com/escaping) (A) -> Reader<E, B>) -> Reader<E, B {
 return Reader<E, B> { e in g(self.run(e)).run(e) } 
 }
}
Enter fullscreen mode Exit fullscreen mode

With this implementation we can clearly see that Reader is nothing but a wrapper for a function f: (E) -> A where E is the input environment and A is the output expected. Let’s take a practical example to see how Reader Monad can simplify the Dependency Injection.

Understanding By Example

Let’s look at examples to solve the dependency problem. This example will be injecting a dependency on getting User objects from a repository:

Let’s first define a User Repository which will help to get and find a user:-

struct UserRepository {
 func get(id: Int) -> User { // get user based on ID }
 func find(username: String) -> User { //Find User based on name }
}
Enter fullscreen mode Exit fullscreen mode

Now there is a UserInfo class which uses the UserRepository to fetch specific information related to a user.
Let’s see how to implement and fulfil the dependency first using our classic way of DI with DI framework like Swinject and later compare it with Reader monad implementation.

User Info using DI Framework like Swinject

// Defining UserInfo
struct UserInfo {
 let userRepo: UserRepository

init(userRepo: UserRepository) {
 self.userRepo = userRepo
 }

func getEmailID(withUserId userId: String) -> String {
 return self.userRepo.get(id: userId).email
 }

func getBossEmailId(forUserName userName: String) -> String {
 // get the currect user
 let user = self.userRepo.find(userName: userName)
 let boss = self.userRepo.get(id: user.supervisorId)

// return the result
 boss.email
 }
}

// Now we need to register User Info before using it
let container = Container()
container.register(UserRepository.self) { _ in UserRepository() }
container.register(UserInfo.self) { r in
 UserInfo(userRepo: r.resolve(UserRepository.self)!)
}

// Now using UserInfo
struct Application {
 let userInfo: UserInfo = container.resolve(UserInfo.self)!

func sendEmail(forUserId id: String, userName name: String){
 // get user emailID
 let userEmail = userInfo.getEmailID(withUserId: id)
 let bossEmail = userInfo.getBossEmailId(forUserName: name)
 self.sendEmail(to: [userEmail, bossEmail])
}
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we can clearly note a few points:-

  • There is a lot of boiler-plate to register and resolve dependency

  • Application class has no clue what dependency UserInfo class is using

  • DI is hidden in this way

  • Application struct cannot change the source of getting User data as that is not under its control anymore

Now let’s see how the same can be achieved using Reader Monad

// Defining UserInfo
struct UserInfo {

func getEmailID(withUserId userId: String) -> Reader<UserRepository, String> {
 return Reader<UserRepository, String>.unit(self.userRepo.get(id: userId).email)
 }

func getBossEmailId(forUserName userName: String) -> Reader<UserRepository, String> {
 // get the currect user
 let user = self.userRepo.find(userName: userName)
 let boss = self.userRepo.get(id: user.supervisorId)

// return the result
 Reader<UserRepository, String>.unit(boss.email)
 }
}

// Now using UserInfo
struct Application {
 func sendEmail(forUserId id: String, userName name: String){
 let userRepo = UserRepository()
 // get user emailID
 let userEmail = userInfo.getEmailID(withUserId: id).run(userRepo)
 let bossEmail = userInfo.getBossEmailId(forUserName: name).run(userRepo)
 self.sendEmail(to: [userEmail, bossEmail])
}
}
Enter fullscreen mode Exit fullscreen mode

So let’s compare the above implementation of Reader Monad:-

  • There is less boiler-plate now

  • Since Reader is a Monad it gets composition for free

  • Effects can be tracked as the dependency is now no more hidden and it’s explicit

  • Application struct can now inject Mock implementation also and this can ease-out the unit testing

But I can’t choose!

What to choose is a Big question. There is no rule or guidelines which states what to choose Reader Monad or IOC Containers like Swinject for Dependency Injection. But seeing the strength and weakness of both the approaches we can use the Reader Monad throughout our application’s core and IOC Containers at the outer edges.
This way we get the benefit of the IOC Containers but we only have to apply it to the boundaries. The Reader Monad lets us push the injection out to the edges of our application where it belongs.

In our next session, we will see more such scenarios to simplify our applications. Stay tuned till then!!!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .