云辇103

Love Life Love Coding Love You

一、类型状态模式(Typestate Pattern)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::marker::PhantomData;

struct Disconnected;
struct Connected;

struct Socket<State> {
fd: i32,
_state: PhantomData<State>,
}

impl Socket<Disconnected> {
fn connect(self, addr: &str) -> Socket<Connected> {
// ... connect logic ...
Socket { fd: self.fd, _state: PhantomData }
}
}

impl Socket<Connected> {
fn send(&mut self, data: &[u8]) { /* ... */ }
fn disconnect(self) -> Socket<Disconnected> {
Socket { fd: self.fd, _state: PhantomData }
}
}

// Socket<Disconnected> has no send() method — compile error if you try

这段代码展示了 Rust 中一种非常高级且强大的设计模式,叫做 Typestate Pattern(类型状态模式)

它的核心意思是:利用 Rust 的编译检查,确保你永远不会在错误的状态下调用错误的函数。

1.1 直白解释

在普通的编程语言中(比如 C++ 或 Java),你可能会写这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Socket {
is_connected: bool,
fd: i32,
}

impl Socket {
fn send(&self, data: &[u8]) {
if !self.is_connected {
panic!("还没连接呢!不能发数据!"); // 这种错误只有在程序运行时才会发现
}
// ...
}
}

但在 Rust 代码里,如果你试图在断开连接时调用 send(),程序根本编译不通过。 编译器会直接告诉你:”对不起,Socket<Disconnected> 这个类型没有 send 方法”。

1.2 实现原理

1.2.1 定义不同的状态(标签)

1
2
struct Disconnected; // 状态 A:断开
struct Connected; // 状态 B:连接

这两个结构体没有任何字段,它们只是”标签”。

1.2.2 让 Socket 携带状态

1
2
3
4
struct Socket<State> {
fd: i32,
_state: PhantomData<State>, // 告诉编译器:这个 Socket 现在处于 State 状态
}

PhantomData 就像一个占位符,它不占内存,只在编译阶段起作用,标记当前的 State 是什么。

1.2.3 为不同状态实现不同的方法

这是最关键的地方:方法是分家写的。

  • 只有 Socket<Disconnected> 才有 connect 方法:

    1
    2
    3
    impl Socket<Disconnected> {
    fn connect(self, ...) -> Socket<Connected> { ... }
    }

    注意:connect 会消耗掉原来的自己(self),返回一个全新的类型 Socket<Connected>

  • 只有 Socket<Connected> 才有 senddisconnect 方法:

    1
    2
    3
    4
    impl Socket<Connected> {
    fn send(&mut self, data: &[u8]) { ... }
    fn disconnect(self) -> Socket<Disconnected> { ... }
    }

1.3 代码示例对比

如果你这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let socket = Socket { fd: 1, _state: PhantomData }; // 默认是 Disconnected

// ❌ 编译报错!
// 编译器会说:Socket<Disconnected> 没定义 send 方法
// 你在写代码阶段就能发现错误,而不是等程序跑起来崩溃
socket.send(b"hello");

// ✅ 必须先 connect
let mut connected_socket = socket.connect("127.0.0.1");

// ✅ 现在可以 send 了,因为类型变成了 Socket<Connected>
connected_socket.send(b"hello");

// ❌ 再次报错!
// 一旦调用 disconnect,变量会被消耗,不能再调用 send
let closed_socket = connected_socket.disconnect();
connected_socket.send(b"bye"); // 编译报错:Value used after move

1.4 优势总结

  1. 状态安全:你不可能忘记检查连接状态,因为编译器替你检查了。
  2. 零成本抽象:这些 PhantomData 和泛型状态只存在于编译期,编译出来的机器码里没有任何多余的 if-else 判断,性能极高。
  3. API 指引:开发者在使用你的库时,编辑器只弹出会展示当前状态下可用的方法。如果你没连接,编辑器压根不会提示你 send 方法。

一句话总结:它把”逻辑错误”变成了”语法错误”。


二、通过关联类型确保协议正确性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::io;

trait IpmiCmd {
type Response;
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

// Simplified for illustration — see ch02 for the full trait with
// net_fn(), cmd_byte(), payload(), and parse_response().

struct ReadTemp { sensor_id: u8 }
impl IpmiCmd for ReadTemp {
type Response = Celsius;
fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
Ok(Celsius(raw[0] as i8 as f64))
}
}


fn execute<C: IpmiCmd>(cmd: &C, raw: &[u8]) -> io::Result<C::Response> {
cmd.parse_response(raw)
}
// ReadTemp always returns Celsius — can't accidentally get Rpm

