如何在Zio中编排图层?

236 阅读3分钟

水平和垂直构图

简介

根据ZIO文档,我们知道ZIO是一个用于异步和并发编程的库,提倡纯函数式编程。

ZIO库的核心提供了一个强大的效果类型ZIO,其灵感来自于Haskell的IO monad。这个类型的值本身并不做任何事情。它只是对应该做的事情的描述。ZIO运行时系统负责实际做描述的事情。

ZIO[-R, +E, +A] datatype有3个类型参数。

  • R - 解释效果所需的环境类型
  • E - 错误类型
  • A--返回类型

这种数据类型也可以用R => Either[E, A]的形式表示。 ZIO允许我们定义模块,并使用它们来创建相互依赖的不同应用层。一个模块是一组只处理一个问题的函数。

ZIO遵循模块模式,其中一层依赖于紧随其后的各层所暴露的接口。这意味着每一层都依赖于紧随其后的层,但它对它们的实现细节一无所知。

在这篇博客中,我们将了解到我们如何有效地使用不同的方式来组成这些层。

依赖库

将给定的依赖关系添加到你的build.sbt.

libraryDependencies += "dev.zio" %% "zio" % "1.0.12

ZLayer

ZLayer数据类型是一个不可变的值,它包含一个描述,
从一个值RIn开始,
建立一个
ROut类型的环境,在创建过程中可能产生一个错误E。

ZLayer[-RIn, +E, +ROut <: Has[_]]

特质Has[A] 与ZIO环境一起使用,以表达一个效果对类型A 的服务的依赖性。

例如,RIO[Has[Console.Service], Unit] 是一个需要Console.Service 服务的效果。

我们可以从简单值、管理资源、ZIO效果、其他服务中创建ZLayer。

ZLayer.success: 允许从一个服务创建ZLayer。当你想定义一个ZLayer时,这很有用,它的创建不依赖于任何东西,也不会失败。

ZLayer.fail。 这允许建立一个总是不能建立输出的ZLayer。

ZLayer.fromEffect。 允许将一个ZIO效果提升到一个ZLayer。当你想定义一个ZLayer的创建取决于环境和/或可能失败时,这特别方便。你也可以在ZIO数据类型中使用相应的操作符。ZIO#toLayer

ZLayer.fromFunction。 允许从一个输入为环境、输出为服务的函数中创建ZLayer。当你想定义一个ZLayer时,你可以使用它,它的创建取决于环境,但不能失败。

ZLayer.fromManaged。 允许将一个ZManaged效果提升到一个ZLayer。当你想定义一个ZLayer,其创建依赖于环境和/或可能失败,以及你想获得额外的资源安全时,这就适用。你也可以使用ZManaged数据类型中的等效操作。ZManaged#toLayer

ZLayer.fromAcquireRelease。 这与ZLayer.fromManaged非常相似**,**但它期望有一个ZIO效果和一个释放函数来代替。

ZLayer.fromService。 允许从一个函数中创建一个ZLayer,该函数的输入是一个服务,输出是另一个服务。当你想定义一个ZLayer的创建依赖于另一个服务但不能失败时,这很有用。

让我们创建两个服务:Logger和Database。

记录器服务

object LoggerService {
  type Logger = Has[Logger.Service]

  object Logger {
    trait Service {
      def log(line: String): UIO[Unit]
    }

    val any: ZLayer[Logger, Nothing, Logger] =
      ZLayer.requires[Logger]

    val live: Layer[Nothing, Has[Service]] = ZLayer.succeed {
      (line: String) => {
        putStrLn(line).provideLayer(Console.live).orDie
      }
    }
  }
  def log(line: => String): ZIO[Logger, Throwable, Unit] =
    ZIO.accessM(_.get.log(line))
}

数据库服务

case class User(id: String,name: String)

object DatabaseService {
  type Database = Has[Database.Service]

  object Database {
    trait Service {
      def getUser(id: String): Task[User]
    }

    val any: ZLayer[Database, Nothing, Database] =
      ZLayer.requires[Database]

    val live: Layer[Nothing, Has[Service]] = ZLayer.succeed {
      (id: String) => {
        Task(User(id,"Akash"))
      }
    }
  }
  def getUser(id: => String): ZIO[Database, Throwable, User] =
    ZIO.accessM(_.get.getUser(id))
}

