ZIO ZLayerのアプリケーション

7月に、OTUSは新しいコース「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")


お名前



それで、最初のサービス-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)
  }


ここにあるすべては、典型的なモジュラーパターンのフレームワーク内にあります。



  • 名前をHasの型エイリアスとして宣言する
  • オブジェクトで、サービスを特性として定義します
  • 実装を作成します(もちろん、複数作成できます)。
  • 指定された実装のオブジェクト内にZLayerを作成します。ZIO規約では、それらをリアルタイムで呼び出す傾向があります。
  • アクセスしやすいショートカットを提供するパッケージオブジェクトが追加されます。


ライブはされ使用ZLayer.fromServiceのように定義されています。



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


タグ付けを無視すると(これはすべてのHas / Layerが機能するために必要です)、ここでは関数f:A => Bが使用されていることがわかります。この場合、これはのケースクラスのコンストラクターにすぎませんNamesImpl



ご覧のように、Namesが動作するには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),


provideCustomLayerNamesレイヤーを既存の環境に追加します。



チーム



チーム(チーム) の本質は、私たちが作成したモジュール間の依存関係をテストすることです。



 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は2回作成されます。サービスに独自のアプリケーションシステムリソースが含まれている場合、どのようなリスクがありますか?実際、問題はLayersメカニズムにはまったくないことがわかりました-レイヤーは記憶され、ディペンデンシーグラフで何度も作成されていません。これは実際にはテスト環境の成果物です。



テストスイートを次のように変更します。



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


これは修正層は一度だけテスト中に作成されることを意味している。問題は、



JustTeamsTestが唯一必要とチームをしかし、チーム名前にアクセスしたい場合はどうなりますか?



 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が必要です。しかし、それを取得する唯一の方法は、チームから引き出すことです。そして、それは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ケース



環境の「標準」要素には、ClockとRandomが含まれます。名前にはすでにランダムを使用しています。しかし、これらの要素の1つに依存関係をさらに「低下」させたい場合はどうでしょうか。これを行うには、履歴の2番目のバージョンである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、レイヤーをレイヤースタックに追加でき、魔法のようにランダムに名前が表示されます。しかし、これは以下の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のWebサイトおよびドキュメントにアクセスしてください






コースの詳細をご覧ください。







All Articles