Application de ZIO ZLayer

En juillet, OTUS lance un nouveau cours "Scala-developer" , dans le cadre duquel nous avons préparé une traduction de matériel utile pour vous.








La nouvelle fonctionnalité ZLayer dans ZIO 1.0.0-RC18 + est une amélioration significative par rapport à l'ancien modèle de module, rendant l'ajout de nouveaux services beaucoup plus rapide et plus facile. Cependant, dans la pratique, j'ai constaté que la maîtrise de cet idiome peut prendre un certain temps.



Vous trouverez ci-dessous un exemple annoté de la version finale de mon code de test dans lequel j'examine un certain nombre de cas d'utilisation. Un grand merci à Adam Fraser pour m'avoir aidé à optimiser et affiner mon travail. Les services sont intentionnellement simplifiés, donc j'espère qu'ils seront suffisamment clairs pour être lus rapidement.



Je suppose que vous avez une compréhension de base des tests ZIO et que vous connaissez les informations de base concernant les modules.



Tout le code s'exécute dans les tests zio et constitue un seul fichier.



Voici le conseil:



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")


Des noms



Donc, nous sommes arrivés à notre premier service - Noms (Noms)



 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)
  }


Tout ici est dans le cadre d'un modèle modulaire typique.



  • Déclarer des noms comme alias de type pour Has
  • Dans l'objet, définissez Service comme un trait
  • Créez une implémentation (bien sûr, vous pouvez en créer plusieurs),
  • Créez un ZLayer à l'intérieur de l'objet pour l'implémentation donnée. La convention ZIO a tendance à les appeler en temps réel.
  • Un objet package est ajouté qui fournit un raccourci facile d'accès.


En live, il est utilisé ZLayer.fromServicece qui est défini comme:



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


En ignorant Tagged (cela est nécessaire pour que tous les Has / Layers fonctionnent), vous pouvez voir qu'ici la fonction f: A => B est utilisée - qui dans ce cas est juste un constructeur de la classe case pour NamesImpl.



Comme vous pouvez le voir, Names nécessite Random de l'environnement zio pour fonctionner.



Voici un test:



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


Il utilise ZIO.accessMpour extraire les noms de l'environnement. _.get récupère le service.



Nous fournissons des noms pour le test comme suit:



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


provideCustomLayerajoute la couche Noms à l'environnement existant.



Les équipes



L'essence des Teams (Teams) est de tester les dépendances entre les modules, que nous avons créés.



 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)

  }


Les équipes sélectionneront une équipe parmi les noms disponibles par taille .



En suivant les modèles d'utilisation du module, bien que pickTeam ait besoin de Names pour fonctionner , nous ne le mettons pas dans le ZIO [Names, Nothing, Set [String]] - à la place, nous en gardons une référence TeamsImpl.



Notre premier test est simple.



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


Pour l'exécuter, nous devons lui donner une couche Teams:



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


Qu'est-ce que ">>>"?



C'est une composition verticale. Cela indique que nous avons besoin de la couche Names , qui a besoin de la couche Teams .



Cependant, lors de l'exécution de cela, il y a un petit problème.



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


Revenir à la définition NamesImpl



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


Le nôtre NamesImplest donc créé deux fois. Quel est le risque si notre service contient une ressource système d'application unique? En fait, il s'avère que le problème ne réside pas du tout dans le mécanisme des couches - les couches sont mémorisées et ne sont pas créées plusieurs fois dans le graphe de dépendances. Il s'agit en fait d'un artefact de l'environnement de test.



Changeons notre suite de tests en:



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


Cela résout un problème, ce qui signifie que la couche n'est créée qu'une seule fois dans le test.



JustTeamsTest ne nécessite que des équipes . Mais que faire si je voulais accéder aux équipes et aux noms ?



 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
  }


Pour que cela fonctionne, nous devons fournir les deux:



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


Ici, nous utilisons le combinateur ++ pour créer la couche Names avec Teams . Faites attention à la priorité des opérateurs et aux parenthèses supplémentaires



(Names.live >>> Teams.live)


Au début, je suis tombé pour moi-même - sinon le compilateur ne le fera pas correctement.



L'histoire



L'histoire est un peu plus compliquée.



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))
    }
    
  }


Le constructeur HistoryImplnécessite de nombreux noms . Mais le seul moyen de l'obtenir est de le retirer de Teams . Et cela nécessite ZIO - nous l'utilisons ZLayer.fromServiceMdonc pour nous donner ce dont nous avons besoin.

Le test est effectué de la même manière que précédemment:



 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))


Et c'est tout.



Erreurs pouvant être rejetées



Le code ci-dessus suppose que vous renvoyez ZLayer [R, Nothing, T] - en d'autres termes, la construction du service d'environnement est de type Nothing. Mais s'il fait quelque chose comme la lecture d'un fichier ou d'une base de données, alors ce sera probablement ZLayer [R, Throwable, T] - parce que ce genre de chose implique souvent le facteur très externe qui est à l'origine de l'exception. Imaginez donc qu'il y ait une erreur dans la construction Names. Il existe un moyen pour vos tests de contourner ce problème:



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


puis à la fin du test



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


mapErrortransforme l'objet throwableen un échec de test - c'est ce que vous voulez - cela peut indiquer que le fichier de test n'existe pas ou quelque chose du genre.



Plus de cas ZEnv



Les éléments "standard" de l'environnement incluent Clock et Random. Nous avons déjà utilisé Random dans nos noms. Mais que se passe-t-il si nous voulons également que l'un de ces éléments «abaisse» davantage nos dépendances? Pour ce faire, j'ai créé une deuxième version de History - History2 - et ici Clock est nécessaire pour créer une instance.



 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)
    }
    
  }


Ce n'est pas un exemple très utile, mais la partie importante est que la ligne



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


nous oblige à fournir l'horloge au bon endroit.



Vous .provideCustomLayerpouvez maintenant ajouter notre couche à la pile de couches et elle apparaît comme par magie aléatoire dans les noms. Mais cela ne se produira pas pendant les heures requises ci-dessous dans History2. Par conséquent, le code suivant ne compile PAS:



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)),


Au lieu de cela, vous devez fournir l' History2.livehorloge explicitement, ce qui se fait comme suit:



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


Clock.anyEst une fonction qui obtient n'importe quelle horloge disponible d'en haut. Dans ce cas, ce sera une horloge de test, car nous n'avons pas essayé de l'utiliser Clock.live.



La source



Le code source complet (à l'exclusion du jetable) est indiqué ci-dessous:



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))
}


Pour des questions plus avancées, veuillez contacter Discord # zio-users ou visitez le site Web et la documentation de zio.






En savoir plus sur le cours.







All Articles