记录器服务将数据打印到控制台,数据库服务返回用户。现在我们使用这些服务来使用其他服务用户。

这个用户服务依赖于DatabaseService和LoggerService来获取和打印用户数据。

用户服务

object UserRepo {
  type Users = Has[Users.Service]

  def getUser(id: => String): ZIO[Users, Throwable, Unit] =
    ZIO.accessM(_.get.getUser(id))

  object Users {
    val any: ZLayer[Users, Nothing, Users] =
      ZLayer.requires[Users]
    val live: ZLayer[Has[Database.Service] with Has[Logger.Service], Nothing, Has[Service]] =
      ZLayer.fromServices[Database.Service, Logger.Service, Service] { (database, logger) =>
        new Service {
          override def getUser(id: String): Task[Unit] = for {
            user <- database.getUser(id)
            _ <- logger.log(s"Hello $user")
          } yield ()
        }
      }

    trait Service {
      def getUser(id: String): Task[Unit]
    }
  }
}

层的组成

现在我们首先知道组成层的方法。ZLayer,可以在水平或垂直方向上组成。

横向组成

它们可以通过++ 操作符在水平方向上组合在一起。当我们对两个层进行水平组合时,这个层需要所有它们都需要的服务,同时这个层也产生所有它们都产生的服务。

水平组合是一种将两个层并排组合的方式。当我们把两个相互之间没有任何关系的层结合起来时,这种方式很有用。

换句话说,如果我们有两个层ZLayer[RIn1, E1, ROut1]和ZLayer[RIn1, E1, ROut1],那么我们可以得到一个更大的ZLayer ,它可以接受输入RIn1 with RIn2 ,并产生输出ROut1 with ROut2

在我们的用例中,我们可以横向组合数据库和记录器,因为它们没有依赖关系,可以产生一个强大的层,将Has[Database.Service]和Has[Logger.Service]结合起来。

 val horizontalComposeLayer: ZLayer[Any, Nothing, Has[Database.Service] with Has[Logger.Service]] = Database.live ++ Logger.live
  

垂直组合

当我们有一个需要A ,并产生B 的层时,我们可以将这个层与另一个需要B ,并产生C 的层进行组合;这个组合产生一个需要A ,并产生C 的层。进位运算符,>>> ,通过使用垂直组合将它们堆叠在一起。

这种组合就像函数组合,把一个层的输出喂给另一个层的输入。

在我们的案例中,我们的用户服务同时依赖于记录器服务和数据库服务。如上所述,我们对数据库和记录器层进行了水平组合,所以现在我们可以使用这个 horizontalcomposeLayer进行垂直组合。

val combinedLayer: ZLayer[Any, Nothing, Has[Users.Service]] = horizontalComposeLayer >>> Users.live

我们使用ZIO的输出**horizontalcomposeLayer**的输出作为ZIO的输入。**Users.live**.因此,我们得到一个单一的**ZLayer**它包含了对.的实现。**Users.Service**,以及创建/传递的**Database.Service**Logger.Service ,这是因为构建了**horizontalcomposeLayer**(它包含两者的实现)和>>> 操作符,然后调用**ZLayer.fromService** s的回调。

运行应用程序

我们可以通过覆盖zio.App trait的run方法来运行这个应用程序,它提供了所有需要的环境。

object Main extends zio.App {

  val program: ZIO[Users, Throwable, Unit] = for {
    id <- ZIO(UUID.randomUUID().toString)
    _ <- UserRepo.getUser(id)
  } yield ()

  val horizontalComposeLayer: ZLayer[Any, Nothing, Has[Database.Service] with Has[Logger.Service]] = Database.live ++ Logger.live

  val combinedLayer: ZLayer[Any, Nothing, Has[Users.Service]] = horizontalComposeLayer >>> Users.live

  override def run(args: List[String]): URIO[ZEnv, ExitCode] = program.provideSomeLayer(combinedLayer).exitCode
}

总结

我们学习了如何在ZIO中组合层,利用ZIO的不同功能创建复杂的应用程序。由于每次对层的组成都有不同的需求,所以首先对所有横向组成的层进行组成是非常有益的。