Inyección de dependencias en programación funcional III. Mónada Reader

Llegamos a la la última entrada de la sería de inyección de dependencias con la entrada, Inyección de dependencias en programación funcional III. Mónada Reader. El objetivo de la misma es mostrar al lector cómo se realiza la inyección de dependencias con la mónada Reader en lenguaje Scala. Para el lector interesado, las entradas de la serie son las siguientes:

La diferencia conceptual respecto a las otras dos es el uso de la mónada Reader. La mónada Reader es aquella mónada la cual puede leer un determinado componente; en dicho  componente, es donde definimos los elementos con las referencias de las funciones a inyectar. Así, necesitamos definir un elemento, en nuestro caso una case class, con las referencias a las funciones las cuáles están definidas en los componentes. Por otro lado, el servicio de negocio lo definimos a partir de un trait con un constructor de tipos.

Desde un punto de vista gráfico, la vista estática de los componentes queda definida como sigue:

Los tipos utilizados son los siguientes:

import cats.data.Reader
import cats.syntax.either._
import scala.language.higherKinds
object typesEjem3{
  type MensajeError = String
  type GetComponent1 = (String) => Either[MensajeError, String]
  type GetComponent2 = (Int) => Either[MensajeError, Int]
  type ResponseService = Either[MensajeError, String]
  type ParameterString = String
  type ParameterInt = Int
  type ServiceOperation[A] = Reader[ServiceContext, A]
  case class ServiceContext( funcComponent1: GetComponent1, funcComponent2: GetComponent2 )
}

La definición de los componentes de negocio del ejemplo son los representados por el objeto Component1Ejem3 y Component2Ejem3. El snippet del código de los componentes es el siguiente:

object Component1Ejem3{
  import typesEjem3._
  val response1: MensajeError = "Error en Response1"
  val doSomething: GetComponent1 = (elem: String) => {
    elem.length match {
      case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
      case _ => response1.asLeft
    }
  }
}
object Component2Ejem3{
  import typesEjem3._
  val response2: MensajeError = "Error en Response2"
  val doSomething: GetComponent2 = (num: Int) => {
    num match {
      case elem: Int if elem > 0 => elem.asRight
      case _ => response2.asLeft
    }
  }
}

La definición del servicio de negocio se realiza con un type class empleando un trait Service3 y el objeto ServiceImpl. Para el lector interesado en conocer lo que es un Type Class en los siguientes enlaces describo cómo se define y describe dicho patrón. Los enlaces son los siguientes:

El snippet del código del servicio es el siguiente:

trait Service3[ F[_] ]{
  def doBusiness(msg: typesEjem3.ParameterString): F[ Either[typesEjem3.MensajeError, String] ]
}
object ServiceImpl extends Service3[typesEjem3.ServiceOperation]{
  override def doBusiness(msg: typesEjem3.ParameterString): typesEjem3.ServiceOperation[Either[typesEjem3.MensajeError, String]] = Reader{ ctx =>
    for{
      response1 <- ctx.funcComponent1(msg).right
      response2 <- ctx.funcComponent2(msg.length).right
    }yield{
      response1 + "-" + response2
    }
  }
}

Como se muestra en el snippet anterior la función doBusiness del objeto ServiceImpl define la funcionalidad del servicio y es donde se utiliza la mónada Reader. La Mónada Reader se define de la siguiente manera : Reader[ServiceContext, A]; siendo la entrada de tipo ServiceContext; y, como salida, el tipo A el cual en nuestro caso es de tipo Either. Analizando la función, el objeto de entrada es de tipo ServiceContext con las referencias a los componentes que se inyectan y, como resultado, se retorna un elemento de tipo Either.

La aplicación que usa los anteriores elementos es la siguiente:

object Ejem3DependencyInyector extends App{
  import typesEjem3._
  def ejemplo1(): Unit = {
    val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
    val message1 = "Mensaje de prueba"
    ServiceImpl.doBusiness(message1).run(context) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    println
  }
  ejemplo1()
}

En la aplicación anterior, se muestra cómo usar un servicio con una mónada Reader: lo primero, es definir una clase ServiceContext con las funciones de los componentes; segunda, crear e invocar la clase con la mónada usando la función run; y, para finalizar, tratar el resultado con un pattern matching.

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17

La definición de los test del servicio de negocio descrito en el ejemplo es el siguiente:

import cats.syntax.all._
import es.ams.dependencyinyector.typesEjem3.{ GetComponent1, GetComponent2, ServiceContext} //
import org.scalatest.{Matchers, WordSpec}
class Ejem3DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
    "Example OK: mock component1" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val context = ServiceContext(funcGetResponse1Mock, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-6"))
    }
    "Example OK: mock component2" in {
      val funcComponent2: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(Component1Ejem3.doSomething,funcComponent2)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
    }
    "Example OK: mock component1 and mock component2" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(funcGetResponse1Mock, funcGetResponse2Mock)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-0"))
    }
  }
}

La inyección de dependencias desde un punto de vista funcional sigue la misma filosofía que la inyección de dependencias de objetos. La primera consecuencia es la desaparición de la utilización de framework de Mock necesarios en otros paradigmas como el utilizado en los lenguajes Java o Python. La utilización del paradigma funcional permite la composición de elementos más intuitiva aunque, evidentemente, la curva de aprendizaje es mayor.

Deja un comentario