Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jonas Bonér's dependency injection strategy seems limiting--but maybe I don't understand it

I've read this article several times:

http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di.html

I think that I get it. However there's something that I'm not quite getting.

Looking at his UserService example, I see that he's set up UserRepositoryComponent to encapsulate UserRepository. But what I don't understand is why UserRepositoryComponent plays two roles: it encapsulates UserRepository and also offers a reference to a UserRepository object.

I'm trying to imagine how I would use this pattern if I wanted to create a service that depends on two UserRepository instances. Maybe the job of the new service is to copy users from a "source" UserRepository to a "destination" UserRepository. So I'm imagining something like this:

trait CopyUserServiceComponent {
  val source: UserRepositoryComponent
  val destination: UserRepositoryComponent
  class CopyUserServiceComponent { 
    ... 
  }
}

But this is different from the original pattern. In this case I'm defining the dependencies in the component itself instead of inheriting them from some other component. But this seems to me to be the right way to do it: the components should declare their dependencies, not instances of their included services.

What am I missing here?

like image 284
Willis Blackburn Avatar asked Nov 02 '10 15:11

Willis Blackburn


2 Answers

In this case I'm defining the dependencies in the component itself instead of inheriting them from some other component.

The cake pattern doesn't use inheritance to declare dependencies. Did you see any "extend" in UserServiceComponent?

But this seems to me to be the right way to do it: the components should declare their dependencies, not instances of their included services.

But that's exactly what the cake pattern does: declare dependencies! Perhaps if the example contained def userRepositoryFactory = new UserRepository instead of val userRepository = new UserRepository, that would have been more clear?

So, let's go back to your example:

trait CopyUserServiceComponent {
  val source: UserRepositoryComponent
  val destination: UserRepositoryComponent
  class CopyUserServiceComponent { 
    ... 
  }
}

Let's see the things we can't do with that:

trait CopyUserServiceComponent {
  // The module will need to see my internals!
  private val source: UserRepositoryComponent
  private val destination: UserRepositoryComponent
  class CopyUserServiceComponent { 
    ... 
  }
}

trait CopyBigUserServiceComponent extends CopyServiceComponent {
  // Any change in implementation will have to be reflected in the module!
  val tmp: UserRepositoryComponent
  ...
}

On the other hand...

trait UserRepositoryComponent {
  val userRepositoryFactory: () => UserRepository

  class UserRepository {
    ...
  }
} 

trait CopyUserServiceComponent {
  self: UserRepositoryComponent =>
  // No problem here
  private val source: UserRepository = userRepositoryFactory()
  private val destination: UserRepository = userRepositoryFactory()
  class CopyUserServiceComponent { 
    ... 
  }
}

trait CopyBigUserServiceComponent extends CopyServiceComponent {
  self: UserRepositoryComponent =>
  // No problem here either
  val tmp: : UserRepository = userRepositoryFactory()
  ...
}

EDIT

Complementating the answer, let's consider two different needs:

  • I need many instances of UserRepository.

In this case, you are applying the pattern at the wrong level. In Jonas' example, UserRepository is at the level of a factory-providing singleton.

So, in that case, you wouldn't do UserRepository and UserRepositoryComponent but, say, UserRepositoryFactory and UserRepositoryFactoryComponent.

  • I need precisely two singleton UserRepository.

In this case, just do something like this:

trait UserRepositoryComponent {
  val sourceUserService: UserService
  val destinationUserService: UserService

  class UserService ...
}
like image 92
Daniel C. Sobral Avatar answered Oct 20 '22 03:10

Daniel C. Sobral


I assume, Jonas in his article refers to a widely accepted methodology of building scalable applications called Composite Software Construction that in a few words can be explained as follows: entire application (orchestrated on a meta-level) is an assembly built of independend components, which in their turn are compositions of other components and services. In terms of composite software, 'cake' (ComponentRegistry object in the example) is an assembly of components ('UserServiceComponent' and 'UserRepositoryComponent') etc. Whilst in the example components enclose service implementations it can hardly happen in the real-world.

In your example you don't need to define an inner class - you can put your workflow in an ordinary method:

trait CopyUserServiceComponent {
  val source: UserRepositoryComponent
  val destination: UserRepositoryComponent

  def copy = {...}
}

It perfectly conforms to original pattern - essential feature of the cake is not [only] specifying dependencies via self-type annotations, but also ability to abstract from the concrete implementation, till the moment when you need to build an assembly from the components.

like image 39
Vasil Remeniuk Avatar answered Oct 20 '22 04:10

Vasil Remeniuk