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

news/2024/12/22 23:32:26/

打破域子模块

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

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)中的电子邮件地址是否符合预期格式只是确保数据质量的第一步。如你所提到的,即使一个电子邮件地址在语法上是有效的,我们仍然无法确定该地址是否实际存在、被使用或可以接收邮件。

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

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

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

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


http://www.ppmy.cn/news/1557320.html

相关文章

Java项目--仿RabbitMQ的消息队列--内存数据管理

目录 一、引言 二、MemoryDataCenter 1.设计数据结构 2.封装Exchange方法 3.封装MsgQueue方法 4.封装Binding方法 5.封装Message 6.实现待确定消息的管理 7.将数据从硬盘上恢复到内存中 三、测试MemoryDataCenter 1.准备工作 2.测试交换机 3.测试队列 4.测试绑定 …

Go, Jocko, Kafka

本篇内容是根据2016年8月份# 31. Go, Jocko, Kafka 音频录制内容的整理与翻译 Travis Jeffery 参加了节目&#xff0c;谈论 Go、Jocko、Kafka、Kafka 的存储内部结构如何工作&#xff0c;以及有趣的 Go 项目和新闻。 Erik St. Martin: 大家好&#xff0c;欢迎回到《GoTime》的另…

建站经验:服务器同步与数据库备份的终极解决方案

兼容宝塔面板&#xff0c;请按照步骤来配置 一键部署脚本 curl -sS -O https://raw.githubusercontent.com/woniu336/open_shell/main/rsync-vps.sh && chmod x rsync-vps.sh && ./rsync-vps.sh注意事项 提示&#xff08;宝塔面板&#xff09;&#xff1a;两…

对 MYSQL 架构的了解

MySQL 是一种广泛使用的关系型数据库管理系统&#xff0c;其架构主要包括以下几个关键部分&#xff1a; 一、连接层 客户端连接管理&#xff1a;MySQL 服务器可以同时处理多个客户端的连接请求。当客户端应用程序&#xff08;如使用 Java、Python 等语言编写的程序&#xff09;…

K8s中 statefulset 和deployment的区别

在 Kubernetes 中&#xff0c;StatefulSet 和 Deployment 是两种管理 Pod 的控制器&#xff0c;它们的主要区别在于 状态管理 和 Pod 的标识。以下是详细对比&#xff1a; 1. 功能定位 Deployment 用途&#xff1a;用于 无状态应用 的部署&#xff0c;例如 Web 服务、API 服务…

【LuaFramework】服务器模块相关知识

目录 一、客户端代码 二、本地服务器代码 三、解决服务器无法多次接收客户端消息问题 一、客户端代码 连接本地服务器127.0.0.1:2012端口&#xff08;如何创本地服务器&#xff0c;放最后说&#xff09;&#xff0c;连接成功后会回调 协议号Connect是101&#xff0c;其他如下…

霍特林分布

内容来源 应用多元统计分析 北京大学出版社 高惠璇编著 霍特林 T 2 T^2 T2 分布 一元统计中&#xff0c;若 X ∼ N ( 0 , 1 ) X\sim N(0,1) X∼N(0,1) ξ ∼ χ 2 ( n ) \xi\sim\chi^2(n) ξ∼χ2(n) 且 X X X 与 ξ \xi ξ 相互独立&#xff0c;则 t X ξ / n ∼ t ( n …

Transform组件的用法

文章目录 1. 概念介绍2. 使用方法3. 示例代码我们在上一章回中介绍了Checkbox Widget相关的内容,本章回中将介绍Transform Widget.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在这里说的Transform是一种容器类widget,它和Container组件类似。它可以包含其它的组件…