We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through Functional Works average a 15% increase in salary 🚀

Blog hero image

Demystifying Dependency Injection with Airframe

Taro L. Saito 10 August, 2018 (8 min read)

If I mention the word Dependency Injection (DI) in Scala, I might receive a lot of negative responses. This is largely because:

For a while, let’s forget about dependency injection. It just addresses some of the common coding patterns that we need to solve in daily application development. For example, we need an efficient way to:

  • Build service objects by passing required objects
    • Minimize application code by removing unnecessary objects
    • Manage service lifecycles, such as start-up and shutdown processes

If we can solve these issues conveniently, the choice of a DI framework (or not using any DI framework at all) doesn’t matter.

Airframe is a library of lightweight building blocks for Scala, which simplifies these programming concerns, and helps us to reduce the number of things to consider when building applications. More importantly while using Airframe we don’t need to use the word dependency injection at all.

Airframe Github repo Source

My thought process is now more about how to build services by combining other service objects, rather than how to use DI framework itself. You can find more detailed examples in the documentation of Airframe, so in this article, I will focus on how Airframe can simplify your daily application development.

Airframe Github repo2 Source

#Building Service Objects#
Let’s consider building a service object (Service) which takes a configuration object (ServiceConfig):

case class ServiceConfig(host:String, port:Int)
class Service(config:ServiceConfig) {
  // ...
}
// Building a service instance
val config = new ServiceConfig("localhost", "8080")
val service = new Service(config)

This code looks good at first glance because it is just regular Scala code, but if you need to add more service objects, you will notice that the actual application configuration and service instantiation will happen separately. The code below shows an example of reading configurations from a YAML file, packing configurations for multiple services into ConfigSet, and then instantiating service objects:

// New Services
class WebApp(webAppConfig:WebAppConfig, component:WebComponent)
class WebComponent(service:Service)

// Pack configuration set in a class
case class ConfigSet(serviceConfig:ServiceConfig, webAppConfig:WebAppConfig)

// Read configurations from a YAML file
def readConfig: ConfigSet = {
  val yaml = readConfig("config.yml") 
  val serviceConfig = new ServiceConfig(yaml.get(...), yaml.get(..))
  val webAppConfig = new WebAppConfig(...)
  ConfigSet(serviceConfig, webAppConfig)
}

// Initialize services
val configSet = readConfig
val service = new Service(configSet.serviceConfig)
val webComponent = new WebComponent(service)
val webApp = new WebApp(configSet.webAppConfig, webComponent)

In the above example, you basically need to think the following things:

  • How to pass configuration objects to services (e.g., by packing service configurations into a single object like ConfigSet, or by using local variable references).
    • How to instantiate services by composing required objects.
    • Additionally you may need to think about the timing to instantiate these service objects. The above code might not be appropriate if you need to start other service objects before starting WebApp. In this case you also need to care about the initialization order.

With Airframe, you can forget about these details and can start writing the service initialization logic in a more straightforward manner:

import wvlet.airframe._

