La inyección de dependencias es un patrón que en otros paradigmas y lenguajes es un patrón muy utilizado; por ejemplo en Java, el framework Spring, se basa en el patrón de inyección de dependencias de objetos. En la programación funcional, la inyección de dependencias se realiza inyectando funciones, no objetos. En la presente entrada, Inyección de dependencias en programación funcional I, describiré la forma de inyectar funciones en lenguja Scala.
Supongamos que tenemos dos funciones que implementan la siguiente funcionalidad: la primera, dada un valor de tipo String de entrada, realiza la transformación de dicho parámetro concatenándole el valor «modificado»; la segunda función, dado un valor entero de entrada si es mayor a cero retorna dicho valor; en otro caso para las dos funciones, retorna un mensaje de error. El valor de retorno es un contenedor binario de tipo Either.
Por otro lado, definimos una función servicio que realiza una operación de negocio la cual utiliza las dos funciones anteriores descritas previamente. A esta función, para realizar su funcionalidad, necesitará que se le inyecten las funciones.
El objetivo del ejemplo es entender la inyección, no en definir una solución a un problema complejo.
Definición de tipos
Los tipos GetComponent1 y GetComponent2 definen las funciones básicas; el tipo Service, define la función de negocio; ResponseService, define el contenedor binario de respuesta del servicio; y, los tipos Parameter y MensajeError, otros tipos básicos necesarios.
En el siguiente snippet se define la definición en lenguaje Scala.
type MensajeError = String type GetComponent1 = (String) => Either[MensajeError, String] type GetComponent2 = (Int) => Either[MensajeError, Int] type ResponseService = Either[MensajeError, String] type Parameter = String type Service = (GetComponent1, GetComponent2) => (Parameter) => (ResponseService)
Definición de componentes
Teniendo la definición de tipos, necesitamos la implementación de las funciones. La primera función, definida con el tipo GetComponent1, define una función cuyo parámetro de entrada es de tipo String, si el parámetro de entrada es un string cuya longitud es mayor a 0, retorna un elemento Right del tipo Either con la concatenación de la propia cadena de entrada y la palabra » modificado».
La segunda función, definida con el tipo GetComponent2, define una función cuyo parámetro de entrada es de tipo entero, si el valor de entrada es mayor a cero, retorna el mismo valor,en otro caso, retorno un elemento Right de tipo entero con el valor de entrada.
El código con la definición de las funciones es la siguiente:
val response1: MensajeError = "Error en Response1" val funcGetResponse1: GetComponent1 = (elem: String) => { elem.length match { case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight case _ => response1.asLeft } } val response2: MensajeError = "Error en Response2" val funcGetResponse2: GetComponent2 = (num: Int) => { num match { case elem: Int if elem > 0 => elem.asRight case _ => response2.asLeft } }
Definición del servicio
La definición de la función de servicio tiene un formado tipo Curry. Los primeros parámetros corresponde con las funciones de los tipo GetComponent1 y 2; el segundo grupo, corresponde con el parámetro que se empleará en las función; y, por último, se define la funcionalidad propiamente de negocio; en esta última parte, es donde se define la funcionalidad de negocio con las funciones inyectadas y los parámetros, así como, el resultado parcial, si fuera necesario, de las funciones.
El código con la definición de la función de servicio es la siguiente:
val funcService: Service = (getComponent1, getComponent2) => (msg) => { for { respon1 <- getComponent1(msg) respon2 <- getComponent2(msg.length) } yield { respon1 + "-" + respon2 } }
La función del servicio, funcService, recibe por parámetro aquellas funciones que le son necesarias, es decir, se le inyecta los elementos necesarios para realizar su operativa.
Dados las descripciones de las funciones de los apartados anteriores, una ejemplo básico de uso de la función servicio con la inyección de funciones es el siguiente:
object Ejem1DependecyInyectorApp extends App { import Ejem1DependecyInyector._ def ejemplo1(): Unit = { val message1 = "Mensaje de prueba" funcService(funcGetResponse1, funcGetResponse2)(message1) match { case Right(msg) => println(s"Test1=${msg}") case Left(error) => println(error) } val message2 = "" funcService(funcGetResponse1, funcGetResponse2)(message2) match { case Right(msg) => println(s"Test2=${msg}") case Left(error) => println(error) } } ejemplo1() }
La salida por consola es la siguiente:
Test1=Mensaje de prueba modificado-17 Error en Response1
Test
Una tarea fundamental en el desarrollo del software es la realización de pruebas unitarias de aquellos componentes realizados. En nuestro caso, las pruebas unitarias de la función servicio dependerán de los resultado de las funciones inyectadas;y, para sus pruebas, es necesario moquear aquellas funciones inyectadas. En programación función con el patrón que presento, las pruebas se simplifican porque no hay que utilizar un framework específico, simplemente, necesitamos definir una función con un determinado valor la cual se inyecta a la función servicio.
Los test unitarios de la función servicio presentada son los siguientes:
class Ejem1DependecyInyectorTest extends WordSpec with Matchers { "Example Mock" should { "Example OK" in { val msg: String = "prueba" val result: String = funcService(funcGetResponse1, funcGetResponse2)(msg) match { case Right(msg) => { println(msg); msg} case Left(error) => error } result shouldBe(msg + " modificado-6") } "Example OK: mock component1" in { val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight val msg: String = "prueba" val result: String = funcService(funcGetResponse1, funcGetResponse2Mock )(msg) match { case Right(msg) => { println(msg); msg} case Left(error) => error } assert(result.length > 0) } "Example OK: mock component2" in { val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight val msg: String = "prueba" val result: String = funcService(funcGetResponse1Mock, funcGetResponse2 )(msg) match { case Right(msg) => { println(msg); msg} case Left(error) => error } assert(result.length > 0) assert(result.equals("mock-6")) } "Example OK: mock component1 and mock component2" in { val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight val msg: String = "prueba" val result: String = funcService(funcGetResponse1Mock, funcGetResponse2Mock )(msg) match { case Right(msg) => { println(msg); msg} case Left(error) => error } assert(result.length > 0) assert(result.equals("mock-0")) } } }
En las próximas entregas, continuaré profundizando en la inyección de dependencias.