Async Builder + Provider API Case Study 中文翻译
这个案例展示了 AWS SDK 中 builder 的常见的 api 模式
目前的 API
AWS SDK 中的一些 builder 遵循 “异步 provider” 的模型,即 builder 实现 trait 的某种返回 Future 方法去自定义行为。
let credentials = DefaultCredentialsChain::builder()
// Provide an `impl ProvideCredentials`
.with_custom_credential_source(MyCredentialsProvider)
.build()
.await;
在这个案例中,用户可以为 DefaultCredentialsChain
添加自定义凭证源,而且源可以在被凭证链调用的时候干一些异步的工作。Builder 方法 with_custom_credential_source
接受实现了 ProvideCredentials
trait 的对象
pub trait ProvideCredentials: Send + Sync + Debug {
fn provide_credentials(&self) -> ProvideCredentials<'_>;
}
但是现在的 ProvideCredentials
trait 有点尴尬,它期望实现对象返回一个 ProvideCredentials<'_>
结构体,这个结构体就像一个可以产生 Result<Credentials, CredentialsError>
的 Box 的结构体
struct MyCredentialsProvider;
// Implementations return `ProvideCredentials<'_>`, which is basically a boxed
// `impl Future<Output = Result<Credentials, CredentialsError>>`.
impl ProvideCredentials for MyCredentialsProvider {
fn provide_credentials(&self) -> ProvideCredentials<'_> {
ProvideCredentials::new(async move {
/* Make some credentials */
})
}
}
在这样的想法下,当 builder 的 with_custom_credential_source
被调用时,他把实现了 ProvideCredentials 的对象扔进 Box 并且把它存在要构建的 DefaultCredentialsChain
以便使用。
使用 Async Function In Trait(AFIT)
既然 ProvideCredentials
返回 impl Future
,有了 AFIT,ProvideCredentials
可以被简化为这样
trait ProvideCredentials {
async fn provide_credentials(&self) -> Result<Credentials, CredentialsError>;
}
用户可以提供这个 trait 的实现,省去了将函数主题用 ProvideCredentials::new(async { ... })
包裹的多余步骤
而且 builder 调用不变
let credentials = DefaultCredentialsChain::builder()
// Provide an `impl ProvideCredentials`
.with_custom_credential_source(MyCredentialsProvider)
.build()
.await;
动态分发:在 API 背后
为了使 builder 支持这些变动,我们需要封装新的 ProvideCredentials
trait 的实例。
如果没有 AFIDT("async functions in dyn trait",允许有 async fn 方法的 trait 变得对象安全,我们就不能像以前在 with_custom_credential_source
里一样简单的封装 impl ProvideCredentials
。
幸运的是,我们可以使用一些类型擦除技巧来解决失去了 AFIDT 的问题。我们要引入一个所有实现 ProvideCredentials
的对象都没有实现任何方法的新 trait,在这里称为 ProvideCredentialsDyn
。
trait ProvideCredentialsDyn {
fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + '_>>;
}
impl<T: ProvideCredentials> ProvideCredentialsDyn for T {
fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + '_>> {
Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
}
}
这个新 trait 是对象安全的,并且可以被扔进 Box 里存储在 builder 中,而不是直接存原 trait。
struct DefaultCredentialsChain {
credentials_source: Box<dyn ProvideCredentialsDyn>,
// ...
}
impl DefaultCredentialsChain {
fn with_custom_credential_source(self, provider: impl ProvideCredentials) {
// Coerce `impl ProvideCredentials` to `Box<dyn ProvideCredentialsDyn>`
Self { provider: Box::new(credentials_source), ..self }
}
}
这个额外的 trait 是实现的细节,不对外公开,在 AFIDT 引入时就可以删了。
完整的 builder 模式样例实现在这儿: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=8daf7b2d5236e581f78d2c09310d09ac
Send 约束
提议的异步版本 ProvideCredentials 的一项限制就是返回的 future 缺少 Send
约束。这个约束是 pre-AFIT 版本强制的(?),因此在迁移到 AFIT 后使用 builder 的任意 futures 将不会有 Send 特性。
为了解决这个问题,我们可以在 builder 的 with_custom_credential_source
方法上使返回类型约束(https://smallcultfollowing.com/babysteps/blog/2023/02/13/return-type-notation-send-bounds-part-2/)。
impl DefaultCredentialsChain {
fn with_custom_credential_source(
self,
provider: impl ProvideCredentials<provide_credentials(): Send>
) {
// Coerce `impl ProvideCredentials` to `Box<dyn ProvideCredentialsDyn>`
Self { provider: Box::new(credentials_source), ..self }
}
}
接下来 ProvideCredentialsDyn
就可以改成返回 Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>>
trait ProvideCredentialsDyn {
fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>>;
}
impl<T: ProvideCredentials<provide_credentials(): Send>> ProvideCredentialsDyn for T {
fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>> {
Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
}
}
相等的替代方法可能是类似于加上 T: async(Send) ProvideCredentials
的约束,比如
impl<T: async(Send) ProvideCredentials> ProvideCredentialsDyn for T {
fn provide_credentials(&self) -> Pin<Box<dyn Future<Output = Result<Credentials, CredentialsError>> + Send + '_>> {
Box::pin(<Self as ProvideCredentials>::provide_credentials(self))
}
}
用法
该 SDK 使用了很多次这些习语
ProvideCredentials
: https://docs.rs/aws-credential-types/0.54.1/aws_credential_types/provider/trait.ProvideCredentials.htmlAsyncSleep
: https://docs.rs/aws-smithy-async/0.54.3/aws_smithy_async/rt/sleep/trait.AsyncSleep.htmlProvideRegion
: https://docs.rs/aws-config/0.54.1/aws_config/meta/region/trait.ProvideRegion.html
未来的优化
有了 AFIDT,我们可以丢掉 ProvideCredentialsDyn
并像原来一样使用 Box<dyn ProvideCredentials>
。重构 API 来使用 AFIDT 只会涉及内部修改。