I have also been thinking about the relevance of Dependency Injection in Scala and taking cue from the ideas discussed in the Scala community, tried out some techniques in a Scala application.
One of the features that a DI framework offers is the ability to decouple explicit concrete dependencies from the abstraction of the component. This is what Martin Odersky calls the service oriented software component model, where the actual component uses the services of other cooperating components without being statically dependent on their implementations. This composition of services is typically done using a DSL like module that sets up the wiring between components by injecting all relevant dependencies into the component graph.
Do I need a separate DI framework in Scala ?
With powerful mechanisms of object construction (and built-in factory method
apply()
with companion objects), abstraction over types and values and flexible composition using traits, Scala offers more power than Java in decoupling of concrete implementations from the abstract services. Every component can have separate configuration units that will inject the concrete type and data members that wire up non-intrusively to deliver the runtime machinery for the particular service.Here is an example Scala class for computing the salary sheet of employees ..
abstract class SalaryCalculationEngine {
trait Context {
val calculator: Calculator;
val calendar: Calendar;
}
protected val ctx: Context
type E <: Employee
def calculate(dailyRate: Double): Double = {
ctx.calculator.calculate(dailyRate, ctx.calendar.noOfDays)
}
def payroll(employees: List[E]) = {
employees.map(_.getDailyRate).foreach(s => println(calculate(s)))
}
}
where
Calculator
is defined as a trait that mixes in ..trait Calculator {
def calculate(basic: Double): Double
}
Notice the use of the bounded abstract type and the abstract data members as configurable points of the abstraction. A concrete implementation of the above class will inject these abstract members to complete the runtime machinery. The configurable abstract data members form the context of the abstraction and has been encapsulated into another trait. This will be mixed in with a concrete implementation as part of the configuration module. Before going into the usage of the abstraction
SalaryCalculationEngine
, we need to configure the abstract types and data members. Let us define a module for Class1 Employees that will supply the exact concrete configuration parameters ..trait Class1SalaryConfig {
val calculator = new DefaultCalculator
val calendar = new DefaultCalendar
class DefaultCalculator extends Calculator {
def calculate(basic: Double, noOfDays: Int): Double = basic * noOfDays
}
class DefaultCalendar extends Calendar {
def noOfDays: Int = 30
}
}
and use this configuration to instantiate the abstraction for generating salary sheet ..
val emps = List[Employee](..
val sal = new SalaryCalculationEngine {
type E = Employee
protected val ctx = new Class1SalaryConfig with Context
}
sal.payroll(emps)
Note how the concrete configuration (
Class1SalaryConfig
) mixes-in with the Context
defined in the abstraction to inject the dependencies.We can easily swap out the current implementation by mixing in with another configuration - the MockConfiguration ..
trait MockSalaryConfig {
type T = MockCalendar
val calculator = new MockCalculator
val calendar = new MockCalendar
class MockCalculator extends Calculator {
def calculate(basic: Double, noOfDays: Int): Double = 0
}
class MockCalendar extends Calendar {
def noOfDays: Int = 10
}
}
and the application ..
val mock = new SalaryCalculationEngine {
type E = Employee
protected val ctx = new MockSalaryConfig with Context
}
mock.payroll(emps)
Extensibility ..
Traits make the above scheme very extensible. If we add another dependency in the Context, then we just need to provide a configuration for it in the implementation of the config trait. All sites of usage do not need to change since the Context mixes in dynamically with the configuration.
Powerful abstraction techniques of the Scala language help us achieve easy composability of services enable swapping in and out of alternative implementations in a fairly non-intrusive manner - one of the main features thet DI frameworks offer. The configurations can be easily reused at a much more coarse level of granularity and can be kept decoupled from the main flow of the application. From this point of view, these can act similar to the Modules of Guice, as they serve as the repository for binding information.
However, standard dependency injection frameworks shine in a couple of other ways which the above abstraction techniques fail to achieve :
- DI frameworks offer a container of their own that abstracts the object creation and injection services in a manner completely decoupled from the main business logic of the application. To the application code, the developer gets a bunch of unit testable classes with all dependencies injected transparently through declarative configuration based on DSLs. Language based techniques are not that non-intrusive and often are found to tangle with the main logic of the application.
- DI frameworks like Guice and Spring have a separate lifecycle of their own and perform lots of work upfront during application initialization. This is called the bootstrapping process, when all published dependencies are analysed and recursively injected starting from the root class. Hence runtime performance is greatly enhanced, since we have the injector as well as all bindings completely set up. In the case with Scala, every class asks for the configurations - hence it is more like a Service Locator pattern.
- Modules in Guice provide a nice way to decouple compile time independent components and act as the central repository for all published dependencies that need to be injected. The Spring application context does the same thing during startup. You create a dependency one time and use it in many places - hence development scalability improves. The configuration specification in Scala is at a finer level of granularity and is strictly not centrally managed. I would love to have a DSL that allows me to manage all configurations declaratively in a centralized repository.
- Another great feature that DI frameworks provide is integration with Web components and frameworks that allow objects to be created with specialized scopes, e.g. an object per http request, or an object per user session. These facilities come out of the box and provide great productivity boost to the developers.
- Interceptors and AOP are another area where the DI frameworks shine. The following snippet from Guice manual applies a transcation interceptor to all methods annotated with @Transaction ..
binder.bindInterceptor(
any(), // Match classes.
annotatedWith(Transactional.class), // Match methods.
new TransactionInterceptor() // The interceptor.
);
Despite all powerful abstraction techniques present in the Scala language, I think a separate dependency injection framework has a lot to offer. Most importantly it addresses the object construction, injection and interception of lifecycle services as a completely separate concern from the application logic, leading to more modular software construction.
5 comments:
One can argue that most of what Java DI frameworks provide today can be achieved with the abstraction techniques you mentioned in conjunction with implicit parameters. A Scala-based DI framework would have a difficult time providing a serious improvement over the language built-in capabilities.
Your articles are very hard to print. Especially with firefox.
Thank you for sharing this. I'm mulling over DI techniques in pure scala language constructs, and so this and jboner's post (http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di.html), as well as ch 27 in Programming in Scala are essential reads.
There is much I'm curious about, but I want to ask first what one gains by declaring the Context trait as a nested member? Why not use the (admittedly java standard) method of a common package instead?
Chris -
I just wanted to keep the Context in the narrowest possible namespace, just to indicate that it's a context of the SalaryCalculationEngine. You can keep it in a package scope as in Java. However, in a production Scala application, I would possibly make a separate Scala module for SalaryCalculationEngine and make Context a part of it. Scala modules are very underrated, but damn powerful. They can be composed recursively as well .. but that needs to be a fodder for a separate post :)
Thanks.
The example code is incomplete as given; I've posted an inferred version here: http://bit.ly/discala
As an exercise, I transposed it to java: http://bit.ly/dijava
Oddly enough, it clarified some of the Scala abstractions as well as the technique.
Thanks again
Post a Comment