Async Builder + Provider API Case Study 中文翻译

49

这个案例展示了 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 使用了很多次这些习语

未来的优化

有了 AFIDT,我们可以丢掉 ProvideCredentialsDyn 并像原来一样使用 Box<dyn ProvideCredentials>。重构 API 来使用 AFIDT 只会涉及内部修改。