Anwendung von ZIO ZLayer

Im Juli startet OTUS einen neuen Kurs "Scala-Entwickler" , in dessen Zusammenhang wir eine Übersetzung von nützlichem Material für Sie vorbereitet haben.








Die neue ZLayer-Funktion in ZIO 1.0.0-RC18 + stellt eine erhebliche Verbesserung des alten Modulmusters dar und macht das Hinzufügen neuer Dienste viel schneller und einfacher. In der Praxis habe ich jedoch festgestellt, dass es eine Weile dauern kann, diese Redewendung zu beherrschen.



Unten finden Sie ein kommentiertes Beispiel für die endgültige Version meines Testcodes, in dem ich eine Reihe von Anwendungsfällen betrachte. Vielen Dank an Adam Fraser, der mir geholfen hat, meine Arbeit zu optimieren und zu verfeinern. Die Dienste sind absichtlich vereinfacht, sodass sie hoffentlich klar genug sind, um schnell gelesen zu werden.



Ich gehe davon aus, dass Sie ein grundlegendes Verständnis für ZIO-Tests haben und mit grundlegenden Informationen zu Modulen vertraut sind.



Der gesamte Code wird in Zio-Tests ausgeführt und ist eine einzelne Datei.



Hier ist der Tipp:



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


Namen



Also kamen wir zu unserem ersten Dienst - Namen (Namen)



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


Alles hier ist im Rahmen eines typischen modularen Musters.



  • Deklarieren Sie Namen als Typalias für Has
  • Definieren Sie im Objekt Service als Merkmal
  • Erstellen Sie eine Implementierung (natürlich können Sie mehrere erstellen),
  • Erstellen Sie einen ZLayer innerhalb des Objekts für die angegebene Implementierung. Die ZIO-Konvention ruft sie in der Regel in Echtzeit auf.
  • Es wird ein Paketobjekt hinzugefügt, das eine leicht zugängliche Verknüpfung bietet.


Im Leben wird es verwendet, ZLayer.fromServicedas definiert ist als:



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


Wenn Sie Tagged ignorieren (dies ist erforderlich, damit alle Has / Layer funktionieren), können Sie sehen, dass hier die Funktion f: A => B verwendet wird - in diesem Fall nur ein Konstruktor der case-Klasse für NamesImpl.



Wie Sie sehen können, erfordert Names Random aus der Zio-Umgebung, um zu funktionieren.



Hier ist ein Test:



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


Es wird verwendet ZIO.accessM, um Namen aus der Umgebung zu extrahieren . _.get ruft den Dienst ab.



Wir geben Namen für den Test wie folgt an:



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


provideCustomLayerFügt der vorhandenen Umgebung die Ebene " Namen" hinzu .



Teams



Die Essenz der Teams (Teams) besteht darin, die Abhängigkeiten zwischen den von uns erstellten Modulen zu testen.



 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)

  }


Die Teams wählen ein Team aus den verfügbaren Namen nach Größe aus .



Den Pick- Verwendungsmustern folgend, obwohl pickTeam Namen benötigt, um zu funktionieren , fügen wir sie nicht in das ZIO [Names, Nothing, Set [String]] ein - stattdessen behalten wir einen Verweis darauf bei TeamsImpl.



Unser erster Test ist einfach.



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


Um es auszuführen, müssen wir ihm eine Team-Ebene geben:



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


Was ist ">>>"?



Dies ist eine vertikale Komposition. Es zeigt an, dass wir die Ebene " Namen" benötigen , die die Ebene " Teams" benötigt .



Wenn Sie dies ausführen, gibt es jedoch ein kleines Problem.



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


Zurück zur Definition NamesImpl



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


Also wird unsere NamesImplzweimal erstellt. Was ist das Risiko, wenn unser Service eine eindeutige Anwendungssystemressource enthält? Tatsächlich stellt sich heraus, dass das Problem überhaupt nicht im Ebenenmechanismus liegt - die Ebenen werden gespeichert und im Abhängigkeitsdiagramm nicht mehrmals erstellt. Dies ist tatsächlich ein Artefakt der Testumgebung.



Ändern wir unsere Testsuite in:



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


Dies behebt ein Problem, das bedeutet, dass die Ebene im Test nur einmal erstellt wird.



JustTeamsTest erfordert nur Teams . Aber was ist, wenn ich auf Teams und Namen zugreifen möchte ?



 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
  }


Damit dies funktioniert, müssen wir beides bereitstellen:



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


Hier verwenden wir den ++ - Kombinator , um die Namensebene mit Teams zu erstellen . Achten Sie auf die Priorität des Bedieners und zusätzliche Klammern



(Names.live >>> Teams.live)


Am Anfang bin ich selbst darauf hereingefallen - sonst macht der Compiler es nicht richtig.



Geschichte



Die Geschichte ist etwas komplizierter.



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


Der Konstruktor HistoryImplbenötigt viele Namen . Der einzige Weg, dies zu erreichen, besteht darin, es aus den Teams herauszuziehen . Und es erfordert ZIO - also verwenden wir ZLayer.fromServiceMes, um uns das zu geben, was wir brauchen.

Der Test wird auf die gleiche Weise wie zuvor durchgeführt:



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


Und alle.



Wirfbare Fehler



Der obige Code setzt voraus, dass Sie ZLayer [R, Nothing, T] zurückgeben. Mit anderen Worten, das Umgebungsdienstkonstrukt ist vom Typ Nothing. Wenn es jedoch so etwas wie das Lesen aus einer Datei oder Datenbank ausführt, ist es höchstwahrscheinlich ZLayer [R, Throwable, T] - da dies häufig den sehr externen Faktor betrifft, der die Ausnahme verursacht. Stellen Sie sich also vor, dass das Namenskonstrukt einen Fehler enthält. Es gibt eine Möglichkeit für Ihre Tests, dies zu umgehen:



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


dann am Ende des Tests



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


mapErrorverwandelt das Objekt throwablein einen Testfehler - das ist, was Sie wollen - es könnte sagen, dass die Testdatei nicht existiert oder so etwas.



Weitere ZEnv-Fälle



Zu den "Standard" -Elementen der Umgebung gehören Clock und Random. Wir haben Random bereits in unseren Namen verwendet. Aber was ist, wenn wir auch möchten, dass eines dieser Elemente unsere Abhängigkeiten weiter "senkt"? Zu diesem Zweck habe ich eine zweite Version von History erstellt - History2 - und hier wird Clock benötigt, um eine Instanz zu erstellen.



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


Dies ist kein sehr nützliches Beispiel, aber der wichtige Teil ist, dass die Linie



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


zwingt uns, die Uhr am richtigen Ort bereitzustellen.



Jetzt .provideCustomLayerkönnen Sie unsere Ebene zum Ebenenstapel hinzufügen und Random in Names auf magische Weise einfügen. Dies wird jedoch nicht für die unten in History2 erforderlichen Stunden geschehen. Daher wird der folgende Code NICHT kompiliert:



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


Stattdessen müssen Sie die History2.liveUhr explizit angeben. Dies geschieht wie folgt:



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


Clock.anyIst eine Funktion, die jede Uhr von oben zur Verfügung stellt. In diesem Fall handelt es sich um eine Testuhr, da wir nicht versucht haben, sie zu verwenden Clock.live.



Quelle



Der vollständige Quellcode (mit Ausnahme des Throwables) wird unten angezeigt:



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


Bei weiteren Fragen wenden Sie sich bitte an Discord # zio-Benutzer oder besuchen Sie die zio- Website und -Dokumentation .






Erfahren Sie mehr über den Kurs.







All Articles