Aplicación de ZIO ZLayer

En julio, OTUS lanzará un nuevo curso "Scala-developer" , en relación con el cual hemos preparado una traducción de material útil para usted.








La nueva característica ZLayer en ZIO 1.0.0-RC18 + es una mejora significativa en el patrón del módulo anterior, lo que hace que agregar nuevos servicios sea mucho más rápido y fácil. Sin embargo, en la práctica, he descubierto que puede llevar un tiempo dominar este idioma.



A continuación se muestra un ejemplo anotado de la versión final de mi código de prueba en el que analizo varios casos de uso. Muchas gracias a Adam Fraser por ayudarme a optimizar y refinar mi trabajo. Los servicios se simplifican intencionalmente, por lo que esperamos que sean lo suficientemente claros como para leerlos rápidamente.



Supongo que tiene una comprensión básica de las pruebas ZIO y que está familiarizado con la información básica sobre los módulos.



Todo el código se ejecuta en pruebas zio y es un solo archivo.



Aquí está el consejo:



import zio._
import zio.test._
import zio.random.Random
import Assertion._

object LayerTests extends DefaultRunnableSpec {

  type Names = Has[Names.Service]
  type Teams = Has[Teams.Service]
  type History = Has[History.Service]

  val firstNames = Vector( "Ed", "Jane", "Joe", "Linda", "Sue", "Tim", "Tom")


Nombres



Entonces, llegamos a nuestro primer servicio - Nombres (Nombres)



 type Names = Has[Names.Service]

  object Names {
    trait Service {
      def randomName: UIO[String]
    }

    case class NamesImpl(random: Random.Service) extends Names.Service {
      println(s"created namesImpl")
      def randomName = 
        random.nextInt(firstNames.size).map(firstNames(_))
    }

    val live: ZLayer[Random, Nothing, Names] =
      ZLayer.fromService(NamesImpl)
  }

  package object names {
    def randomName = ZIO.accessM[Names](_.get.randomName)
  }


Todo aquí está dentro del marco de un patrón modular típico.



  • Declarar nombres como un alias de tipo para Has
  • En el objeto, defina Servicio como un rasgo
  • Cree una implementación (por supuesto, puede crear múltiples),
  • Cree un ZLayer dentro del objeto para la implementación dada. La convención ZIO tiende a llamarlos en tiempo real.
  • Se agrega un objeto de paquete que proporciona un acceso directo fácil de acceder.


En vivo se usa lo ZLayer.fromServiceque se define como:



def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]


Ignorando Etiquetado (esto es necesario para que todos los Has / Layers funcionen), puede ver que aquí se usa la función f: A => B , que en este caso es solo un constructor de la clase de caso NamesImpl.



Como puede ver, Names requiere Random del entorno zio para funcionar.



Aquí hay una prueba:



def namesTest = testM("names test") {
    for {
      name <- names.randomName
    }  yield {
      assert(firstNames.contains(name))(equalTo(true))
    }
  }


Se utiliza ZIO.accessMpara extraer nombres del entorno. _.get recupera el servicio



Proporcionamos nombres para la prueba de la siguiente manera:



 suite("needs Names")(
       namesTest
    ).provideCustomLayer(Names.live),


provideCustomLayeragrega la capa de nombres al entorno existente.



Equipos



La esencia de los equipos (equipos) es probar las dependencias entre módulos, que hemos creado.



 object Teams {
    trait Service {
      def pickTeam(size: Int): UIO[Set[String]]
    }

    case class TeamsImpl(names: Names.Service) extends Service {
      def pickTeam(size: Int) = 
        ZIO.collectAll(0.until(size).map { _ => names.randomName}).map(_.toSet ) // ,  ,     < !   
    }

    val live: ZLayer[Names, Nothing, Teams] =
      ZLayer.fromService(TeamsImpl)

  }


Los equipos seleccionarán un equipo de los nombres disponibles por tamaño .



Siguiendo los patrones de uso del módulo, aunque pickTeam necesita nombres para funcionar , no lo colocamos en ZIO [Names, Nothing, Set [String]] ; en cambio, guardamos una referencia a él TeamsImpl.



Nuestra primera prueba es simple.



 def justTeamsTest = testM("small team test") {
    for {
      team <- teams.pickTeam(1)
    }  yield {
      assert(team.size)(equalTo(1))
    }
  }


Para ejecutarlo, debemos darle una capa de equipos:



 suite("needs just Team")(
      justTeamsTest
    ).provideCustomLayer(Names.live >>> Teams.live),


¿Qué es ">>>"?



Esta es una composición vertical. Indica que necesitamos la capa de nombres , que necesita la capa de equipos .



Sin embargo, al ejecutar esto, hay un pequeño problema.



