Como ustedes sabrán, cualquier aplicación no trivial está formada por dos o más clases que colaboran entre sí para realizar alguna lógica. Tradicionalmente cada objeto se hacía cargo de obtener sus propias referencias a los objetos a los cuales colaboraba (sus dependencias). Esto lleva a código acoplado y difícil de probar. Cuando se aplica inyección de dependencia le decimos a una entidad externa que provea las dependencias a los objetos. Esto nos resuelve el problema del acoplamiento.
Que framework utilizar en Scala? Existen numerosos framework que podemos utilizar que proveen la implementación de la inyección de dependencia. Pero no creen que vamos muy rápido? Ya estamos pensando en un framework? Si la inyección de dependencias es tan buena, porque el lenguaje no nos brinda una forma de implementarla? Existen lenguajes que proveen la inyección de dependencia en el mismo lenguaje un ejemplo era Noop.
Scala brinda dos formas de implementar inyección de dependencias sin utilizar framework. La primera es por medio de cake patterns y la segunda es utilizando reader monad.
Probablemente cake patterns es la técnica más popular para implementar inyección de dependencia sin utilizar un framework. En esta técnica se basa en la técnica de mixing. Veamos un ejemplo:
object idExample {
class User(var id: Int, var firstName: String, var lastName: String, var email: String, var supervisorId: Int) {}
trait UserRepositoryComponent {
def userRepository: UserRepository
trait UserRepository {
def get(id: Int): User
def find(username: String): User
}
}
trait Users {
this: UserRepositoryComponent =>
def getUser(id: Int): User = {
userRepository.get(id)
}
def findUser(username: String): User = {
userRepository.find(username)
}
}
trait UserInfo extends Users {
this: UserRepositoryComponent =>
def userEmail(id: Int): String = {
getUser(id).email
}
def userInfo(username: String): Map[String, String] = {
val user = findUser(username)
val boss = getUser(user.supervisorId)
Map(
"fullName" -> s"${user.firstName} ${user.lastName}",
"email" -> s"${user.email}",
"boss" -> s"${boss.firstName} ${boss.lastName}")
}
}
trait UserRepositoryComponentImpl extends UserRepositoryComponent {
def userRepository = new UserRepositoryImpl
class UserRepositoryImpl extends UserRepository {
var db: Map[Int, User] = Map()
var dbUserName: Map[String, User] = Map()
def save(user: User) = {
db += (user.id -> user)
dbUserName += (user.email -> user)
}
def get(id: Int) = db(id)
def find(username: String) = dbUserName(username)
}
}
object UserInfoImpl extends
UserInfo with
UserRepositoryComponentImpl
}
Como podemos ver UserInfoImpl hereda de UserInfo pero utiliza los métodos de UserRepositoryComponentImpl usando mixing.
La otra técnica es utilizar reader monad para explicarla es necesario entender monad, para luego centrarnos en el monad reader.
Básicamente una función con un solo parámetro es un objeto de tipo Function1, por ejemplo la función triple es de tipo Int => Int
val triple = (i: Int) => i * 3
triple(3) // => 9
Pero Int => Int es sólo una forma elegante de decir Function1[Int, Int] y Function1 nos permite crear nuevas funciones usando andThen.
val thricePlus2 = triple andThen (i => i + 2)
thricePlus2(3) // => 11
El método andThen combina dos función de un parámetro, generando una tercera función la cual aplica la primera función y luego la segunda. A la vez andThen puede cambiar los tipos de salida, por ejemplo:
val f = thricePlus2 andThen (i => i.toString)
f(3) // => "11"
El tipo de f es Int => String.
Reader Monad es un monad definido para funciones unarias, que utiliza andthen como la operación map veamos un ejemplo:
val triple = Reader((i: Int) => i * 3)
triple(3) // => 9
val thricePlus2 = triple map (i => i + 2)
thricePlus2(3) // => 11
Todo muy lindo pero y esto como me puede ayudar a implementar la inyección de dependencia?
Podemos utilizar Reader Monad para implementar inyección de dependencia, nosotros solo necesitamos definir las funciones con UserRepository como parámetro y podemos envolver cada una de estas funciones en un scalaz.Reader:
trait Users {
import scalaz.Reader
def getUser(id: Int) = Reader((userRepository: UserRepository) => userRepository.get(id) )
def findUser(username: String) = Reader((userRepository: UserRepository) => userRepository.find(username) )
}
Noten que las funciones devuelven un objeto de tipo Reader[UserRepository, User] y no un usuario. Luego el Reader devolverá un usuario cuando se le asigne un repositorio. La inyección de dependencia se difiere.
Pero en algún momento debemos indicar a la aplicación que utilice una implementación. Esto se puede hacer en una capa superior por ejemplo un controler:
object Application extends Application(UserRepositoryImpl)
class Application(userRepository: UserRepository) extends Controller with Users {
}
Otras formas de implementar inyección de dependencia es utilizar algún framework. Por supuesto podemos utilizar los frameworks de java, que entre los más utilizados esta Spring y Guice.
Pero también tenemos frameworks en Scala entre los que podemos nombrar:
- Subcut: provee tanto inyección de dependencias como servicio de búsqueda.
- Scaldi: Similar a Subcut.
Scala nos provee la inyección de dependencia tanto con técnicas del lenguaje como con frameworks de java y Scala.