The problem with DI rameworks:
When creating components, one way or another, we must assure that other components we depend on are in fact constructed, properly initialized, and ready to use. In object oriented programming (OOP), this task typically falls on the constructor of our component. Hard-coding the construction of our dependencies in TransactionProcessor’s constructor comes with a number of pitfalls. Let’s consider the following example of a transaction processor that depends on an accounts repository, a user repository, and an audit log which logs all transactions. What’s wrong with the following code?
class AccountRepository{..}
class AuditLog{..}
class UserRepository{..}
class TransactionProcessor(){
val accounts = new AccountRepository()
val users = new UserRepository()
val audit = new AuditLog()
[..other stuff like business logic..]
def transfer(userId: String, fromAccount: String, toAccount: String) = ???
}
Here the dependencies are set up within the constructor, which means they are hard to change. For instance, what if we wanted to replace the audit log with another audit log that logs off-site? Or what if we wanted to replace our old user repository with a new LDAP based one for a while to see how it works. What if we wanted to pass in fake implementations because we are writing integration tests and we want to use test versions of these components. We would have to modify the constructor code by hand, which implies finding dependency initialization code (as opposed to other code in the class) and changing it. Now that we touched the class we would have to make sure it still works. Since we opened it up, it’s possible we introduced errors. This is a pretty bad way to go about writing this class. One step in the right direction would be to make our components depend on traits and inject the desired implementations of those traits via the constructor, as shown in the following example:
trait AccountsRepository
class OldAccountRepository extends AccountsRepository
class LDAPAccountsRepository extends AccountsRepository
trait AuditLog
class OffsetAuditLog extends AuditLog
[..other implementations..]
class TransactionProcessor(accounts: AccountRepository, users: UserRepository, audit: AuditLog){
[..business logic..]
}
Now we can pass in the implementation we see fit. The problem is that the responsibility of creating these dependencies by hand still has not disappeared. We just moved the messy code up one level, where TransactionProcessor is created. That class must create all the dependencies by hand, which is still not ideal. This is the problem that dependency injection frameworks aim to fix. We annotate the constructor with @Inject and create somewhere else a module definition class that maps the interfaces to the implementations, see bind method below.
@Inject
class TransactionProcessor(accounts: AccountRepository, users: UserRepository, audit: AuditLog){
[..business logic..]
}
class AppModule extends AbstractModule{
override def configure() : Unit = {
bind(classOf[AccountsRepository], classOf[LDAPAccountRepository])
bind(classOf[AuditLog], classOf[OffSiteAuditLog])
}
}
Type based dependency injection with Spring and Guice improves code code quality compared to doing dependency initialization by hand, because now all the mappings are in one place, there is less boilerplate, and we can easily swap out implementation code with new versions, test versions, experimental version, or what have you, simply by changing the mapping. Because the changes can be done so cleanly, injected in from the outside, we don’t have to worry that we accidentally corrupted business logic inside TransactionProcessor.
There are some draw backs to this solution as well, though. AppModule, is a sort of global component mapping registry, which sets up the whole map. If we just wanted to test for instance two or three classes in an application of thousands of classes, we would always have to deal with a global initialization framework. When we use dependency injection frameworks, it is very hard to think locally. Also, the notion of global anything flies in the face of functional programming where the return value and possible side effects of a function should only depend on its arguments. Now we have this global gadget somewhere which does injection magic for us. As functional programmers, we don’t like this. Maybe it was good for Java and OOP, but not good enough for Scala and FP. Scala offers a much lighter weight construct for type based injection which allows us to only deal with the classes we are interested in, which makes it much easier to write integration tests. So let’s go back to our original example and see what a functional approach would look like. Constructor injection fits the functional paradigm, we can simply create all the components in App and pass them down as constructor parameters. But let’s do this in a way that won’t recreate the boilerplate we have seen before with hand-coded constructor injection. Let’s make the constructor parameters implicit:
class LDAPAccountsRepository(implicit audit: AuditLog) extends AccountsRepository
class OffsiteAuditAuditLog extends AuditLog
class LDAPUserRepository(implicit audit: AuditLog) extends UserRepository
class MyTransactionProcessor(implicit audit, accounts, users)
class App{
implicit val audit = new OffSiteAuditLog
implicit val users = new UserRepository
implicit val accounts = new LDAPAccountsRepository
implicit val processor = new TransactionProcessor
//..and so on..
}
We don’t need to pass the arguments and the code is as clean or cleaner then the dependency injection frameworks
Testing becomes much simpler than with dependency injection frameworks, because it’s no longer necessary to mock up everything. We can simply mock up the components directly connected to the one being tested. For example let’s assume we are creating a test just for the user repository. We only need to set up the immediately dependent component, the audit log. This is much simpler than the DI module definition files of Spring and Guice, because it won’t require you to deal with all the classes of the entire app. All you need to understand is the set of classes you are actually trying to test:
class TestAuditAuditLog extends AuditLog
class FlatSpec{
implicit val testAudit = new TestAuditLog
val users = new UserRepository
}
Conclusion
Dependency management is not optional. We have to deal with setting up the components we depend on one way or another. The question is which of these approaches is the cleanest and the easiest to manage. Which approach will make it easier to make changes. Which method will accommodate test driven development the most. Dependency injection frameworks were an improvement over hand coding these component wirings. But they also created very heavy, centralized, global constructs, which make it necessary to consider the entire application when making changes or writing tests. The notion of global anything works against the functional programming paradigm which demands that the value of a pure function only depend on its arguments. Scala offers its own solution for type based dependency injection which fits well with the functional paradigm, is simple and light weight, and is supported directly by the language: implicits. This makes dependency injection frameworks unnecessary when using Scala, because we have better tools that do the job.