Durante los últimos meses he estado trabajando en un proyecto utilizando Scala como lenguaje de programación principal. La mezcla de programación orientada a objetos y programación funcional que permite Scala resulta interesante, pero cada vez estoy más convencido de que la verdadera potencia del mismo se consigue cuando se aplica un estilo puramente funcional. Al fin y al cabo, si únicamente queremos un lenguaje con algunas características funcionales que hagan nuestro código un poco más claro y limpio, hay otras opciones que nos dan eso (véanse la última versión de Java o Kotlin), sin las complejidades con las que hay que lidiar en Scala.

Supongo que escribiré en el futuro un artículo más elaborado sobre el tema, pero dejadme que hoy me centre en algo que llevo viendo bastante cuando examino código en Scala, casi siempre cuando éste ha sido escrito por desarrolladoras que tienen amplia experiencia en algún lenguaje orientado a objetos (como Java) y empiezan a escribir código en Scala: cómo hacer inyección de dependencias.

Empezaré proponiendo un escenario muy básico para analizar el problema y algunas de las soluciones que proporciona el lenguaje. Supongamos que queremos crear un servicio de informes que nos devuelva los balances del último mes de todas las cuentas bancarias abiertas por un cliente. Nuestros clientes están relacionados con todas las cuentas que tienen abiertas (serían nuestro Aggregate Root si hablásemos de Domain Driven Design) y se identifican por una secuencia alfanumérica de longitud diez elementos. Dichas cuentas mantienen todos los movimientos ordenados por su fecha de ejecución. Un balance contiene, en definitiva, el número que identifica a una cuenta, la fecha hasta donde el balance ha contabilizado y una cantidad de dinero.

De esta forma, definimos nuestro servicio con la siguiente operación:

type Id = String

case class Balance(acNo: String, balanceAt: Date, amount: Money)

trait Reporting {
  def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance]
}

Los clientes son accesibles dados sus identificadores vía un repositorio:

trait CustomerRepository {
  def retrieve(customerId: Id): Option[Customer]
}

Nuestra implementación del servicio de informes necesitará una instancia válida del repositorio de clientes para poder tener acceso al cliente solicitado y hacer el cálculo de los balances en sus cuentas. Hasta el momento, lo que he visto es que un porcentaje alto de desarrolladores que vienen de otros lenguajes como Java harán algo así para implementar dicho servicio:

class ReportingInterpreter(repo: CustomerRepository) implements Reporting {
  override def monthlyBalance(customerId: Id, year: Int, month: Int): List[Balance] = //do something here...
}

A muchos desarrolladores acostumbrados al paradigma orientado a objetos, esta implementación les parecerá bastante natural (y acertada). Sin embargo, desde el punto de vista de la programación funcional, tiene el principal inconveniente de que en ninguna parte se hace explícito que la operación monthlyBalance necesita de un repositorio de clientes. Alguien que utilice esta función nunca sabrá que, sin un repositorio de clientes, dicha operación no podrá ser ejecutada. El repositorio de clientes es parte del contexto de ejecución de la clase ReportingInterpreter, a quien se lo estamos inyectando a través de su constructor.

Esta situación puede mejorarse un poco aplicando un diseño algo más funcional a nuestra función monthlyBalance. ¿Por qué no pasarle explícitamente el repositorio de clientes como un parámetro de entrada a dicha función? De esta forma tendríamos:

trait Reporting {
  def monthlyBalance(customerId: Id, year: Int, month: Int, repo: CustomerRepository): List[Balance]
}

Ahora ya hemos hecho explícita la dependencia de la función monthlyBalance con el repositorio de clientes y podemos decir que nuestro diseño es más funcional. Sin embargo, todavía hay algunas desventajas con esta solución. Una de ellas es que añadimos un parámetro extra a nuestra función, lo que hace que ésta sea más difícil de ser llamada. Si nuestro módulo Reporting crece y ofrece muchas otras funciones que también dependan de CustomerRepository (y es probable que dichas funciones también dependan de este repositorio), siempre tendremos que pasar éste como último parámetro en todas nuestras llamadas a funciones del módulo Reporting. Esto resulta especialmente engorroso si queremos componer nuevas funciones de dicho módulo para ofrecer nuevas funcionalidades.

