行业资讯
Rust 实战中的设计模式:Builder、Visitor 和新类型模式在真实代码里的应用
Rust 实战中的设计模式Builder、Visitor 和新类型模式在真实代码里的应用一、从一次数据迁移的翻车说起前阵子我在给项目加一个新功能支持从 JSON、TOML、环境变量三种方式加载配置。一开始我写了三个独立的parse_json、parse_toml、parse_env函数分别返回ResultConfig, Error。看起来没问题直到产品经理说用户可能需要同时使用 JSON 和环境变量环境变量的优先级更高。于是我的代码变成了这样let mut config parse_json(json_path)?; let env_config parse_env()?; // 合并环境变量覆盖 JSON 配置…… // 代码越来越乱这不是 Rust 的问题是我没用好设计模式。Rust 的所有权、生命周期和 trait 系统让传统 GoF 模式的实现方式完全不同但核心思想依然适用。这篇文章我会结合真实项目中的三个例子讲清 Builder、Visitor 和新类型模式在 Rust 里怎么用才地道。二、Builder 模式复杂对象的构造者Builder 在 Rust 里最常见的使用场景是构造配置对象——字段多、有默认值、构造逻辑不互斥。2.1 标准实现/// 数据库连接配置——字段多每个都有合理默认值 #[derive(Debug, Clone)] pub struct DatabaseConfig { pub host: String, pub port: u16, pub username: String, pub password: String, pub database: String, pub max_connections: u32, pub connection_timeout_ms: u64, pub use_ssl: bool, pub ssl_ca_path: OptionString, } /// 构建器——通过链式调用逐步填充配置 /// 每个 setter 返回 mut Self支持建者链式调用 pub struct DatabaseConfigBuilder { host: String, port: u16, username: String, password: String, database: String, max_connections: u32, connection_timeout_ms: u64, use_ssl: bool, ssl_ca_path: OptionString, } impl DatabaseConfigBuilder { /// 必填字段通过构造函数传入 pub fn new(host: str, username: str, password: str) - Self { Self { host: host.to_string(), port: 5432, // Postgres 默认端口 username: username.to_string(), password: password.to_string(), database: postgres.to_string(), max_connections: 10, connection_timeout_ms: 5000, use_ssl: true, ssl_ca_path: None, } } /// 可选字段通过链式调用设置 pub fn port(mut self, port: u16) - mut Self { self.port port; self } pub fn database(mut self, db: str) - mut Self { self.database db.to_string(); self } pub fn max_connections(mut self, max: u32) - mut Self { self.max_connections max; self } pub fn connection_timeout(mut self, timeout_ms: u64) - mut Self { self.connection_timeout_ms timeout_ms; self } pub fn with_ssl(mut self, enabled: bool) - mut Self { self.use_ssl enabled; self } /// 最终构建——在此处做参数校验 pub fn build(self) - ResultDatabaseConfig, ConfigError { // 校验如果开启 SSL 但没提供 CA 证书路径则报错 if self.use_ssl self.ssl_ca_path.is_none() { return Err(ConfigError::MissingSslCertificate); } // 校验连接数不能为 0 if self.max_connections 0 { return Err(ConfigError::InvalidMaxConnections); } Ok(DatabaseConfig { host: self.host.clone(), port: self.port, username: self.username.clone(), password: self.password.clone(), database: self.database.clone(), max_connections: self.max_connections, connection_timeout_ms: self.connection_timeout_ms, use_ssl: self.use_ssl, ssl_ca_path: self.ssl_ca_path.clone(), }) } } #[derive(Debug)] pub enum ConfigError { MissingSslCertificate, InvalidMaxConnections, }build()方法里做校验是 Builder 模式的关键——把构造和验证分离。构造阶段可以随意传参验证在最后一步统一做。这比在构造函数里写一堆if/else清晰太多。2.2 多层配置的 Builder 组合我们的 AI CLI 工具有一层复杂的配置优先级配置文件 环境变量 命令行参数。多个 Builder 组合后的流程如下每个 Builder 关注一种配置来源最后一个MergedConfigBuilder负责按优先级合并。这种分治 合并的策略让配置逻辑从几百行 if/else 缩减到每个 Builder 不超过 50 行。三、Visitor 模式在不修改类型的前提下扩展操作Visitor 的核心价值在于添加新操作时不需要修改已有的类型。在 Rust 里enum 配合 match 已经覆盖了很多场景但当你的 AST 类型特别多、每种类型需要多种不同的处理方式时Visitor 就比大 match 更优雅。/// AST 节点定义——约定了所有节点类型 /// 注意添加新节点不需要改动任何 Visitor #[derive(Debug, Clone)] pub enum SqlNode { Select(SelectStatement), Insert(InsertStatement), Update(UpdateStatement), Delete(DeleteStatement), Where(WhereClause), Join(JoinClause), } /// Visitor trait——定义所有操作的接口 /// 每种操作实现一个独立的 Visitor pub trait SqlVisitor { type Output; fn visit_select(self, node: SelectStatement) - Self::Output; fn visit_insert(self, node: InsertStatement) - Self::Output; fn visit_update(self, node: UpdateStatement) - Self::Output; fn visit_delete(self, node: DeleteStatement) - Self::Output; fn visit_where(self, node: WhereClause) - Self::Output; fn visit_join(self, node: JoinClause) - Self::Output; } impl SqlNode { /// 接受 Visitor 的通用入口 /// 添加新操作只需要实现新的 Visitor不需要改这行代码 pub fn acceptV: SqlVisitor(self, visitor: V) - V::Output { match self { SqlNode::Select(node) visitor.visit_select(node), SqlNode::Insert(node) visitor.visit_insert(node), SqlNode::Update(node) visitor.visit_update(node), SqlNode::Delete(node) visitor.visit_delete(node), SqlNode::Where(node) visitor.visit_where(node), SqlNode::Join(node) visitor.visit_join(node), } } } // 两个具体的 Visitor 实现 /// 格式化 Visitor——负责将 AST 转为 SQL 字符串 pub struct FormatVisitor; impl SqlVisitor for FormatVisitor { type Output String; fn visit_select(self, node: SelectStatement) - String { format!( SELECT {} FROM {}, node.columns.join(, ), node.table ) } fn visit_insert(self, node: InsertStatement) - String { format!( INSERT INTO {} ({}) VALUES ({}), node.table, node.columns.join(, ), node.values.join(, ) ) } // ... 其他方法实现 } /// 安全检查 Visitor——负责检测危险操作 pub struct SecurityCheckVisitor; impl SqlVisitor for SecurityCheckVisitor { type Output VecString; // 返回发现的警告列表 fn visit_select(self, node: SelectStatement) - VecString { let mut warnings vec![]; // 检测是否 SELECT *(全表扫描风险) if node.columns.contains(*.to_string()) node.where_clause.is_none() { warnings.push(SELECT * 无 WHERE 条件可能触发全表扫描.to_string()); } warnings } fn visit_insert(self, node: InsertStatement) - VecString { // 插入安全检查逻辑 vec![] } // ... 其他方法实现 } // 使用示例 // let node SqlNode::Select(select_stmt); // let sql node.accept(FormatVisitor); // 生成 SQL // let warnings node.accept(SecurityCheckVisitor); // 安全检查Visitor 的真实收益体现在复杂度的解耦上。我们的 SQL Builder 有 6 种节点类型和 4 种操作格式化、安全检查、参数提取、血缘分析。如果用一个大 match 每个 arm 里写 4 种逻辑代码会膨胀到 500 行以上且难以阅读。Visitor 把每种操作拆分到独立的 impl 块里维护成本降低了至少一半。四、新类型模式给裸类型套上语义新类型模式Newtype是 Rust 里最简单却最强大的模式——用单字段结构体包裹原始类型赋予它独特的语义和类型安全。/// 新类型模式——防止不同类型的 ID 混用 /// 下面的三个 ID 即使底层都是 u64也不能互相赋值 /// 用户 ID——只能通过 parse 方法创建防止魔数 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(u64); /// 订单 ID #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct OrderId(u64); /// 支付 ID #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PaymentId(u64); impl UserId { /// 从字符串解析校验格式 pub fn parse(input: str) - ResultSelf, ParseError { let id: u64 input.parse().map_err(|_| ParseError::InvalidFormat)?; if id 0 { return Err(ParseError::ZeroId); } Ok(UserId(id)) } pub fn as_u64(self) - u64 { self.0 } } // 编译期保证类型安全 fn get_user(id: UserId) - User { // 只接受 UserId unimplemented!() } fn get_order(id: OrderId) - Order { // 只接受 OrderId unimplemented!() } // ❌ 编译错误类型不匹配 // let order_id OrderId(100); // get_user(order_id); // expected UserId, found OrderId /// 另一种典型用途——限制字符串的语义 /// 电子邮件地址——构造时自动校验格式 #[derive(Debug, Clone)] pub struct Email(String); impl Email { pub fn new(input: str) - ResultSelf, EmailError { // 简单的邮箱格式校验 if !input.contains() || !input.contains(.) { return Err(EmailError::InvalidFormat); } Ok(Email(input.to_string())) } /// 获取原始字符串用于数据库存储或序列化 pub fn as_str(self) - str { self.0 } } impl std::fmt::Display for Email { fn fmt(self, f: mut std::fmt::Formatter_) - std::fmt::Result { write!(f, {}, self.0) } }新类型最实际的收益在函数签名里看一眼参数类型就知道它要什么。fn charge(amount: u64, from: UserId, to: PaymentId)一眼就能看懂调用方不会把amount和from弄混——编译器帮你保证。踩过一个坑用新类型包装了u64做Timestamp但忘了实现Ordtrait结果排序的时候编译器报了一堆the trait boundTimestamp: Ordis not satisfied。排查了 20 分钟才发现问题在类型定义处不在排序代码。新类型好用但每个需要用到的 trait 都得显式 derive少了任何一个就是编译错误。新类型模式虽简单但犯错的代价几乎为零——编译器一分钱不收就帮你改好了。五、总结Rust 的设计模式和传统 OOP 语言最大的区别在于不使用继承全靠 trait 和组合。Visitor 里的SqlVisitortrait 替代了 OOP 里的interface所有权是免费的文档。新类型模式不需要额外的注释类型本身就告诉了你它的用法编译期保证安全。Builder 的build()验证、Newtype 的类型隔离——错误在编译期就被发现而不是在生产环境炸。我的建议是读代码时先关注类型定义。一个精心设计的新类型结构体比十行注释更能说清楚函数的意图。Builder 能大幅减少构造函数的参数爆炸Visitor 能让 AST 操作和 AST 定义解耦。这三个模式我几乎在每个 Rust 项目里都会用到而且每次都能让代码质量提升一个档次。
郑州网站建设
网页设计
企业官网