这段代码展示了 Rust 中一个非常核心的设计哲学:通过类型系统确保协议正确性(Protocol Correctness),即所谓的”使非法状态不可表达“(Make invalid interactions unrepresentable)。

2.1 核心目标:绑定”请求”与”响应”

在底层协议(如 IPMI、NVMe)中,你发送一个特定的命令(Request),硬件会返回一段二进制数据。传统做法通常是手动解析这些字节,但这容易出错:你可能会不小心用解析”转速”的代码去解析”温度”数据。

这段代码通过 Rust 的 关联类型(Associated Types) 在编译阶段解决了这个问题。

2.2 代码逐段解析

2.2.1 定义 Trait(协议骨架)

1
2
3
4
trait IpmiCmd {
type Response; // 关联类型:每个命令必须定义它对应的返回类型
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
  • type Response; 是关键。它告诉编译器:任何实现了 IpmiCmd 的类型,都必须明确指定它返回什么类型的数据。

2.2.2 具体命令实现

1
2
3
4
5
6
7
8
9
10
struct ReadTemp { sensor_id: u8 }

impl IpmiCmd for ReadTemp {
type Response = Celsius; // 绑定:ReadTemp 命令只能返回 Celsius 类型

fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
// 将原始字节解析为 Celsius 结构体
Ok(Celsius(raw[0] as i8 as f64))
}
}
  • 这里将 ReadTemp 结构体与 Celsius(摄氏度)类型硬绑定。
  • 如果你尝试在 parse_response 里返回一个 Rpm(转速)类型,编译器会直接报错。

2.2.3 泛型执行函数

1
2
3
fn execute<C: IpmiCmd>(cmd: &C, raw: &[u8]) -> io::Result<C::Response> {
cmd.parse_response(raw)
}
  • 这是一个通用的执行函数。它的返回类型是 C::Response
  • 这意味着:如果你传入的是 ReadTemp,函数返回值的类型在编译时就被确定为 Celsius

2.3 为什么说这实现了”协议正确性”?

“ReadTemp always returns Celsius — can’t accidentally get Rpm”
(ReadTemp 总是返回 Celsius —— 不会意外地得到 Rpm)

  • 传统方式的风险: 在 C 语言或动态语言中,你可能写出 data = execute(READ_TEMP_CMD); rpm = parse_as_rpm(data); 这样的代码。这在逻辑上是错的,但编译器不会阻止你。
  • Rust 的安全性: 在这段代码的设计下,如果你写 let res: Rpm = execute(&read_temp_cmd, raw);代码根本无法通过编译。因为 ReadTempResponse 类型是 Celsius,它与 Rpm 类型不匹配。

2.4 硬件层面的实际应用

例子中提到的 IPMI, Redfish, NVMe Admin commands 都是复杂的硬件管理协议。

  • 这些协议有成百上千个命令。
  • 每个命令对应的二进制响应格式都不同。
  • 使用这种模式,可以为每个命令编写一个结构体,并绑定其特有的响应结构体。这样开发者在调用 API 时,类型系统会自动引导他们写出正确的解析逻辑,极大地减少了底层驱动开发中的 Bug。

2.5 总结

这段代码演示了如何利用 Rust 的 TraitAssociated Types 来建模复杂的交互协议。它将”运行时的协议逻辑错误”转换成了”编译时的类型错误”,从而保证了程序的绝对健壮性。

核心思想:把”逻辑错误”变成”语法错误”。

在 Rust 中实现”依赖倒置”(Dependency Inversion)非常自然。Rust 使用 Trait 来定义契约,并利用 泛型(Generics)Trait 对象(Trait Objects) 来注入依赖。

一、依赖倒置与订单服务

1.1 定义数据和契约

首先定义业务对象和它需要的操作接口。

1
2
3
4
5
6
7
8
9
10
#[derive(Debug, Clone)]
pub struct Order {
pub id: u64,
pub price: f64,
}

// 1. 定义 "契约" (What) —— 相当于 Go 的 Interface
pub trait OrderRepository: Send + Sync {
fn save(&self, order: &Order) -> Result<(), String>;
}

1.2 实现业务逻辑

在 Rust 中,我们通常使用泛型来实现这种注入。这在编译时就会确定类型,性能极高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 2. "不变" 的业务逻辑
pub struct OrderService<R: OrderRepository> {
repo: R, // 依赖于契约 R
}

