我正在设计一个 ZIO 服务。我必须在其内部逻辑中使用记忆化。我怎样才能做到这一点? 例如,如果服务的一个方法(或所有方法)需要远程主机的某种授权,我如何确保授权过程只执行一次?
trait ServiceA {
def doSomething(question: String): Task[String]
}
class RemoteServiceA(username: String, password: String, httpClient: HttpClient) extends ServiceA {
private val authorize: Task[Cookie] = for {
req <- ZIO.attempt {
HttpRequestBuilder()
.setUri("https://.../auth/")
.setUsername(username)
.setPassword(password)
.build()
}
resp <- httpClient.sendRequest(req)
} yield resp.getCookie
override def doSomething(question: String): Task[String] = for {
authCookie <- authorize
req = HttpRequestBuilder().setUri(s"https://.../do-something?q=$question")
answer <- httpClient.sendRequest(req)
} yield answer
}
现在据我了解,我不能在这里简单地使用
authorize.memoize
,它不会那样工作。我必须将授权例程移到 RemoteServiceA
类之外,并传递 memoizedAuth: Task[Cookie]
作为构造函数参数。但在我看来,这破坏了代码封装原则,因为授权过程是服务本身的内部逻辑。如果我有多个私有方法返回值,我想(懒惰地)计算一次并记住这些值以供(潜在的)将来使用,事情会变得更加丑陋。构造函数因 memoizedSomething: Task[Something]
和 memoizedSomethingElse: Task[SomethingElse]
而变得臃肿,并且许多内部类逻辑必须移到类外部到其他地方。
针对这种场景有更好的方法吗?
出于同样的原因,我也发现很难使用
memoize
。在 https://day-to-day-stuff.blogspot.com/2022/11/speed-up-zios-with-memoization.html 中,您可以找到一个在 Map
中使用
Ref
的记忆实现.
Ref[Map]
仍然需要在服务本身之外构建。这就是 ZLayer
存在的原因,它们抽象了服务的生命周期。它允许在 ZIO 上下文中进行构建和销毁。
简而言之,首先你需要缓存辅助方法:
import zio._
object ZioCaching {
implicit class ZioCachedBy[R, E, A](zio: ZIO[R, E, A]) {
def cachedBy[B](cacheRef: Ref[Map[B, Promise[E, A]]], key: B): ZIO[R, E, A] = {
for {
newPromise <- Promise.make[E, A]
actualPromise <- cacheRef.modify { cache =>
cache.get(key) match {
case Some(existingPromise) => (existingPromise, cache)
case None => (newPromise, cache + (key -> newPromise))
}
}
_ <- ZIO.when(actualPromise eq newPromise) {
zio.intoPromise(newPromise)
}
value <- actualPromise.await
} yield value
}
}
}
现在我们可以创建构建服务的层:
trait ServiceA {
def doSomething(question: String): Task[String]
}
object ServiceA {
val layer: ZLayer[HttpClient, Nothing, ServiceA] = ZLayer {
for {
httpClient <- ZIO.service[HttpClient]
username <- ...get from config?
password <- ...get from config?
cache <- Ref.make(Map.empty[String, Cookie])
} yield new RemoteServiceA(username, password, httpClient, cache)
}
}
class RemoteServiceA(username: String, password: String, httpClient: HttpClient, cache: Ref[Map[String, Cookie]]) extends ServiceA {
private val authorize: Task[Cookie] = ... as before ...
override def doSomething(question: String): Task[String] = for {
authCookie <- authorize.cachedBy(cache, username) // <-- memoization
req = HttpRequestBuilder().setUri(s"https://.../do-something?q=$question")
answer <- httpClient.sendRequest(req)
} yield answer
}