[Scala, Программирование] Применение ZIO ZLayer (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Июл-2020 19:30

В июле OTUS запускает новый курс «Scala-разработчик», в связи с чем мы подготовили для вас перевод полезного материала.
Новая функция 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")

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

Тут все в рамках типичного модульного паттерна.
  • Объявите Names как псевдоним типа для Has
  • В объекте, определите Service как трейт
  • Создайте реализацию (конечно, вы можете создать несколько),
  • Создайте ZLayer внутри объекта для данной реализации. Конвенция ZIO имеет тенденцию вызывать их в реальном времени.
  • Добавляется объект пакета, который обеспечивает удобное для доступа сокращение.

В live используется ZLayer.fromService, который определяется как:
def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]

Игнорируя Tagged (это необходимо для работы всего Has/Layers), вы можете видеть, что здесь используется функция f: A => B — которая в данном случае является просто конструктором кейс класса для NamesImpl.
Как вы можете видеть, для работы Names требуется Random из среды zio.
Вот тест:
def namesTest = testM("names test") {
    for {
      name <- names.randomName
    }  yield {
      assert(firstNames.contains(name))(equalTo(true))
    }
  }

Он использует ZIO.accessM для извлечения Names из среды. _.get извлекает сервис.
Мы предоставляем Names для теста следующим образом:
suite("needs Names")(
       namesTest
    ).provideCustomLayer(Names.live),

provideCustomLayer добавляет слой Names в существующую среду.
Teams
Суть 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)
  }

Teams выберут команду из доступных names, сделав выбор по размеру.
Следуя паттернам использования модулей, хотя для работы 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),

Что такое «>>>»?
Это вертикальная композиция. Она указывает, что нам нужен слой Names, которому нужен слой 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 создается дважды. Чем это чревато, если наш сервис содержит какой-нибудь уникальный системный ресурс приложения? На самом деле, оказывается, что проблема вовсе не в механизме Layers — слои запоминаются и не создаются несколько раз в графе зависимостей. На самом деле это артефакт тестовой среды.
Изменим наш набор тестов на:
suite("needs just Team")(
      justTeamsTest
    ).provideCustomLayerShared(Names.live >>> Teams.live),

Это исправляет проблему, что означает, что слой создается в тесте только один раз
JustTeamsTest требует только teams. Но что, если я хотел получить доступ к Teams и 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)),

Здесь мы используем комбинатор ++ для создания слоя Names с Teams. Обратите внимание на приоритет оператора и дополнительные скобки
(Names.live >>> Teams.live)

Вначале, я сам на это попался — в противном случае компилятор будет делать не правильно.
History
History (История) немного сложнее.
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))

И все.
Throwable ошибки
В приведенном выше коде предполагается, что вы возвращаете 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. В наших Names мы уже использовали 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.
Узнать о курсе подробнее.
===========
Источник:
habr.com
===========

===========
Автор оригинала: https://timpigden.github.io/
===========
Похожие новости: Теги для поиска: #_scala, #_programmirovanie (Программирование), #_zio, #_zlayer, #_scala, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
)
, #_scala, #_programmirovanie (
Программирование
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 21-Ноя 19:33
Часовой пояс: UTC + 5