Reply to comment
Scala dependency injection
Vložil/a petr, Ne, 15/03/2009 - 13:33Recently I've discovered Scala, and being a functional languages fan I enjoyed many great Scala's features. I decided to use it in our projects and one of the things I wanted to learn is how to do dependency injection in some elegant way.
After searching for a while I stumbled upon [url=http://jonasboner.com/]Jonas Bonér's[/url] great article [url=http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di.html]Real-World Scala: Dependency Injection (DI)[/url]. I learned a lot about Scala from the article, as well as about dependency injection. But I wasn't comfortable with one thing in his approach: The dependencies of a component are fixed in such a way that it's not possible to have two different components providing the same service, but each depending on different sub-components. (Or at least I wasn't able to figure it out.). For example, let's say that I have a service that returns a list of users and I want to implement it by two interchangeable components: one that reads users from a database and another that reads users from a file. So the former should depend on a database provider subcomponent, while the other one on a subcomponent providing access to a file-system. However, it's not very difficult to refine the idea a bit further to gain (IMHO) even more flexible architecture.
Let's define a few term we are going to use:
[list]
[*][b]Service[/b] - an interface (trait) that describes some sort of service, for example a method that returns a list of users. It can be completely arbitrary interface (trait), not necessarily one defined in our program.
[*][b]Component[/b] - a special trait that declares how a [i]service[/i] is made available to other parts of the program (i.e. to other components). Each component contains just an abstract value that defines under what name the [i]service[/i] will be made available.
[*][b]Provider[/b] - a special trait that extends a [i]component[/i] and gives a concrete implementation to [i]component[/i]'s value. This way a [i]provider[/i] wires an implementation of a service to declared in a component to some actual implementation.
[/list]
So components and providers are part of our architecture and we use them to bind the configuration together. On the other hand, services (and their implementations) can be completely arbitrary. For example, a service could be the [url=http://java.sun.com/javase/6/docs/api/javax/sql/DataSource.html]DataSource[/url] interface from javax.sql package that provides database connections.
We shall continue with the example of getting a list of users. Our program will use these three services:
[blockcode language=scala]
trait UserService {
def listAll(): Seq[String]
}
[/blockcode]
[blockcode language=scala]
trait FileService {
def readFile(fileName: String): String
}
[/blockcode]
[blockcode language=java5]
interface DataSource {
// ... defined in javax.sql
}
[/blockcode]
The [code language=java5]DataSource[/code] is an interfaces defined in Java API, while the other two traits are traits (interfaces) defined in our program.
Now let's define the corresponding components. The components are nothing more than traits that define abstract values for accessing the services:
[blockcode language=scala]
trait UserComponent {
val userService: UserService
}
[/blockcode]
[blockcode language=scala]
trait FileComponent {
val fileService: FileService
}
[/blockcode]
[blockcode language=scala]
trait DataSourceComponent {
val dataSourceService: javax.sql.DataSource
}
[/blockcode]
Now, we will define the providers. Each provider binds the value declared in its component trait to an actual implementation. First let's define one implementation for [code language=scala]FileComponent[/code] and one for [code language=scala]DataSourceComponent[/code]:
[blockcode language=scala]
trait FileProvider extends FileComponent {
override val fileService: FileService = new FileImpl
class FileImpl extends FileService {
def readFile(fileName: String): String = {
// read the file and return it's contents
...
}
}
}
[/blockcode]
[blockcode language=scala]
trait DataSourceProvider extends DataSourceComponent {
override val dataSourceService: javax.sql.DataSource = ... // get the DataSource somewhere, for example JNDI
}
[/blockcode]
and two different providers for [code language=scala]UserComponent[/code].
[blockcode language=scala]
/* Read a list of users from a file. */
trait UserProviderFile extends UserComponent {
// declare dependency on the FileComponent:
this: FileComponent =>
override val userService: UserService = new UserImpl
class UserImpl extends UserService {
def listAll(): Seq[String] = {
// use the 'injected' FileComponent to access read some file:
val fileContents: String = fileService.readFile("users.txt")
// parse the string and return the result
...
}
}
}
[/blockcode]
[blockcode language=scala]
/* Read a list of users from a database. */
trait UserProviderDataSource extends UserComponent {
// declare dependency on the DataSourceComponent:
this: DataSourceComponent =>
override val userService: UserService = new UserImpl
class UserImpl extends UserService {
def listAll(): Seq[String] = {
// use the 'injected' DataSourceComponent to access read some file:
val ds: javax.sql.DataSource = dataSourceService
// use the datasource to get some data and return the result
...
}
}
}
[/blockcode]
So we have two implementations of the UserService, each depending on different subcomponent.
Wiring them together cannot be easier. The providers are traits, so we just define a class or an object that extends all the providers we need in a particular configuration:
[blockcode language=scala]
object ConfigurationUsingFiles
extends UserProviderFile
with FileProvider
{ }
[/blockcode]
[blockcode language=scala]
object ConfigurationUsingDataSource
extends UserProviderDataSource
with DataSourceProvider
{ }
[/blockcode]
[h2]Tips & tricks[/h2]
[list]
[*]A component can declare more than one service.
[*]A provider can extend more than component.
[*]In many cases I found convenient do declare a service trait inside its component trait like this:
[blockcode language=scala]
trait UserComponent {
val userService: UserService
trait UserService {
def listAll(): Seq[String]
}
}
[/blockcode]
[*]If you add [code language=scala]@scala.reflect.BeanProperty[/code] annotation to the values in your providers like this:
[blockcode language=scala]
trait UserProviderDataSource extends UserComponent {
// declare dependency on the DataSourceComponent:
this: DataSourceComponent =>
@scala.reflect.BeanProperty
override val userService: UserService = new UserImpl
...
}
[/blockcode]
and define the configuration as a regular class:
[blockcode language=scala]
class ConfigurationUsingDataSource
extends UserProviderDataSource
with DataSourceProvider
{ }
[/blockcode]
you get a classical JavaBean and the values with configured services can be accessed as bean properties. (Note that this doesn't work with lazy values for some reason.)
[/list]
