构建一个rust生产应用读书笔记6-拒绝无效订阅者02

打破域子模块

通常指的是对应用程序的某个特定业务领域进行重构或重新组织。这可能包括拆分、合并或重组代码结构以更好地反映业务规则和逻辑。下面是一些关于如何处理这种情况的建议:

1. 理解当前状态

首先,确保你完全理解现有系统的工作方式。这包括:

  • 阅读文档:如果有任何现有的文档,请先阅读。
  • 代码审查:深入研究代码库,了解各个部分的功能和相互之间的关系。
  • 与团队沟通:与熟悉系统的同事讨论,获取他们的见解和经验。

2. 定义边界上下文

根据DDD的原则,定义清晰的边界上下文(Bounded Contexts)。每个上下文应该封装一个独立的业务领域,并且有明确的接口与其他上下文交互。这样可以帮助保持各部分的分离,减少耦合。

3. 识别核心域

确定哪些部分是你的应用的核心域(Core Domain),即那些最能为业务提供价值的部分。核心域应该得到更多的关注和资源投入,非核心域则可以考虑外包或者使用现成解决方案。

4. 设计新结构

基于上述分析,设计新的子模块结构。考虑以下几点:

  • 职责单一原则:每个子模块应该有一个明确的目的或责任。
  • 高内聚低耦合:确保子模块内部紧密协作,而不同子模块之间尽量松散耦合。
  • 可维护性和扩展性:构建易于理解和维护的架构,同时考虑到未来的扩展需求。

5. 实施重构

逐步实施重构计划。遵循敏捷开发实践,小步快跑,每次只做一小部分改动,并通过自动化测试保证质量。关键步骤包括:

  • 编写测试:确保有足够的单元测试和集成测试覆盖将要更改的部分。
  • 持续集成/部署:利用CI/CD工具来自动执行构建、测试和部署流程。
  • 版本控制:使用Git等版本控制系统管理代码变更历史。

6. 持续改进

重构不是一次性任务,而是持续的过程。随着业务的发展和技术的进步,不断评估并优化你的域模型和代码结构。

重新组织代码结构

新建domain目录,分别在下面创建mod.rs,new_subscriber.rs,subscriber_email.rs,subscriber_name.rs文件

sub domain

mod.rs代码

rust 复制代码
//! src/domain/mod.rs 
mod new_subscriber;
mod subscriber_email;
mod subscriber_name;

pub use new_subscriber::NewSubscriber;
pub use subscriber_email::SubscriberEmail;
pub use subscriber_name::subscriber_name;

subscriber_name.rs 代码

从domain.rs 里面把SubscriberName抽取到subscriber_name.rs

rust 复制代码
#[derive(Debug)]
pub struct SubscriberName(String);

impl SubscriberName {
    pub fn parse(s: String) -> Result<SubscriberName, String> {
        let is_empty_or_whitespace = s.trim().is_empty();

        let is_too_long = s.graphemes(true).count() > 256;

        let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
        let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));

        if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
            Err(format!("{} is not a valid subscriber name.", s))
        } else {
            Ok(Self(s))
        }
    }
}

属性测试

属性测试(Property-based Testing)是一种测试方法,它允许我们基于一组输入的特性来验证代码的行为,而不是针对特定的输入和输出进行测试。这种方法可以显著增加我们对代码正确性的信心,因为它通常会测试比手工选择的测试用例更广泛的输入范围。

接下来我们将讨论如何为 SubscriberEmail 实现属性测试。假设 SubscriberEmail 是一个用于表示订阅者电子邮件地址的数据结构或类型,并且我们希望确保我们的解析器不会拒绝任何有效的电子邮件地址。

rust 复制代码
//! src/domain/subscriber_email.rs
use validator::ValidateEmail;

#[derive(Debug)]
pub struct SubscriberEmail(String);

impl SubscriberEmail {
    pub fn parse(s: String) -> Result<SubscriberEmail, String> {
        if s.validate_email() {
            Ok(Self(s))
        } else {
            Err(format!("{} is not a valid subscriber email.", s))
        }
    }
}

