「这是我参与2022首次更文挑战的第 8 天,活动详情查看:2022首次更文挑战」。
面向控制流
上面我们达到了我们想要的结果(获取有用的日志),但不太喜欢这个解决方案:我们的Web框架中为一个操作:store_token,此操作返回的错误类型实现了一个trait(ResponseError),而这个trait对REST或HTTP协议无感知。我们可以从一个不同的入口(例如CLI)调用 store_token。它的实现不应该有任何改变。
即使假设我们只是在REST API的上下文中调用 store_token,我们也可能添加其他依赖该函数的点 —— 它们可能不希望在失败时返回500。
当错误发生时,请求处理程序应当选择适当的HTTP状态代码返回,它不应该在其他地方泄露。
为了实现正确的关注点分离,我们需要引入另一种错误类型,即 SubscribeError。我们将使用它作为 subscribe() 的错误返回类型,它将拥有HTTP相关的逻辑(实现 ResponseError)。
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
}
#[derive(Debug)]
struct SubscribeError {}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Failed to create a new subscriber."
)
}
}
impl std::error::Error for SubscriberError {}
impl ResponseError for SubscribeError {}
如果你运行 cargo check,你会看到雪崩式的 ? 无法将错误转换为 SubscribeError。我们需要由我们业务函数返回的错误和SubscribeError返回的错误类型之间的转换。
枚举错误
枚举是解决这个问题的最常见的方法:我们需要处理的每种错误类型提供一个变量。
//! src/routes/subscriptions.rs
// [...]
#[derive(Debug)]
pub enum SubscribeError {
ValidationError(String),
DatabaseError(sqlx::Error),
StoreTokenError(StoreTokenError),
SendEmailError(reqwest::Error),
}
然后,我们可以在我们的处理程序中利用 ? 操作符,为每个包裹的错误类型提供 From 实现:
//! src/routes/subscriptions.rs
// [...]
impl From<reqwest::Error> for SubscribeError {
fn from(e: reqwest::Error) -> Self {
Self::SendEmailError(e)
}
}
impl From<sqlx::Error> for SubscribeError {
fn from(e: sqlx::Error) -> Self {
Self::DatabaseError(e)
}
}
impl From<StoreTokenError> for SubscribeError {
fn from(e: StoreTokenError) -> Self {
Self::StoreTokenError(e)
}
}
impl From<String> for SubscribeError {
fn from(e: String) -> Self {
Self::ValidationError(e)
}
}
现在我们可以通过删除所有这些 match/if fallible_function().is_err() 来清理我们的请求处理程序:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
let new_subscriber = form.0.try_into()?;
let mut transaction = pool.begin().await?;
let subscriber_id = insert_subscriber(/* */).await?;
let subscription_token = generate_subscription_token();
store_token(/* */).await?;
transaction.commit().await?;
send_confirmation_email(/* */).await?;
Ok(HttpResponse::Ok().finish())
}
代码可以编译,但是我们的一个测试失败了:
thread 'subscriptions::subscribe_returns_a_400_when_fields_are_present_but_invalid'
panicked at 'assertion failed: `(left == right)`
left: `400`,
right: `500`: The API did not return a 400 Bad Request when the payload was empty name.'
我们仍然在使用 ResponseError 的默认实现 —— 它总是返回500。 这就是枚举的闪光点:我们可以使用控制流的匹配语句 → 根据我们所处理的故障情况,我们的行为是不同的。
//! src/routes/subscriptions.rs
use actix_web::http::StatusCode;
// [...]
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::DatabaseError(_)
| SubscribeError::StoreTokenError(_)
| SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
测试应该再次通过。