Have you ever used Grafter library? SoftwareMill recently got the opportunity to switch it up and make use of this library. In this article Senior Software Engineer Michał Matłoka advises us how they used this library starting with the basics!
'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, good 'new' 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.Reader
object 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, the 'implicits' 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.'