impl AsRef<str> for SubscriberEmail {
    fn as_ref(&self) -> &str {
        &self.0
    }
}
#[cfg(test)]
mod tests {
    use crate::domain::subscriber_email::SubscriberEmail;
    use claims::assert_err;
    use fake::{faker::internet::en::SafeEmail, Fake};
    use rand::{rngs::StdRng, SeedableRng};

    #[test]
    fn empty_string_is_rejected() {
        let email = "".to_string();
        assert_err!(SubscriberEmail::parse(email));
    }

    #[test]
    fn email_missing_at_symbol_is_rejected() {
        let email = "cokerlk.com".to_string();
        assert_err!(SubscriberEmail::parse(email));
    }

    #[test]
    fn email_missing_subject_is_rejected() {
        let email = "@domain.com".to_string();
        assert_err!(SubscriberEmail::parse(email));
    }

    #[derive(Debug, Clone)]
    struct ValidEmailFixture(pub String);

    impl quickcheck::Arbitrary for ValidEmailFixture {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            let mut rng = StdRng::seed_from_u64(u64::arbitrary(g));
            let email = SafeEmail().fake_with_rng(&mut rng);
            Self(email)
        }
    }

    #[quickcheck_macros::quickcheck]
    fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
        SubscriberEmail::parse(valid_email.0.clone()).is_ok()
    }
}
  • parse 方法接受一个字符串参数 s,并尝试将其解析成 SubscriberEmail 类型。
  • 如果字符串通过了 validate_email() 检查(假设这是 validator crate 提供的功能),则返回包含该字符串的新 SubscriberEmail 实例。
  • 如果字符串无效,则返回一个错误信息。
  • 为了使 SubscriberEmail 可以像普通字符串一样被引用,实现了 AsRef<str> trait。
  • 这样可以更方便地将 SubscriberEmail 传递给那些期望 &str 参数的函数或方法。

属性测试代码解释

  • ValidEmailFixture 是一个辅助结构体,用来携带由 SafeEmail 生成器创建的有效电子邮件地址。
  • 实现了 quickcheck::Arbitrary trait,使得 ValidEmailFixture 可以与 quickcheck 库一起使用,以生成随机但有效的电子邮件地址进行测试。
  • valid_emails_are_parsed_successfully 函数是一个属性测试,它检查所有由 quickcheck 生成的有效电子邮件地址是否都能成功被 parse 方法解析。

subscriptions.rs 代码修改

rust 复制代码
use core::result::Result::{Err, Ok};

use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use actix_web::{web, HttpResponse};
use chrono::Utc;
use sqlx::PgPool;
use uuid::Uuid;

#[derive(serde::Deserialize)]
pub struct FormData {
    email: String,
    name: String,
}

impl TryFrom<FormData> for NewSubscriber {
    type Error = String;
    fn try_from(value: FormData) -> Result<Self, Self::Error> {
        let name = SubscriberName::parse(value.name)?;
        let email = SubscriberEmail::parse(value.email)?;
        Ok(Self { email, name })
    }
}
#[allow(clippy::async_yields_async)]
#[tracing::instrument(name = "Adding a new subscriber",
    skip(form, pool),
    fields(subscriber_email=%form.email,subscriber_name=%form.name))]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
    let new_subscriber = match form.0.try_into() {
        Ok(form) => form,
        Err(_) => return HttpResponse::BadRequest().finish(),
    };
    match insert_subscriber(&pool, &new_subscriber).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[tracing::instrument(
    name = "Save new subscriber detial in database",
    skip(new_subscriber, pool)
)]
pub async fn insert_subscriber(
    pool: &PgPool,
    new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
    sqlx::query!(
        r#"insert into subscriptions (id,email,name,subscribed_at) values($1,$2,$3,$4)"#,
        Uuid::new_v4(),
        new_subscriber.email.as_ref(),
        new_subscriber.name.as_ref(),
        Utc::now()
    )
    .execute(pool)
    .await
    .map_err(|e| {
        tracing::error!("Failed to execute query :{:?}", e);
        e
    })?;
    Ok(())
}

运行单元测试

