I just started learning Scala. For those who still do not know what kind of language, a short excerpt
from the official site :
Scala is a concise, elegant and statically typed programming language that combines the capabilities of an object-oriented and functional language. Scala is fully compatible with Java.Today I would like to show you how, using the rich expressive abilities of this language, to solve a problem that is relevant for any more or less large project, namely work with component dependencies or dependency injection. The last few years I have used spring ioc to solve this problem, however this framework has several flaws, the most obvious of which is building an application from components at runtime and having xml descriptors (yes, of course, you can use autowiring and annotations, but also these opportunities have their own serious problems).
Maybe I, of course, suffer from a lack of attention, but a typical problem for me is that when I write a component, I often forget to write it in the xml-descriptor, or I forget to write some of its dependencies. This problem can only be detected at runtime — by running either automated tests or the application itself. And in general, the presence of descriptors in the xml application has caused me some irritation since the days of working with JSF. The fewer the descriptors, the better.
')
To my delight in Scala there is a rather laconic way of organizing DI free from such flaws. The Cake pattern itself was described in the article
“Scalable Components Abstraction,” written by Martin Odersky and Matthias Zenger, which I highly recommend reading. The name of the template was suggested by Jon Pretty in the
discussion on Nabble : "The rationale is, besides my love for cakes, the fact that the cake is made of several layers (separated by jam) and can be cut into slices." The name stuck.
Consider a simple example of the implementation of this template. Let's start with the announcement of two components of the application:
trait NameProviderComponent {
val nameProvider:NameProvider
trait NameProvider {
def getName:String
}
}
trait SayHelloComponent {
val sayHelloService:SayHelloService
trait SayHelloService {
def sayHello:Unit
}
}
What is required to package our component interfaces into NameProviderComponent and SayHelloComponent containers, and why nameProvider constants were required: NameProvider and sayHelloService: SayHelloService will become clear a little later, in the meantime, we will write the implementation of these components. Let's start with the name provider:
trait NameProviderComponentImpl extends NameProviderComponent {
class NameProviderImpl extends NameProvider {
def getName:String = "World"
}
}
Everything is quite simple, however, until it clarifies the process of inserting dependencies. Consider the only module of our application that will have dependencies:
trait SayHelloComponentImpl extends SayHelloComponent {
this: SayHelloComponentImpl with NameProviderComponent =>
class SayHelloServiceImpl extends SayHelloService {
def sayHello:Unit = println("Hello, "+nameProvider.getName+"!")
}
}
We will understand how this works. The first thing to notice is the construction: “this: SayHelloComponentImpl with NameProviderComponent =>”, which literally tells the compiler exactly which type should have an object that implements the given trait. After such a declaration, it becomes possible to use the indefinite nameProvider constant in the body of the sayHello function, since the trait NameProviderComponent declares such a constant. This is the same jam that connects the layers.
Let's turn to the cake as a whole:
object ComponentRegistry
extends SayHelloComponentImpl
with NameProviderComponentImpl {
val nameProvider = new NameProviderImpl
val sayHelloService = new SayHelloServiceImpl
}
; the ComponentRegistry object implements both the trait implementations of our modules and defines the last, so far undefined, class members — the constants nameProvider and sayHelloService. The sayHelloService will use this object to get the nameProvider NameProviderImpl. ComponentRegistry is the direct equivalent of SpringContext.
How to use it:
object MyApplication {
def main(args : Array[String]) : Unit = {
ComponentRegistry.sayHelloService.sayHello
}
}
the execution of this application gives the expected result: “Hello, World!”
Now a little about the advantages:
- The context / registry is “assembled” at the time of compilation. Forgotten dependencies will break the build. It is impossible to compile with dependencies of inappropriate types. For example:
object BrokenComponentRegistry
extends SayHelloComponentImpl {
val sayHelloService = new SayHelloServiceImpl
}
will break the compilation with a completely sane message “illegal inheritance; self-type BrokenComponentRegistry.type does not conform to SayHelloComponentImpl's selftype SayHelloComponentImpl with NameProviderComponent »
- Let it not be very critical, but the application start time is reduced, because it does not require parsing of xml descriptors and active work with the reflections API.
It is clear that dependency injection is just a small piece needed in a typical project. In addition, the AOP would not hurt at all, say, to ensure verification of rights to execute code or declarative transaction management, and many other things, but I will write about this some time next time.