
水平和垂直构图
简介
根据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的不同功能创建复杂的应用程序。由于每次对层的组成都有不同的需求,所以首先对所有横向组成的层进行组成是非常有益的。