// Read configurations from a YAML file
val yaml = readConfig("config.yml") 
// Configure Application
val d = newDesign
  .bind[ServiceConfig].toInstance(new ServiceConfig(yaml.get(...), yaml.get(..))
  .bind[WebAppConfig].toInstance(new WebAppConfig(...))

// Initialize Services
d.build[WebApp]{ webApp =>
  // new WebApp(session.get[WebAppConfig], session.get[WebComponent]) will be called
  // new WebComponent(session.get[Service]) will be called
  // new Service(session.get[ServiceConfig]) will be called
}

In this example, you only need to think about:

  • What configuration objects will be necessary (bind[X])
    • When to build service objects (build[WebApp])

Then Airframe will take care of the rest. This is possible because:

  • Service object constructors already define their required objects as constructor arguments. So we can automatically instantiate objects if we can find (or build) required constructor argument values.
    • Airframe can pass objects through a Session instance, which holds references to instantiation rules (e.g., constructor argument types) and pre-defined object instances.

Airframe takes advantage of these facts and generates code using Scala macros to build service objects on your behalf. If you need more flexibility, Airframe also supports building instances from Scala traits and provider functions. This type of automated instance construction is called auto-wiring, while manually passing objects to constructors is called hand-wiring. In Airframe you can use hand-wiring as well if you need to instantiate objects explicitly.

Minimizing Code

One of the advantages of using Airframe is you can separate the concern of how to build service objects from writing core application code. This has been improving my productivity a lot because I always can start from writing minimum code.

For example, when I need to write an application which requires some thread pool, database access, etc., I just start writing a code like this:

import wvlet.airframe._

trait MyApp {
  private val threadPool = bind[ThreadPool]
  private val dbService = bind[DbService]
  ...
  threadPool.submit(dbService.query("select ..."))
}

By using bind[X] syntax of Airframe, I can write the first draft of the code even if there is no ThreadPool nor DbService implementation yet. I know how I want to use services like ThreadPool and DbServices to make an SQL query. I don’t need to care about how to build ThreadPool and DbService at this phase. I just know I need these services in this application, and directly expressed this idea into the code.

Airframe helps writing service logic by using only necessary objects. A general practice of writing test code for complex services is using mocks (by using Mockito, etc.). One of the reasons why you need mocks is that it is usually difficult to implement all interface methods for testing, and providing null implementation via mocking can reduce such burdens. But if you already have minimized the code, you don’t need to create mocks at all. You can just replace some of required objects into convenient ones for testing (e.g., using some in-memory DBMS, instead of launching an actual DBMS server, etc.)

Managing Resources

When implementing the above DbService, I would notice that I need some connection pool and database configurations (e.g., database types, host, port, etc.). So I can put its concrete implementation inside DbService trait. ConnectionPool usually requires startup and shutdown processes. In order to ensure having these resource management steps, I will add onStart/onShutdown hooks, which will be called at session start/terminate phases.

trait DbService {
  private val connectionPool = bind[ConnectionPool]
    .onStart { dbConfig: DbConfig => startConnectionPool(dbConfig) }
    .onShutdown { _.close }

  def query(sql:String) = { connectionPool.execute(sql) ... }
}

RIIA (Resource Instantiation Is Resource Allocation) is a common practice in C++ to ensure writing resource initialization/deallocation code inside constructor/deconstructor. Even though Java and Scala have no deconstructor, Airframe’s lifecycle management hooks allow writing such resource management code in a proper location.

Airframe uses FILO (First-In Last-Out) order by default for resource start-up and shutdown processes. This means resources allocated first will be released last. This ordering is appropriate for the most of the cases. Extensions of Guice, such as airlift-bootstrap (used in Presto, a distributed query engine by Facebook) and guice-bootstrap (used in Embulk, a bulk data input/output connector) also follow FILO ordering.

Choosing The Right Style for You

Actually it depends on your team’s expertise:

  • If your team is already familiar with a purely-functional programming (FP) style using tagless-final and cats-effect in Scala, a purely-functional approach would work. You can still combine this FP approach with Airframe later.
    • If your team members are not familiar with FP style programming, or have used Google Guice in Java, Airframe is a good choice in Scala since it basically follow the same syntax with Guice, while making it more suitable for Scala.
    • If you prefer compile-time check, consider using MacWire. In Java, there is Dagger2, which is popular for developing Android applications, but its annotation processor based implementation doesn’t work for Scala.

For more detailed comparisons of various approaches, refer the following article, Airframe: DI Framework Comparison:

Airframe comparison Source

If you prefer a pure-Scala approach, I won’t stop you. It’s actually worth trying. But remember, the problem I have talked here is how to reduce what you need to think for getting the practical benefits in your programming task.

Actually using purely functional style in Scala will be a long journey. If you look at the history of FP in Scala, which became popular after the book Functional Programming in Scala was published in 2014, you will notice that there have been many attempts to implement DI-like concepts in Scala: Cake Pattern, Reader Monad, Tagless Final (used in scalaz and cats), etc.

The tagless final approach looks promising considering the recent popularity of cats, but its learning cost until finding the best practice would be still high. Eugene Yokota’s blogs (herding cats and learning scalaz) will be good learning materials, but simply choosing which library to use (cats or scalaz?) is also a big decision, although these two libraries are addressing almost the same problem. In addition, to use FP approaches, you need to convince your team members to follow some specific FP style, because understanding the rationale on using abstractions like F[_] requires some basic experiences and background of FP. And also you need to find a way to practically teach this FP style to new engineers.

I can say pursuing FP approach in Scala and getting programming supports from a framework like Airframe are totally different problems. So I think a response like, “(If you use FP) You don’t need DI in Scala” is not properly answering the question on how to reduce the efforts for building and managing service objects. In fact you need to reduce both of the learning cost and the actual coding cost to practically save your time. If you are thinking about the idiomatic FP style for Scala while developing applications, you are already distracted.

Conclusions

I have explained three key points that need to be addressed in daily programming:

  • How to build service objects
    • How to minimize service code
    • And how to manage resources

Airframe is useful to reduce these concerns that occur frequently in application development. Especially this one-page quick start guide is all you need to know to start using Airframe, so you don’t need to learn what is IoC, or what is dependency injection to enjoy the benefits of Airframe.

Takeaway message: Let’s forget about the dependency injection. This might be used somewhere, but this is not what you need to care about.

Originally published on medium.com