created namesImpl
created namesImpl
[32m+[0m individually
  [32m+[0m needs just Team
    [32m+[0m small team test
[36mRan 1 test in 225 ms: 1 succeeded, 0 ignored, 0 failed[0m


Volviendo a la definición NamesImpl



case class NamesImpl(random: Random.Service) extends Names.Service {
      println(s"created namesImpl")
      def randomName = 
        random.nextInt(firstNames.size).map(firstNames(_))
    }


Entonces el nuestro NamesImples creado dos veces. ¿Cuál es el riesgo si nuestro servicio contiene algún recurso de sistema de aplicación único? De hecho, resulta que el problema no está en absoluto en el mecanismo de capas: las capas se recuerdan y no se crean varias veces en el gráfico de dependencia. Esto es en realidad un artefacto del entorno de prueba.



Cambiemos nuestro conjunto de pruebas a:



suite("needs just Team")(
      justTeamsTest
    ).provideCustomLayerShared(Names.live >>> Teams.live),


Esto soluciona un problema, lo que significa que la capa se crea solo una vez en la prueba.



JustTeamsTest solo requiere equipos . Pero, ¿y si quisiera acceder a Equipos y Nombres ?



 def inMyTeam = testM("combines names and teams") {
    for {
      name <- names.randomName
      team <- teams.pickTeam(5)
      _ = if (team.contains(name)) println("one of mine")
        else println("not mine")
    } yield assertCompletes
  }


Para que esto funcione, necesitamos proporcionar ambos:



 suite("needs Names and Teams")(
       inMyTeam
    ).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),


Aquí estamos usando el combinador ++ para crear la capa de Nombres con Teams . Preste atención a la precedencia del operador y paréntesis adicionales



(Names.live >>> Teams.live)


Al principio, me enamoré de mí mismo; de lo contrario, el compilador no lo hará bien.



Historia



La historia es un poco más complicada.



object History {
    
    trait Service {
      def wonLastYear(team: Set[String]): Boolean
    }

    case class HistoryImpl(lastYearsWinners: Set[String]) extends Service {
      def wonLastYear(team: Set[String]) = lastYearsWinners == team
    }
    
    val live: ZLayer[Teams, Nothing, History] = ZLayer.fromServiceM { teams => 
      teams.pickTeam(5).map(nt => HistoryImpl(nt))
    }
    
  }


El constructor HistoryImplrequiere muchos nombres . Pero la única forma de obtenerlo es sacándolo de los equipos . Y requiere ZIO, por lo que lo usamos ZLayer.fromServiceMpara darnos lo que necesitamos.

La prueba se lleva a cabo de la misma manera que antes:



 def wonLastYear = testM("won last year") {
    for {
      team <- teams.pickTeams(5)
      ly <- history.wonLastYear(team)
    } yield assertCompletes
  }

    suite("needs History and Teams")(
      wonLastYear
    ).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live))


Y eso es todo.



Errores arrojables



El código anterior supone que está devolviendo ZLayer [R, Nothing, T]; en otras palabras, la construcción del servicio de entorno es de tipo Nothing. Pero si hace algo como leer un archivo o una base de datos, lo más probable es que sea ZLayer [R, Throwable, T], porque este tipo de cosas a menudo implica el factor muy externo que está causando la excepción. Entonces imagine que hay un error en la construcción de nombres. Hay una manera para que sus pruebas eviten esto:



val live: ZLayer[Random, Throwable, Names] = ???


luego al final de la prueba



.provideCustomLayer(Names.live).mapError(TestFailure.test)


mapErrorconvierte el objeto throwableen una falla de prueba, eso es lo que desea, podría decir que el archivo de prueba no existe o algo así.



Más casos de ZEnv



Los elementos "estándar" del entorno incluyen Reloj y Aleatorio. Ya hemos usado Aleatorio en nuestros nombres. Pero, ¿qué sucede si también queremos que uno de estos elementos "reduzca" aún más nuestras dependencias? Para hacer esto, creé una segunda versión de History - History2 - y aquí se necesita Clock para crear una instancia.



 object History2 {
    
    trait Service {
      def wonLastYear(team: Set[String]): Boolean
    }

    case class History2Impl(lastYearsWinners: Set[String], lastYear: Long) extends Service {
      def wonLastYear(team: Set[String]) = lastYearsWinners == team
    }
    
    val live: ZLayer[Clock with Teams, Nothing, History2] = ZLayer.fromEffect { 
      for {
        someTime <- ZIO.accessM[Clock](_.get.nanoTime)        
        team <- teams.pickTeam(5)
      } yield History2Impl(team, someTime)
    }
    
  }


Este no es un ejemplo muy útil, pero la parte importante es que la línea



 someTime <- ZIO.accessM[Clock](_.get.nanoTime)


nos obliga a proporcionar el reloj en el lugar correcto.



Ahora .provideCustomLayerpuede agregar nuestra capa a la pila de capas y mágicamente aparece Aleatorio en Nombres. Pero esto no sucederá durante las horas requeridas a continuación en History2. Por lo tanto, el siguiente código NO se compila:



def wonLastYear2 = testM("won last year") {
    for {
      team <- teams.pickTeam(5)
      _ <- history2.wonLastYear(team)
    } yield assertCompletes
  }

// ...
    suite("needs History2 and Teams")(
      wonLastYear2
    ).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History2.live)),


En cambio, debe proporcionar el History2.livereloj explícitamente, que se realiza de la siguiente manera:



 suite("needs History2 and Teams")(
      wonLastYear2
    ).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))


Clock.anyEs una función que obtiene cualquier reloj disponible desde arriba. En este caso, será un reloj de prueba, porque no intentamos usarlo Clock.live.



Fuente



A continuación se muestra el código fuente completo (excluyendo el que se puede lanzar):



import zio._
import zio.test._
import zio.random.Random
import Assertion._

import zio._
import zio.test._
import zio.random.Random
import zio.clock.Clock
import Assertion._

object LayerTests extends DefaultRunnableSpec {

  type Names = Has[Names.Service]
  type Teams = Has[Teams.Service]
  type History = Has[History.Service]
  type History2 = Has[History2.Service]

  val firstNames = Vector( "Ed", "Jane", "Joe", "Linda", "Sue", "Tim", "Tom")

  object Names {
    trait Service {
      def randomName: UIO[String]
    }

    case class NamesImpl(random: Random.Service) extends Names.Service {
      println(s"created namesImpl")
      def randomName = 
        random.nextInt(firstNames.size).map(firstNames(_))
    }

    val live: ZLayer[Random, Nothing, Names] =
      ZLayer.fromService(NamesImpl)
  }
  
  object Teams {
    trait Service {
      def pickTeam(size: Int): UIO[Set[String]]
    }

    case class TeamsImpl(names: Names.Service) extends Service {
      def pickTeam(size: Int) = 
        ZIO.collectAll(0.until(size).map { _ => names.randomName}).map(_.toSet )  // ,  ,     < !   
    }

    val live: ZLayer[Names, Nothing, Teams] =
      ZLayer.fromService(TeamsImpl)

  }
  
 object History {
    
    trait Service {
      def wonLastYear(team: Set[String]): Boolean
    }

    case class HistoryImpl(lastYearsWinners: Set[String]) extends Service {
      def wonLastYear(team: Set[String]) = lastYearsWinners == team
    }
    
    val live: ZLayer[Teams, Nothing, History] = ZLayer.fromServiceM { teams => 
      teams.pickTeam(5).map(nt => HistoryImpl(nt))
    }
    
  }
  
  object History2 {
    
    trait Service {
      def wonLastYear(team: Set[String]): Boolean
    }

    case class History2Impl(lastYearsWinners: Set[String], lastYear: Long) extends Service {
      def wonLastYear(team: Set[String]) = lastYearsWinners == team
    }
    
    val live: ZLayer[Clock with Teams, Nothing, History2] = ZLayer.fromEffect { 
      for {
        someTime <- ZIO.accessM[Clock](_.get.nanoTime)        
        team <- teams.pickTeam(5)
      } yield History2Impl(team, someTime)
    }
    
  }
  

  def namesTest = testM("names test") {
    for {
      name <- names.randomName
    }  yield {
      assert(firstNames.contains(name))(equalTo(true))
    }
  }

  def justTeamsTest = testM("small team test") {
    for {
      team <- teams.pickTeam(1)
    }  yield {
      assert(team.size)(equalTo(1))
    }
  }
  
  def inMyTeam = testM("combines names and teams") {
    for {
      name <- names.randomName
      team <- teams.pickTeam(5)
      _ = if (team.contains(name)) println("one of mine")
        else println("not mine")
    } yield assertCompletes
  }
  
  
  def wonLastYear = testM("won last year") {
    for {
      team <- teams.pickTeam(5)
      _ <- history.wonLastYear(team)
    } yield assertCompletes
  }
  
  def wonLastYear2 = testM("won last year") {
    for {
      team <- teams.pickTeam(5)
      _ <- history2.wonLastYear(team)
    } yield assertCompletes
  }


  val individually = suite("individually")(
    suite("needs Names")(
       namesTest
    ).provideCustomLayer(Names.live),
    suite("needs just Team")(
      justTeamsTest
    ).provideCustomLayer(Names.live >>> Teams.live),
     suite("needs Names and Teams")(
       inMyTeam
    ).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
    suite("needs History and Teams")(
      wonLastYear
    ).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live)),
    suite("needs History2 and Teams")(
      wonLastYear2
    ).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
  )
  
  val altogether = suite("all together")(
      suite("needs Names")(
       namesTest
    ),
    suite("needs just Team")(
      justTeamsTest
    ),
     suite("needs Names and Teams")(
       inMyTeam
    ),
    suite("needs History and Teams")(
      wonLastYear
    ),
  ).provideCustomLayerShared(Names.live ++ (Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live))

  override def spec = (
    individually
  )
}

import LayerTests._

package object names {
  def randomName = ZIO.accessM[Names](_.get.randomName)
}

package object teams {
  def pickTeam(nPicks: Int) = ZIO.accessM[Teams](_.get.pickTeam(nPicks))
}
  
package object history {
  def wonLastYear(team: Set[String]) = ZIO.access[History](_.get.wonLastYear(team))
}

package object history2 {
  def wonLastYear(team: Set[String]) = ZIO.access[History2](_.get.wonLastYear(team))
}


Para preguntas más avanzadas, comuníquese con Discord # zio-users o visite el sitio web y la documentación de zio.






Aprende más sobre el curso.







All Articles