¿Cómo podemos mejorar esta situación? Teniendo en cuenta que intentamos utilizar un estilo de programación cada vez más funcional, podemos hacer uso de la idea de que en un lenguaje funcional las funciones son objetos de primera clase que pueden ser utilizados como cualquier otro tipo de objeto. Por ejemplo, pueden ser pasados como parámetros de entrada a otras funciones (creando de esta forma funciones de orden superior). Otra cosa que a veces olvidamos quienes venimos de lenguajes orientados a objetos y no tenemos demasiada experiencia en programación funcional, es que las funciones pueden devolver otras funciones. ¿Qué pasaría si hacemos que nuestra función monthlyBalance devolviera otra función que toma como único parámetro de entrada un repositorio de clientes y devuelve los balances mensuales de las cuentas de un cliente? Vemos cómo podríamos definir esto en Scala:

trait Reporting {
  def monthlyBalance(customerId: Id, year: Int, month: Int): CustomerRepository => List[Balance]
}

Ahora podemos utilizar nuestra función para obtener el balance mensual sin tener que proporcionar el repositorio de clientes:

val reporter: Reporting = ...
val balance = reporter. monthlyBalance(".......", 2017, 6)

Por supuesto, lo que obtenemos no es el balance todavía pues, para obtener el balance, necesitamos proporcionar el repositorio de clientes, sino una función capaz de devolver los balances cuando se le pasa un repositorio adecuado. De esta forma, hemos conseguido obtener una definición de nuestro balance sin tener que resolver la dependencia todavía y, de momento, no hemos ejecutado nada aún. Estamos aplicando un principio bastante interesante y repetido en programación funcional: define pronto, ejecuta tarde.

Supongamos que del balance queremos obtener la cantidad total, podemos aplicar composición de funciones para obtenerlo:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6) _ andThen (map(_.amount))

Y, de nuevo, todavía no hemos proporcionado el repositorio de clientes ni hemos ejecutado nada. Cuando tengamos disponible una instancia válida de CustomerRepository, podemos hacer:

val repo: CustomerRepository = ...

print("this is the monthly balance amount: " + amount(repo)) //amount is a function

Por último, podemos valernos de una herramienta común en programación funcional: las mónadas. Más concretamente, la mónada Reader resuelve nuestro problema de una forma puramente funcional y muy elegante.

En primer lugar, debemos cambiar la definición de nuestra función para que devuelva una instancia de Reader en lugar de una función (podemos decir que, en cierta forma, Reader encapsula una función que, a partir de un contexto que se le pasa como parámetro de entrada, nos devuelve un resultado). En este ejemplo, utilizaré la implementación de Reader proporcionada en por la librería scalaz.

trait Reporting {
   def monthlyBalance(customerId: Id, year: Int, month: Int): Reader[CustomerRepository, List[Balance]]
   ...
}

Ahora, nuestro método monthlyBalance nos devolverá una instancia de Reader. ¿Qué podemos hacer con ella? Muchas cosas, como, por ejemplo, extraer las cantidades de los balances de una forma más limpia que como hicimos anteriormente gracias a la función map:

val amount = reporter.monthlyBalance("ad82-fdad9-2429-a23bc-2b9c0", 2017, 6).map(b => b.map(_.amount))

Como se trata de una mónada, podemos componer instancias de Reader dentro de una estructura for comprehension, por ejemplo, para obtener la evolución de los balances en los primeros tres meses del año:

val thisYear = ...
val firstQuarterReport = for {
   balances1 <- monthlyBalance(customerId, thisYear, 1)
   balances2 <- monthlyBalance(customerId, thisYear, 2)
   balances3 <- monthlyBalance(customerId, thisYear, 3)
} yield balances1 ++ balances2 ++ balances3

De nuevo, todavía no hemos ejecutado nada, solamente hemos proporcionado una definición de los balances del primer trimestre. ¿Cómo resolvemos la dependencia para tener el resultado final? Para ello, utilizamos el método run proporcionado por la mónada Reader:

val balancesQ1 = firstQuarterReport.run(customerRepository)

Es justo en ese momento cuando se ejecuta nuestro código y todo queda resuelto.

Hemos visto tres alternativas funcionales a hacer inyección de dependencias mediante el constructor de clase:

  • Proporcionar la dependencia a través de un parámetro extra añadido a nuestra función
  • Dividir la lista de parámetros para poder hacer currying, obteniendo una función a que devuelve otra función
  • Utilizar la mónada Reader como tipo de retorno

Todas ellas, alternativas mucho más funcionales que, además, nos permiten diferir la resolución de nuestra dependencia hasta justo el momento en el que resulta imprescindible.