impl<R: OrderRepository> OrderService<R> {
// 构造函数,注入具体的实现
pub fn new(repo: R) -> Self {
Self { repo }
}

pub fn create_order(&self, order: Order) -> Result<(), String> {
// 1. 核心业务逻辑 (100% 纯粹)
if order.price < 0.0 {
return Err("价格错误".to_string());
}

// 2. 调用 "契约",不关心底层是 MySQL, PostgreSQL 还是内存
self.repo.save(&order)
}
}

1.3 单元测试

有了抽象,测试业务逻辑就变得非常简单,不需要启动任何数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;

// 3. 定义一个 Mock 实现
struct MockRepo {
saved_orders: Mutex<Vec<Order>>,
}

impl OrderRepository for MockRepo {
fn save(&self, order: &Order) -> Result<(), String> {
let mut orders = self.saved_orders.lock().unwrap();
orders.push(order.clone());
Ok(())
}
}

#[test]
fn test_create_order_logic() {
// 准备 Mock 环境
let mock_repo = MockRepo { saved_orders: Mutex::new(vec![]) };
let service = OrderService::new(mock_repo);

// 测试业务逻辑:价格错误
let bad_order = Order { id: 1, price: -10.0 };
assert!(service.create_order(bad_order).is_err());

// 测试业务逻辑:成功保存
let good_order = Order { id: 2, price: 100.0 };
assert!(service.create_order(good_order).is_ok());
}
}

1.4 生产环境的真实实现

在生产代码中,你才去实现真正的数据库逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SqliteRepository {
// db: SqlitePool,
}

impl OrderRepository for SqliteRepository {
fn save(&self, order: &Order) -> Result<(), String> {
println!("正在将订单 {} 写入 SQLite 数据库...", order.id);
// 这里写真正的 SQL 逻辑
Ok(())
}
}

fn main() {
// 在程序入口处(组合根)完成注入
let repo = SqliteRepository {};
let service = OrderService::new(repo);

let _ = service.create_order(Order { id: 101, price: 50.0 });
}

二、装饰器模式:缓存封装

我们延续之前的例子,展示如何在不改动 OrderService 的情况下,像”套娃”一样把 Redis 功能套上去。

2.1 定义核心契约

1
2
3
4
5
6
7
8
9
use async_trait::async_trait;

#[derive(Debug, Clone)]
pub struct Order { pub id: u64, pub price: f64 }

#[async_trait]
pub trait OrderRepository: Send + Sync {
async fn save(&self, order: &Order) -> Result<(), String>;
}

2.2 数据库实现

1
2
3
4
5
6
7
8
9
pub struct SqliteRepo;

#[async_trait]
impl OrderRepository for SqliteRepo {
async fn save(&self, order: &Order) -> Result<(), String> {
println!(" [DB层] 正在将订单 {} 存入 Sqlite...", order.id);
Ok(())
}
}

2.3 缓存装饰器

重点: 它也实现 OrderRepository,但它内部持有另一个实现了该 Trait 的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub struct CachedOrderRepo<T: OrderRepository> {
inner: T, // "下一层"是谁?我不关心,只要它实现了 OrderRepository
redis_addr: String, // 模拟 Redis 连接
}

impl<T: OrderRepository> CachedOrderRepo<T> {
pub fn new(inner: T, addr: &str) -> Self {
Self { inner, redis_addr: addr.to_string() }
}
}

#[async_trait]
impl<T: OrderRepository> OrderRepository for CachedOrderRepo<T> {
async fn save(&self, order: &Order) -> Result<(), String> {
// 1. 调用下一层(底层的数据库写入)
self.inner.save(order).await?;

// 2. 增加"副作用":写入缓存
println!(" [缓存层] 正在将订单 {} 写入 Redis ({})", order.id, self.redis_addr);

Ok(())
}
}

2.4 业务逻辑层——保持不变

注意: 这里的代码和加缓存之前完全一模一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub struct OrderService<R: OrderRepository> {
repo: R,
}

impl<R: OrderRepository> OrderService<R> {
pub fn new(repo: R) -> Self { Self { repo } }

pub async fn create_order(&self, order: Order) -> Result<(), String> {
// 核心业务校验
if order.price < 0.0 { return Err("价格错误".to_string()); }

// 直接调用契约
self.repo.save(&order).await
}
}

2.5 程序入口处组装