rust 复制代码
cargo test
   Compiling zero2prod v0.1.0 (/Users/kunliu/project/my/zero2prod)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00s
     Running unittests src/lib.rs (target/debug/deps/zero2prod-11120d3e12140346)

running 10 tests
test domain::subscriber_email::tests::email_missing_at_symbol_is_rejected ... ok
test domain::subscriber_email::tests::empty_string_is_rejected ... ok
test domain::subscriber_name::tests::empty_string_is_rejected ... ok
test domain::subscriber_name::tests::a_valid_name_is_parsed_successfully ... ok
test domain::subscriber_name::tests::a_name_longer_than_256_graphemes_is_rejected ... ok
test domain::subscriber_name::tests::names_containing_an_invalid_character_are_rejected ... ok
test domain::subscriber_name::tests::whitespace_only_names_are_rejected ... ok
test domain::subscriber_name::tests::a_256_grapheme_long_name_is_valid ... ok
test domain::subscriber_email::tests::email_missing_subject_is_rejected ... ok
test domain::subscriber_email::tests::valid_emails_are_parsed_successfully ... ok

总结

确实,验证POST请求中/subscriptions路径下的有效负载(payload)中的电子邮件地址是否符合预期格式只是确保数据质量的第一步。如你所提到的,即使一个电子邮件地址在语法上是有效的,我们仍然无法确定该地址是否实际存在、被使用或可以接收邮件。

为了进一步确认电子邮件地址的有效性和可达性,发送一封确认邮件是一种常见且有效的做法。这个过程通常包括以下几个步骤:

  1. 生成唯一的确认令牌:为每个订阅请求创建一个独一无二的令牌,这可以防止恶意用户猜测其他用户的确认链接。
  2. 保存状态:将新订阅者的信息和生成的确认令牌存储在数据库中,但不将其标记为已确认状态。
  3. 发送确认邮件:通过电子邮件服务向提供的电子邮件地址发送一封包含确认链接的邮件。该链接应指向你的应用,并包含上述的唯一令牌作为查询参数或路径的一部分。
  4. 处理确认:当用户点击邮件中的链接时,服务器需要验证提供的令牌,并检查它是否与未确认的订阅记录相匹配。如果匹配成功,则更新数据库以标记该订阅为已确认。
  5. 清理过期的订阅尝试:定期清理那些从未被确认的订阅尝试,以保持数据库整洁。
  6. 提供反馈:给用户提供关于确认过程的状态反馈,无论是通过邮件还是网站上的通知。

编写HTTP客户端来执行这些操作涉及到选择合适的库(例如,在Rust中可以使用reqwest),配置邮件服务(比如SendGrid、Mailgun等),并处理可能发生的各种错误情况。此外,还需要考虑安全性问题,例如保护令牌免受泄露,以及防止滥用API接口。

下一章深入探讨确认邮件的实现细节以及如何构建一个可靠的HTTP客户端,将是提升应用程序用户体验和服务可靠性的重要一步。如果你有具体的编程语言或框架偏好,我可以提供更多针对性的指导。

相关推荐
ahhhhaaaa-1 分钟前
【AI图像生成网站&Golang】项目架构
开发语言·架构·golang
龙少954312 分钟前
【Http,Netty,Socket,WebSocket的应用场景和区别】
java·后端·websocket·网络协议·http
计算机学姐15 分钟前
基于SpringBoot的校园求职招聘管理系统
java·前端·vue.js·spring boot·后端·mysql·intellij-idea
zyx没烦恼17 分钟前
【C++11】可变模板参数
开发语言·c++
Seven_cm35 分钟前
JDK21执行java -jar xxx.jar 文件时 “An unexpected error occurred” 问题处理
java·开发语言·jar
一起学习计算机1 小时前
29、基于springboot的网上购物商城系统研发
java·spring boot·后端
百流1 小时前
Intellij配置scala运行环境
开发语言·scala·intellij-idea
我不是程序猿儿1 小时前
【C#】Debug和Release的区别和使用
开发语言·c#
lisacumt1 小时前
[java] 简单的熔断器scala语言案例
java·开发语言·scala
百流1 小时前
scala基础学习(数据类型)-字符串
开发语言·学习·scala