Aplicação do ZIO ZLayer

Em julho, a OTUS está lançando um novo curso "Scala-developer" , com o qual preparamos uma tradução de material útil para você.








O novo recurso ZLayer no ZIO 1.0.0-RC18 + é uma melhoria significativa no antigo padrão de módulo, tornando a adição de novos serviços muito mais rápida e fácil. No entanto, na prática, descobri que pode demorar um pouco para dominar esse idioma.



Abaixo está um exemplo anotado da versão final do meu código de teste, na qual observo vários casos de uso. Muito obrigado a Adam Fraser por me ajudar a otimizar e refinar meu trabalho. Os serviços são intencionalmente simplificados, portanto, esperamos que sejam claros o suficiente para serem lidos rapidamente.



Suponho que você tenha um entendimento básico dos testes do ZIO e que esteja familiarizado com informações básicas sobre os módulos.



Todo o código é executado nos testes do zio e é um único arquivo.



Aqui está a dica:



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


Nomes



Então, chegamos ao nosso primeiro serviço - Names (Names)



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


Tudo aqui está dentro da estrutura de um padrão modular típico.



  • Declarar nomes como um alias de tipo para Has
  • No objeto, defina Serviço como uma característica
  • Crie uma implementação (é claro que você pode criar várias),
  • Crie um ZLayer dentro do objeto para a implementação fornecida. A convenção da ZIO costuma chamá-los em tempo real.
  • É adicionado um objeto de pacote que fornece um atalho de fácil acesso.


Ao vivo, é usado, ZLayer.fromServicedefinido como:



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


Ignorando Tagged (isso é necessário para que todos os Has / Layers funcionem), você pode ver que aqui a função f: A => B é usada - que neste caso é apenas um construtor da classe de caso NamesImpl.



Como você pode ver, o Names requer o Random do ambiente zio para funcionar.



Aqui está um teste:



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


Ele usa ZIO.accessMpara extrair nomes do ambiente. _.get recupera o serviço.



Fornecemos nomes para o teste da seguinte forma:



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


provideCustomLayeradiciona a camada de nomes ao ambiente existente.



Equipas



A essência das equipes (equipes) é testar as dependências entre os módulos que criamos.



 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)

  }


As equipes selecionam uma equipe entre os nomes disponíveis por tamanho .



Seguindo os padrões de uso do módulo, embora pickTeam precise de Names para funcionar , não o colocamos no ZIO [Names, Nothing, Set [String]] - em vez disso, mantemos uma referência a ele TeamsImpl.



Nosso primeiro teste é simples.



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


Para executá-lo, precisamos fornecer uma camada de equipes:



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


O que é ">>>"?



Esta é uma composição vertical. Indica que precisamos da camada Nomes , que precisa da camada Equipes .



No entanto, ao executar isso, há um pequeno 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


Voltando à definição NamesImpl



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


Então o nosso NamesImplé criado duas vezes. Qual é o risco se nosso serviço contiver algum recurso exclusivo do sistema de aplicativos? De fato, verifica-se que o problema não existe no mecanismo Camadas - as camadas são lembradas e não são criadas várias vezes no gráfico de dependência. Este é realmente um artefato do ambiente de teste.



Vamos mudar nossa suíte de testes para:



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


Isso corrige um problema, o que significa que a camada é criada apenas uma vez no



teste.O JustTeamsTest requer apenas equipes . Mas e se eu quisesse acessar equipes e nomes ?



 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 isso funcione, precisamos fornecer ambos:



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


Aqui estamos usando o combinador ++ para criar a camada de nomes com as equipes . Preste atenção à precedência do operador e parênteses extras



(Names.live >>> Teams.live)


No começo, eu me apaixonei por isso - caso contrário, o compilador não fará o que é certo.



História



A história é um pouco mais 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))
    }
    
  }


O construtor HistoryImplrequer muitos nomes . Mas a única maneira de obtê-lo é retirando-o das equipes . E isso requer o ZIO - portanto, usamos ZLayer.fromServiceMpara nos dar o que precisamos.

O teste é realizado da mesma maneira 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))


E isso é tudo.



Erros jogáveis



O código acima pressupõe que você está retornando o ZLayer [R, Nothing, T] - em outras palavras, a construção de serviço do ambiente é do tipo Nothing. Mas se fizer algo como ler um arquivo ou banco de dados, provavelmente será o ZLayer [R, Throwable, T] - porque esse tipo de coisa geralmente envolve o fator externo que está causando a exceção. Então, imagine que haja um erro na construção de nomes. Existe uma maneira de seus testes contornarem isso:



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


então no final do teste



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


mapErrortransforma o objeto throwableem uma falha de teste - é isso que você deseja - pode ser que o arquivo de teste não exista ou algo parecido.



Mais casos de ZEnv



Os elementos "padrão" do ambiente incluem Clock e Random. Já usamos Random em nossos nomes. Mas e se também queremos que um desses elementos "abaixe" ainda mais nossas dependências? Para fazer isso, criei uma segunda versão do History - History2 - e aqui o Clock é necessário para criar uma instância.



 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 não é um exemplo muito útil, mas a parte importante é que a linha



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


nos obriga a fornecer o relógio no lugar certo.



Agora .provideCustomLayerpode adicionar nossa camada à pilha de camadas e ela aparece magicamente Random em Names. Mas isso não acontecerá nas horas necessárias abaixo no History2. Portanto, o código a seguir NÃO é compilado:



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


Em vez disso, você precisa fornecer o History2.liverelógio explicitamente, da seguinte maneira:



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


Clock.anyÉ uma função que obtém qualquer relógio disponível de cima. Nesse caso, será um relógio de teste, porque não tentamos usar Clock.live.



Fonte



O código fonte completo (excluindo o lançamento) é mostrado abaixo:



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 perguntas mais avançadas, entre em contato com Discord # zio-users ou visite o site e a documentação do zio.






Saiba mais sobre o curso.







All Articles