这是见证奇迹的时刻:你通过改变对象的组合方式,改变了系统的行为,而没有修改业务逻辑代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#[tokio::main]
async fn main() {
// --- 方案 A: 原始版本 ---
// let repo = SqliteRepo;
// let service = OrderService::new(repo);

// --- 方案 B: 带缓存的版本 ---
let sqlite_repo = SqliteRepo;
// 像套娃一样,把 sqlite 塞进 cached repo
let cached_repo = CachedOrderRepo::new(sqlite_repo, "redis://127.0.0.1");
// 把装好的 cached_repo 塞进 service
let service = OrderService::new(cached_repo);

// 调用
let my_order = Order { id: 888, price: 99.0 };
service.create_order(my_order).await.unwrap();
}

2.6 这种写法的优势

  1. 零成本抽象 (Zero-cost Abstraction):
    如果你使用泛型,Rust 编译器在编译时会进行”单态化”(Monomorphization)。它会直接生成一套专门调用 Sqlite 后接 Redis 的机器码。没有运行时的虚函数表查找开销,性能等同于你手写的硬编码代码。

  2. 职责单一 (SRP):

    • OrderService: 只管业务逻辑(价格对不对)。
    • SqliteRepo: 只管 SQL 怎么写。
    • CachedOrderRepo: 只管 Redis 怎么存。
      如果你下周要加 Kafka,你只需要再写一个 KafkaOrderRepo 并在 main 函数里再套一层。
  3. 极度好写测试:

    • 你想测 OrderService?给它塞一个 MockRepo(不连 DB,不连 Redis)。
    • 你想测 CachedOrderRepo 的缓存逻辑?给它塞一个 MockRepo 作为 inner
  4. 符合开闭原则 (OCP):
    软件实体应对扩展开放,对修改关闭。你通过增加新类实现了新功能,而不是通过修改旧代码实现新功能。

三、重试机制

这就是装饰器模式最迷人的地方。我们可以非常轻松地再加一层”重试”逻辑,而无需触碰现有的 OrderServiceSqliteRepoCachedOrderRepo 的任何代码。

3.1 RetryOrderRepo 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pub struct RetryOrderRepo<T: OrderRepository> {
inner: T,
max_attempts: usize,
}

impl<T: OrderRepository> RetryOrderRepo<T> {
pub fn new(inner: T, max_attempts: usize) -> Self {
Self { inner, max_attempts }
}
}

#[async_trait]
impl<T: OrderRepository> OrderRepository for RetryOrderRepo<T> {
async fn save(&self, order: &Order) -> Result<(), String> {
let mut last_error = String::new();

for attempt in 1..=self.max_attempts {
println!(" [重试层] 正在进行第 {} 次尝试...", attempt);

match self.inner.save(order).await {
Ok(()) => return Ok(()), // 成功了,直接返回
Err(e) => {
last_error = e;
println!(" [重试层] 第 {} 次尝试失败: {}", attempt, last_error);
}
}
}

Err(format!("在 {} 次重试后仍然失败: {}", self.max_attempts, last_error))
}
}

3.2 套娃组装

