打破域子模块
通常指的是对应用程序的某个特定业务领域进行重构或重新组织。这可能包括拆分、合并或重组代码结构以更好地反映业务规则和逻辑。下面是一些关于如何处理这种情况的建议:
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 testCompiling zero2prod v0.1.0 (/Users/kunliu/project/my/zero2prod)Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00sRunning 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)中的电子邮件地址是否符合预期格式只是确保数据质量的第一步。如你所提到的,即使一个电子邮件地址在语法上是有效的,我们仍然无法确定该地址是否实际存在、被使用或可以接收邮件。
为了进一步确认电子邮件地址的有效性和可达性,发送一封确认邮件是一种常见且有效的做法。这个过程通常包括以下几个步骤:
- 生成唯一的确认令牌:为每个订阅请求创建一个独一无二的令牌,这可以防止恶意用户猜测其他用户的确认链接。
- 保存状态:将新订阅者的信息和生成的确认令牌存储在数据库中,但不将其标记为已确认状态。
- 发送确认邮件:通过电子邮件服务向提供的电子邮件地址发送一封包含确认链接的邮件。该链接应指向你的应用,并包含上述的唯一令牌作为查询参数或路径的一部分。
- 处理确认:当用户点击邮件中的链接时,服务器需要验证提供的令牌,并检查它是否与未确认的订阅记录相匹配。如果匹配成功,则更新数据库以标记该订阅为已确认。
- 清理过期的订阅尝试:定期清理那些从未被确认的订阅尝试,以保持数据库整洁。
- 提供反馈:给用户提供关于确认过程的状态反馈,无论是通过邮件还是网站上的通知。
编写HTTP客户端来执行这些操作涉及到选择合适的库(例如,在Rust中可以使用reqwest
),配置邮件服务(比如SendGrid、Mailgun等),并处理可能发生的各种错误情况。此外,还需要考虑安全性问题,例如保护令牌免受泄露,以及防止滥用API接口。
下一章深入探讨确认邮件的实现细节以及如何构建一个可靠的HTTP客户端,将是提升应用程序用户体验和服务可靠性的重要一步。如果你有具体的编程语言或框架偏好,我可以提供更多针对性的指导。