ZIO ZLayer的应用

OTUS在7月推出了新课程“ Scala-developer”,与此相关的是,我们已为您准备了有用的材料的翻译。








ZIO 1.0.0-RC18 +中的新ZLayer功能是对旧模块模式的重大改进,使添加新服务变得更快,更容易。但是,在实践中,我发现掌握这一习语可能需要一段时间。



下面是我的测试代码最终版本的带注释的示例,其中我查看了一些用例。非常感谢Adam Fraser帮助我优化和完善了我的工作。这些服务是有意简化的,因此希望它们足够清晰以便快速阅读。



我假设您对ZIO测试有基本的了解,并且熟悉有关模块的基本信息。



所有代码都在zio测试中运行,并且是一个文件。



提示:



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


名字



因此,我们开始了我们的第一项服务-名称(名称)



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


这里的所有内容都在典型的模块化模式的框架内。



  • 将名称声明为Has的类型别名
  • 在对象中,将“服务”定义为特征
  • 创建一个实现(当然您可以创建多个实现),
  • 在给定实现的对象内部创建ZLayer。ZIO约定倾向于实时调用它们。
  • 添加了一个包对象,它提供了易于访问的快捷方式。


现场使用ZLayer.fromService它的定义为:



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


忽略Tagged(这对于所有Has / Lays层都是必需的),您可以看到这里使用了函数f:A => B-在这种情况下,它只是case类的构造函数NamesImpl



如您所见,“名称”需要zio环境中的“随机”才能起作用。



这是一个测试:



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


它用于从环境中ZIO.accessM提取名称_.get 检索服务。



我们为测试提供以下名称



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


provideCustomLayer将“ 名称”层添加到现有环境。



队伍



团队(团队) 的本质是测试我们创建的模块之间的依赖关系。



 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)

  }


团队将从可用名称中按大小选择一个团队



按照模块使用模式,尽管pickTeam需要使用Names才能工作,但我们没有将其放在ZIO [Names,Nothing,Set [String]]中 -而是在其中保留对其的引用TeamsImpl



我们的第一个测试很简单。



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


要运行它,我们需要给它一个Teams层:



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


什么是“ >>>”?



这是一个垂直构图。这表明我们需要名称,而团队层也需要



但是,运行此程序时,存在一个小问题。



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


返回定义 NamesImpl



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


所以我们的NamesImpl被创建了两次。如果我们的服务包含一些独特的应用程序系统资源,会有什么风险?实际上,事实证明,问题根本不在“层”机制中-记住了层,并且在依赖图中没有多次创建层。这实际上是测试环境的产物。



让我们将测试套件更改为:



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


这解决了一个问题,这意味着该层在测试中仅创建一次,而



JustTeamsTest只需要teams但是,如果我想访问团队姓名怎么办?



 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
  }


为此,我们需要同时提供以下两项:



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


在这里,我们使用++组合器通过Teams创建Names注意运算符的优先级和多余的括号



(Names.live >>> Teams.live)


一开始,我本人很喜欢-否则编译器将无法正确执行。



历史



历史有点复杂。



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


构造函数HistoryImpl需要许多Names但是获得它的唯一方法是从Teams中撤出它它需要ZIO-因此我们使用ZLayer.fromServiceM它来满足我们的需求。

该测试以与之前相同的方式进行:



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


就这样。



抛出错误



上面的代码假定您正在返回ZLayer [R,Nothing,T]-换句话说,环境服务构造的类型为Nothing。但是,如果它执行的操作类似于从文件或数据库中读取数据,则很有可能是ZLayer [R,Throwable,T]-因为此类操作通常涉及导致异常的外部因素。因此,假设Names构造中存在错误。有一种方法可以解决此问题:



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


然后在测试结束时



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


mapError将对象throwable变成测试失败-这就是您想要的-可能表示测试文件不存在或类似的东西。



更多ZEnv案例



环境的“标准”元素包括“时钟”和“随机”。我们已经在名称中使用了Random。但是,如果我们还希望这些元素之一进一步“降低”我们的依赖关系呢?为此,我创建了History的第二个版本-History2-在这里需要Clock来创建实例。



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


这不是一个非常有用的示例,但重要的部分是该行



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


迫使我们在正确的位置提供时钟。



现在.provideCustomLayer可以将我们的图层添加到图层堆栈中,并神奇地将Random弹出到Names中。但这不会在History2中要求的以下时间内发生。因此,以下代码无法编译:



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


相反,您需要History2.live显式提供时钟,方法如下:



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


Clock.any是从上方获取任何可用时钟的函数。在这种情况下,它将是一个测试时钟,因为我们没有尝试使用Clock.live



资源



完整的源代码(不包括throwable)如下所示:



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


有关更多高级问题,请联系Discord#zio-users或访问zio 网站和文档






了解有关该课程的更多信息。







All Articles