现在的组装顺序是:
Service -> Retry (重试) -> Cache (缓存) -> Sqlite (数据库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[tokio::main]
async fn main() {
// 1. 最底层:数据库
let sqlite_repo = SqliteRepo;

// 2. 中间层:加个缓存
let cached_repo = CachedOrderRepo::new(sqlite_repo, "redis://127.0.0.1");

// 3. 最外层包装:加个重试(比如最多尝试 3 次)
let retry_repo = RetryOrderRepo::new(cached_repo, 3);

// 4. 注入业务层
let service = OrderService::new(retry_repo);

// 运行
let my_order = Order { id: 999, price: 150.0 };
if let Err(e) = service.create_order(my_order).await {
println!("最终执行失败: {}", e);
} else {
println!("最终执行成功!");
}
}

3.3 套娃顺序的讲究

  1. Retry 包裹 Cache (Retry(Cache(DB))):

    • 语义:如果”写DB+写缓存”整体失败了,就重试整个过程。
    • 适用场景:你希望保证缓存和数据库最终一致。
  2. Cache 包裹 Retry (Cache(Retry(DB))):

    • 语义:重试只针对 DB 写入。只有 DB 真的写成功了,才去写一次缓存。
    • 适用场景:你认为缓存写入通常不会失败,只有 DB 写入需要重试。
  3. Logging 包裹一切 (Log(Retry(Cache(DB)))):

    • 语义:记录最外层的请求和最终结果。

四、防腐层与短信服务

这个例子展示了架构设计中极其重要的 “防腐层”(Anti-Corruption Layer, ACL) 概念。

4.1 定义短信契约与业务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use async_trait::async_trait;

// --- 1. 定义契约 (What) ---
#[async_trait]
pub trait SmsService: Send + Sync {
async fn send(&self, phone: &str, code: &str) -> Result<(), String>;
}

// --- 2. 业务逻辑只依赖契约 ---
pub struct UserService<S: SmsService> {
sms: S, // 也可以用 Box<dyn SmsService> 支持运行时切换
}

impl<S: SmsService> UserService<S> {
pub fn new(sms: S) -> Self { Self { sms } }

pub async fn register(&self, phone: &str) -> Result<(), String> {
println!("[业务逻辑] 开始注册用户: {}", phone);
let code = "123456"; // 模拟生成验证码

// 调用契约,不关心背后是哪家云厂商
self.sms.send(phone, code).await?;

println!("[业务逻辑] 注册成功!");
Ok(())
}
}

4.2 多种发送方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 实现 A:腾讯云 (生产环境)
pub struct TencentSms { api_key: String }
#[async_trait]
impl SmsService for TencentSms {
async fn send(&self, phone: &str, code: &str) -> Result<(), String> {
println!(" [腾讯云] 调用 SDK 发送短信至 {},验证码:{}", phone, code);
Ok(())
}
}

// 实现 B:阿里云 (备用环境)
pub struct AliyunSms { secret_id: String }
#[async_trait]
impl SmsService for AliyunSms {
async fn send(&self, phone: &str, code: &str) -> Result<(), String> {
println!(" [阿里云] 调用 SDK 发送短信至 {},验证码:{}", phone, code);
Ok(())
}
}

// 实现 C:日志短信 (本地开发/单元测试)
pub struct LogSms;
#[async_trait]
impl SmsService for LogSms {
async fn send(&self, phone: &str, code: &str) -> Result<(), String> {
// 不花钱,不连网,只打日志
println!(" [本地模拟] 模拟发送短信至 {},验证码:{}", phone, code);
Ok(())
}
}

4.3 不同场景的组装

1
2
3
4
5
6
7
8
9
10
11
12
#[tokio::main]
async fn main() {
// 场景 1:本地开发
// 只需要传入 LogSms,不需要配置任何云账号,不需要联网
let dev_service = UserService::new(LogSms);
dev_service.register("13800138000").await.unwrap();

// 场景 2:线上生产(使用腾讯云)
let prod_sms = TencentSms { api_key: "TX_123".to_string() };
let prod_service = UserService::new(prod_sms);
prod_service.register("13911112222").await.unwrap();
}

4.4 深度价值

4.4.1 彻底解决”测试地狱”

如果没有这个 Trait,你写单元测试时必须处理腾讯云 SDK 的初始化、网络连接等。
有了 Trait,你在测试文件里写一个 struct TestSms,在 send 方法里直接 return Ok(())你的测试运行速度会从秒级降至毫秒级,且不消耗任何短信资费。

4.4.2 实现防腐层 (Anti-Corruption)

腾讯云 SDK 的 API 设计可能很古怪。如果你直接在 UserService 里用,你的业务逻辑就被腾讯的 API 风格”污染”了。

  • Trait 就像一堵墙:墙内是纯粹的业务语言(phone, code),墙外是肮脏的第三方 SDK 细节。
  • 更换供应商:换阿里云时,你只需要写一个新的 AliyunSms 实现类,业务代码一行都不用动。

4.5 运行时容灾切换

你可以利用 Rust 的 dyn SmsService 实现一个”智能切换器”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct FailoverSms {
primary: TencentSms,
secondary: AliyunSms,
}

#[async_trait]
impl SmsService for FailoverSms {
async fn send(&self, phone: &str, code: &str) -> Result<(), String> {
// 先尝试腾讯云
match self.primary.send(phone, code).await {
Ok(_) => Ok(()),
Err(_) => {
// 如果腾讯云挂了,自动切换到阿里云
println!("!!! 腾讯云异常,正在切换到阿里云备用通道...");
self.secondary.send(phone, code).await
}
}
}
}

这种强大的容灾能力,对于业务层 UserService 来说是完全透明的! 业务层只管调 send,它根本不知道背后发生了惊心动魄的线路切换。

五、总结

这种 impl<T: Trait> Trait for Wrapper<T> 的写法,是 Rust 高级架构的核心:基于已有的能力(Runtime/Inner),通过包装提供新的能力(Typed API/Caching),而不需要去动底层的代码。

这种”面向契约(Trait)”编程的思想,配合”装饰器模式”,能让你的 Rust 项目在变得极其复杂的同时,依然保持每一部分代码的纯粹和简单。

0%