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.”
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) }
}
}
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 }
}
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])
}
}
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])
}
}
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!!!
Top comments (0)