Grafter — a take on yet another DI library

Michał Matłoka
SoftwareMill Tech Blog
5 min readDec 12, 2018

--

Dependency Injection (DI) is a very popular pattern originating in the Java world, at the same time often pursued in other languages. Many people coming to Scala, wonder what to use for DI? This topic was already covered several times by Adam Warski in his articles. They point out that, that there’s actually more than one type of DI. Adam has compared the Reader and Constructor based Dependency Injections in his other text.

On a daily basis in SoftwareMill we work with the latter one (constructor based DI), leveraging MacWire or just pure constructors using old, goodnew keyword. Lately we got an opportunity to try something different — the Zalando Grafter library.

Did you know that the main idea related to the DI concept appeared already around 1994?

Grafter basics

First, let’s have a look at a basic Grafter-based application:

object Main extends App {
val config: ApplicationConfig =
ApplicationConfig(HttpConfig("localhost", 8080), DatabaseConfig("jdbc://postgres"))

val application: Application =
Application.reader[ApplicationConfig].apply(config).singletons


val started = application.startAll.value

if (started.forall(_.success))
println("All modules started")
else
println(started.mkString(System.lineSeparator()))
}
@reader
case class Application(httpServer: HttpServer, database: Database)
...// based on https://opensource.zalando.com/grafter/org.zalando.grafter.QuickStart.html

In order to work with Grafter, first you have to define an application config and the top level application (main component). Then Grafter will build the component tree from that, covering all of the dependencies. By enabling singletons feature on configured Application, you can choose to deduplicate instances of the same type used in different places of your application.

How do you define modules of your application? Quite simply:

@reader
case class Application(httpServer: HttpServer, database: Database)
@reader
case class Database(config: DatabaseConfig) {
// some technical/business logic
}
@reader
case class HttpServer(config: HttpConfig) extends Start {
// some technical/business logic
}

As you can see, every single component is annotated with the @reader annotation, and what is more, it is a case class. Wait, what? Yes, a case class. Usually, we associate case classes with a different role — for storing data, modeling the domain objects. But here, due to technicalities, they may be used as components defining the application structure or providing a technical or business logic. All component dependencies are visible just as case class parameters.

Grafter out of the box includes the application lifecycle support.

@reader
case class Database(config: DatabaseConfig) extends Start {
def start: Eval[StartResult] =
StartResult.eval("Starting the database")(someUltraComplicatedInitializationLogic)
}

Component just needs to extend Start or Stop trait and declare what needs to be executed at the proper time. It is important to remember, that you should not run any side-effects producing code during the instantiation outside of the proper start and stop methods.

Ok, but how is it all related to the Reader or Constructor based DI? Grafter uses macros to auto-generate the reader methods with cats.data.Reader return type. In practice they’re typical Reader Monads. Such Monads contain functions, describing how to obtain specific variable based on another one. Let’s take a look at the example. The previously presented code would basically produce multiple generated methods, put in appropriate companion objects. For the Application class, this would be:

import cats.data.Readerobject Application {    implicit def reader[A](implicit r1: Reader[A, HttpServer], r2:     Reader[A, Database]): Reader[A, Application] =
Reader(a => Application(r1(a), r2(a)))
}// source https://opensource.zalando.com/grafter/org.zalando.grafter.Concepts.html

This means that if there is a reader for every Application dependency ( HttpServer and Database) then it can construct the Application.

Even though you don’t see that explicitly (you won’t see the generated code), you are effectively using Reader Monads. From user (application developer) perspective, it’s like using Spring or similar framework. Just annotate everything and the magic happens automatically. The added value is for sure the finally tagless style support, achieved by additionally annotating the parent trait with @defaultReader annotation.

import org.zalando.grafter.macros.{defaultReader, reader}
import cats.Monad
@defaultReader[SpecificRepository]
trait Repository[F[_]] {
def getAll(): F[Everything]
}

@reader
case class SpecificRepository[F[_]]()(implicit val m: Monad[F]) extends Repository[F] {

def getAll(): F[Everything] = ???
}

@defaultReader will also work if you would like to use just trait + implementation without the generic parameters.

Grafter has also some other nice features, like e.g.: support for testing — you can just execute a replace method on the configured application, and provide different instance or implementation for given object. It will replace all the occurrences of a given component.

Application.reader.apply(testConfiguration).
singletons.
replace[Database](mockDatabase)

You may put here different case class instance (e.g. with other parameters), or another implementation of a trait, if it was used. This approach focuses mainly on testing whole application, not on isolated branch of the application.

Disadvantages

Unfortunately, there are also some drawbacks. If your application leverages architecture using a lot of implicits, then using Grafter will be troublesome. You can’t directly “inject” an implicit to a Grafter component. It is needed to wrap such implicit in other case class, and “inject” this wrapper to the component. What if you wish to use other implicit for production and other for testing? Then your wrapper needs to get a proper one during its instantiation.

Additionally you may encounter issues with traits, with generic parameters, since there are issues with finding proper matching implementation if you wish to bind the generic to a specific type in it.

IDEs won’t give you any Grafter dedicated support. In order to find where given component is used, you just need to look for the given class usages.

If you’d like to learn more and study a full Grafter based project, then take a look at the sample in Grafter repository.

Summing up

From time to time it’s good to have an opportunity to try different things. Other approaches might let you notice drawbacks of things you are used to. Grafter case for sure reminded me the “old java times”, at the same time causing some pain due to, theimplicits handling problems. It leverages Reader Monads, but during standard work with the library, you’re never going to see them. Probably I won’t become the Grafter fan, but maybe I am just too used to the Constructor-based injections. For more details related to Grafter, its internals and features, take a look at the project documentation.

--

--

IT Dev (Scala), Consultant, Speaker & Trainer | #ApacheKafka #KafkaStreams #ApacheCassandra #Lightbend Certified | Open Source Contributor | @mmatloka