تطبيق ZIO ZLayer

في يوليو ، تطلق OTUS دورة جديدة "Scala-developer" ، قمنا بإعداد ترجمة لمواد مفيدة لك.








تعد ميزة ZLayer الجديدة في ZIO 1.0.0-RC18 + تحسينًا كبيرًا على نمط الوحدة القديمة ، مما يجعل إضافة خدمات جديدة أسرع وأسهل. ومع ذلك ، من الناحية العملية ، وجدت أنه قد يستغرق بعض الوقت لإتقان هذه اللغة.



فيما يلي مثال توضيحي للنسخة النهائية من رمز الاختبار الخاص بي حيث ألقي نظرة على عدد من حالات الاستخدام. شكرًا جزيلاً لآدم فريزر لمساعدتي في تحسين عملي وتحسينه. يتم تبسيط الخدمات عن قصد ، لذلك نأمل أن تكون واضحة بما يكفي للقراءة بسرعة.



أفترض أن لديك فهمًا أساسيًا لاختبارات 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]


تجاهل الموسومة (هذا ضروري لجميع Has / Layers للعمل) ، يمكنك أن ترى أنه هنا يتم استخدام الوظيفة f: A => B - وهي في هذه الحالة مجرد مُنشئ لفئة الحالة لـ 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يضيف طبقة الأسماء إلى البيئة الموجودة.



فرق



جوهر الفرق (Teams) هو اختبار التبعيات بين الوحدات التي أنشأناها.



 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 يحتاج إلى أسماء للعمل ، إلا أننا لا نضعها في 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),


ما هو ">>>"؟



هذا تكوين عمودي. يشير إلى أننا بحاجة إلى طبقة الأسماء التي تحتاج إلى طبقة Teams .



ومع ذلك ، عند تشغيل هذا ، هناك مشكلة صغيرة.



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 and Names ؟



 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.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يتطلب المُنشئ العديد من الأسماء . لكن الطريقة الوحيدة للحصول عليها هي سحبها من 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] - لأن هذا النوع من الأشياء غالبًا ما ينطوي على العامل الخارجي الذي يسبب الاستثناء. لذا تخيل أن هناك خطأ في بنية الأسماء. هناك طريقة لكي تعمل اختباراتك حول هذا:



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


ثم في نهاية الاختبار



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


mapErrorيحول الكائن throwableإلى فشل اختبار - هذا ما تريده - قد يقول أن ملف الاختبار غير موجود أو شيء من هذا القبيل.



المزيد من حالات ZEnv



تتضمن العناصر "القياسية" للبيئة الساعة والعشوائية. لقد استخدمنا بالفعل Random في أسماءنا. ولكن ماذا لو أردنا أيضًا أن يؤدي أحد هذه العناصر إلى "تقليل" تبعياتنا؟ للقيام بذلك ، قمت بإنشاء نسخة ثانية من History - History2 - وهنا الساعة مطلوبة لإنشاء مثيل.



 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.



مصدر



يظهر رمز المصدر الكامل (باستثناء الرمي) أدناه:



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 أو زيارة موقع zio الإلكتروني والتوثيق .






تعلم المزيد عن الدورة.







All Articles