Penerapan ZIO ZLayer

Pada bulan Juli, OTUS meluncurkan kursus baru "Pengembang Scala" , sehubungan dengan itu kami telah menyiapkan terjemahan materi yang bermanfaat untuk Anda.








Fitur ZLayer baru di ZIO 1.0.0-RC18 + merupakan peningkatan signifikan pada pola modul lama, membuat menambahkan layanan baru lebih cepat dan lebih mudah. Namun, dalam praktiknya, saya merasa perlu waktu untuk menguasai idiom ini.



Di bawah ini adalah contoh versi terakhir dari kode pengujian saya yang dianotasi di mana saya melihat sejumlah kasus penggunaan. Terima kasih banyak kepada Adam Fraser karena membantu saya mengoptimalkan dan memperbaiki pekerjaan saya. Layanan ini sengaja disederhanakan, jadi semoga layanan tersebut cukup jelas untuk dibaca dengan cepat.



Saya berasumsi bahwa Anda memiliki pemahaman dasar tentang tes ZIO dan bahwa Anda terbiasa dengan informasi dasar mengenai modul.



Semua kode berjalan dalam tes zio dan merupakan satu file.



Inilah tipnya:



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


Nama



Jadi, kami sampai ke layanan pertama kami - Nama (Nama)



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


Semuanya di sini adalah dalam kerangka pola modular yang khas.



  • Nyatakan Nama sebagai alias tipe untuk Has
  • Di objek, tentukan Layanan sebagai sifat
  • Buat implementasi (tentu saja Anda dapat membuat beberapa),
  • Buat ZLayer di dalam objek untuk implementasi yang diberikan. Konvensi ZIO cenderung memanggil mereka secara langsung.
  • Obyek paket ditambahkan yang menyediakan cara pintas yang mudah diakses.


Dalam live itu digunakan ZLayer.fromServiceyang didefinisikan sebagai:



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


Mengabaikan Tagged (ini diperlukan agar semua Has / Layers berfungsi), Anda dapat melihat bahwa di sini fungsi f: A => B digunakan - yang dalam hal ini hanya merupakan konstruktor dari kelas kasus untuk NamesImpl.



Seperti yang Anda lihat, Nama membutuhkan Random dari lingkungan zio agar berfungsi.



Inilah tesnya:



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


Ini digunakan ZIO.accessMuntuk mengekstrak Nama dari lingkungan. _.get mengambil layanan.



Kami memberikan Nama untuk pengujian sebagai berikut:



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


provideCustomLayermenambahkan lapisan Nama ke lingkungan yang ada.



Tim



Inti dari Tim (Tim) adalah untuk menguji dependensi antar modul, yang telah kami buat.



 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)

  }


Tim akan memilih tim dari nama yang tersedia berdasarkan ukuran .



Mengikuti pola penggunaan modul, meskipun pickTeam membutuhkan Nama untuk berfungsi , kami tidak memasukkannya dalam ZIO [Nama, Tidak Ada, Set [String]] - sebagai gantinya, kami menyimpan referensi untuknya TeamsImpl.



Tes pertama kami sederhana.



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


Untuk menjalankannya, kita perlu memberinya layer Tim:



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


Apa itu ">>>"?



Ini adalah komposisi vertikal. Ini menunjukkan bahwa kita memerlukan lapisan Nama , yang membutuhkan lapisan Tim .



Namun, saat menjalankan ini, ada masalah kecil.



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


Kembali ke definisi NamesImpl



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


Jadi kita NamesImpldiciptakan dua kali. Apa risikonya jika layanan kami mengandung beberapa sumber daya sistem aplikasi yang unik? Bahkan, ternyata masalahnya tidak ada sama sekali dalam mekanisme Layers - lapisan diingat dan tidak dibuat beberapa kali dalam grafik dependensi. Ini sebenarnya merupakan artefak dari lingkungan pengujian.



Mari kita ubah suite pengujian kami menjadi:



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


Ini memperbaiki masalah, yang berarti bahwa layer dibuat hanya sekali dalam pengujian.



JustTeamsTest hanya membutuhkan tim . Tetapi bagaimana jika saya ingin mengakses Tim dan Nama ?



 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
  }


Agar ini berfungsi, kami harus menyediakan keduanya:



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


Di sini kita menggunakan combinator ++ untuk membuat lapisan Nama dengan Tim . Perhatikan prioritas operator dan kurung tambahan



(Names.live >>> Teams.live)


Pada awalnya, saya jatuh cinta sendiri - jika tidak kompiler tidak akan melakukannya dengan benar.



Sejarah



Sejarah sedikit lebih rumit.



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


Konstruktor HistoryImplmembutuhkan banyak Nama . Tetapi satu-satunya cara untuk mendapatkannya adalah dengan menariknya dari Tim . Dan itu membutuhkan ZIO - jadi kami menggunakannya ZLayer.fromServiceMuntuk memberikan apa yang kami butuhkan.

Tes dilakukan dengan cara yang sama seperti sebelumnya:



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


Dan itu saja.



Kesalahan yang bisa dilempar



Kode di atas mengasumsikan bahwa Anda mengembalikan ZLayer [R, Nothing, T] - dengan kata lain, konstruk layanan lingkungan adalah tipe Nothing. Tetapi jika ia melakukan sesuatu seperti membaca dari file atau database, maka kemungkinan besar akan menjadi ZLayer [R, Throwable, T] - karena hal semacam ini sering melibatkan faktor yang sangat eksternal yang menyebabkan pengecualian. Jadi bayangkan ada kesalahan dalam konstruksi Nama. Ada cara bagi tes Anda untuk mengatasi ini:



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


lalu di akhir tes



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


mapErrormengubah objek throwablemenjadi kegagalan pengujian - itu yang Anda inginkan - mungkin mengatakan file pengujian tidak ada atau sesuatu seperti itu.



Lebih banyak kasus ZEnv



Unsur "standar" lingkungan termasuk Jam dan Acak. Kami telah menggunakan Acak di Nama kami. Tetapi bagaimana jika kita juga ingin salah satu dari unsur-unsur ini untuk lebih "menurunkan" ketergantungan kita? Untuk melakukan ini, saya membuat versi kedua History - History2 - dan di sini Clock diperlukan untuk membuat 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)
    }
    
  }


Ini bukan contoh yang sangat berguna, tetapi bagian yang penting adalah garis itu



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


memaksa kita untuk menyediakan jam di tempat yang tepat.



Sekarang .provideCustomLayerdapat menambahkan lapisan kita ke tumpukan lapisan dan secara ajaib muncul Acak ke Nama. Tapi ini tidak akan terjadi selama jam yang diperlukan di bawah di History2. Karenanya, kode berikut TIDAK mengkompilasi:



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


Sebagai gantinya, Anda perlu memberikan History2.livejam secara eksplisit, yang dilakukan sebagai berikut:



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


Clock.anyMerupakan fungsi yang mendapatkan jam apa pun yang tersedia dari atas. Dalam hal ini, ini akan menjadi jam tes, karena kami tidak mencoba menggunakannya Clock.live.



Sumber



Kode sumber lengkap (tidak termasuk yang bisa dibuang) ditunjukkan di bawah ini:



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


Untuk pertanyaan lebih lanjut, silakan hubungi Discord # zio-pengguna atau kunjungi situs web dan dokumentasi zio.






Pelajari lebih lanjut tentang kursus.







All Articles