14. Crate Architecture and API Design 🟡
# 15. Crate 架构与 API 设计 🟡
What you’ll learn:
本章将学到什么:
- Module layout conventions and re-export strategies
模块布局惯例与重新导出策略- The public API design checklist for polished crates
打磨公开 API 的一套检查清单- Ergonomic parameter patterns:
impl Into,AsRef,Cow
更顺手的参数模式:impl Into、AsRef、Cow- “Parse, don’t validate” with
TryFromand validated types
如何用TryFrom和已验证类型贯彻“解析,而不是事后校验”- Feature flags, conditional compilation, and workspace organization
特性开关、条件编译以及 workspace 组织方式
Module Layout Conventions
模块布局惯例
my_crate/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── config.rs
│ ├── parser/
│ │ ├── mod.rs
│ │ ├── lexer.rs
│ │ └── ast.rs
│ ├── error.rs
│ └── utils.rs
├── tests/
├── benches/
└── examples/
#![allow(unused)]
fn main() {
// lib.rs — curate your public API with re-exports:
mod config;
mod error;
mod parser;
mod utils;
pub use config::Config;
pub use error::Error;
pub use parser::Parser;
}
The idea is simple: internal layout may be deep, but the public API should feel shallow and intentional. Users should import my_crate::Config, not spend their day spelunking through internal module trees.
核心思路很简单:内部目录结构可以深,但公开 API 应该尽量浅、尽量有意图。调用方最好直接写 my_crate::Config,而不是天天钻内部模块树找类型。
Visibility modifiers:
可见性修饰符:
| Modifier 修饰符 | Visible To 可见范围 |
|---|---|
pub | Everyone 所有地方 |
pub(crate) | This crate only 当前 crate |
pub(super) | Parent module 父模块 |
pub(in path) | Specific ancestor module 指定祖先模块 |
| (none) | Current module and children 当前模块及其子模块 |
Public API Design Checklist
公开 API 设计清单
- Accept references, return owned values when appropriate.
能接引用就先接引用,适合返回拥有值时再返回拥有值。 - Prefer readable signatures.
签名优先清晰,不要为了炫技把泛型写成天书。 - Return
Resultinstead of panicking.
优先返回Result,别把错误处理替调用方做掉。 - Implement standard traits when they make sense.
该实现的标准 trait 尽量实现。 - Make invalid states unrepresentable.
尽量让非法状态根本无法表示。 - Use builders for complex configuration.
复杂配置优先 builder。 - Seal traits you do not want downstream crates to implement.
不希望外部实现的 trait,用 sealed pattern 收口。 - Mark important return values with
#[must_use].
重要返回值可以加#[must_use],防止调用方顺手丢掉。
#![allow(unused)]
fn main() {
mod private {
pub trait Sealed {}
}
pub trait DatabaseDriver: private::Sealed {
fn connect(&self, url: &str) -> Connection;
}
}
#[non_exhaustive] is another valuable tool for public enums and structs, because it lets you add fields or variants later without immediately turning a minor feature release into a semver breakage.#[non_exhaustive] 也是公开枚举和结构体上很有价值的工具,因为它能让后续新增字段或变体时,不至于立刻把一次普通迭代升级成语义化版本灾难。
Ergonomic Parameter Patterns — impl Into, AsRef, Cow
更顺手的参数模式:impl Into、AsRef、Cow
Good Rust APIs usually accept the most general form they can reasonably support, so callers do not have to keep writing .to_string()、.as_ref() and similar conversion noise everywhere.
好的 Rust API 通常会尽量接受“足够泛化”的参数形式,这样调用方就不用在每个调用点重复写 .to_string()、.as_ref() 这种低信息量转换。
impl Into<T> — Accept Anything Convertible
impl Into<T>:接受任何能转成目标类型的值
#![allow(unused)]
fn main() {
fn connect(host: impl Into<String>, port: u16) -> Connection {
let host = host.into();
// ...
}
}
Use this when the function will own the value internally.
当函数内部最终要拿到这个值的所有权时,就很适合用它。
AsRef<T> — Borrow Flexibly
AsRef<T>:灵活借用
#![allow(unused)]
fn main() {
use std::path::Path;
fn file_exists(path: impl AsRef<Path>) -> bool {
path.as_ref().exists()
}
}
Use this when the function only needs a borrowed view and does not need to keep ownership.
如果函数只是想借来看看,不打算长期拥有,那就更适合 AsRef。
Cow<T> — Borrow If You Can, Own If You Must
Cow<T>:能借就借,实在不行再拥有
#![allow(unused)]
fn main() {
use std::borrow::Cow;
fn normalize_message(msg: &str) -> Cow<'_, str> {
if msg.contains('\t') || msg.contains('\r') {
Cow::Owned(msg.replace('\t', " ").replace('\r', ""))
} else {
Cow::Borrowed(msg)
}
}
}
This pattern is ideal when most callers stay on the cheap borrowed path, but a minority need a transformed owned result.
这种模式最适合那种“多数调用都能走廉价借用路径,少数情况才需要真正分配新值”的接口。
Quick Reference
快速参考
| Pattern 模式 | Ownership 所有权 | Allocation 分配 | Use When 适用场景 |
|---|---|---|---|
&str | Borrowed 借用 | Never | Simple read-only string params 简单只读字符串参数 |
impl AsRef<str> | Borrowed | Never | Accept &str、String etc.接受多种字符串形式 |
impl Into<String> | Owned | On conversion | Need to store internally 内部要保存所有权 |
Cow<'_, str> | Either | Only when needed | Usually borrowed, occasionally rewritten 大多借用,偶尔改写 |
Case Study: Designing a Public Crate API — Before & After
案例:公开 crate API 的前后对比
Before:
改造前:
#![allow(unused)]
fn main() {
fn parse_config(path: &str, format: &str, strict: bool) -> Result<Config, String> {
todo!()
}
}
After:
改造后:
#![allow(unused)]
fn main() {
pub enum Format {
Json,
Toml,
Yaml,
}
pub enum Strictness {
Strict,
Lenient,
}
pub fn parse_config(
path: &Path,
format: Format,
strictness: Strictness,
) -> Result<Config, ConfigError> {
todo!()
}
}
The new version is more verbose on paper, but much stronger in meaning: invalid values are harder to pass, booleans stop pretending to be self-documenting, and errors become structured instead of collapsing into raw strings.
新版本表面上更长,但语义强度高得多:非法值更难传进来,布尔参数也不再假装自己“天生就自解释”,错误信息也从原始字符串进化成了结构化类型。
Parse, Don’t Validate — TryFrom and Validated Types
解析,而不是事后校验:TryFrom 与已验证类型
The principle is: parse raw input at the boundary into a type that can only exist when valid, then pass that validated type around everywhere else.
这条原则的意思是:在边界处把原始输入解析成“只有合法时才能存在”的类型,之后在系统内部就一直传这个已验证类型,而不是到处拿裸值再反复校验。
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Port(u16);
impl TryFrom<u16> for Port {
type Error = PortError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value == 0 {
Err(PortError::Zero)
} else {
Ok(Port(value))
}
}
}
}
Once a function accepts Port instead of u16, the compiler itself starts carrying part of the validation burden for you.
一旦函数参数改成接 Port 而不是裸 u16,编译器就开始帮着承担一部分校验工作了。
| Approach 方式 | Data checked? 是否检查数据 | Compiler enforces validity? 编译器是否帮助保证合法性 | Re-validation needed? 是否需要反复校验 |
|---|---|---|---|
| Runtime checks | ✅ | ❌ | Often yes 通常需要 |
Validated newtype + TryFrom | ✅ | ✅ | No 通常不需要 |
Feature Flags and Conditional Compilation
特性开关与条件编译
[features]
default = ["json"]
json = ["dep:serde_json"]
xml = ["dep:quick-xml"]
full = ["json", "xml"]
#![allow(unused)]
fn main() {
#[cfg(feature = "json")]
pub fn to_json<T: serde::Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap()
}
}
Feature flags are for shaping optional capability, not for randomly exploding your API surface. Keep defaults small, document them clearly, and use conditional compilation to make optional dependencies truly optional.
特性开关的作用,是组织“可选能力”,而不是把 API 面摊得一地都是。默认特性尽量小,文档说明尽量清楚,条件编译则要真正把可选依赖隔离开。
Workspace Organization
Workspace 组织
[workspace]
members = [
"core",
"parser",
"server",
"client",
"cli",
]
A workspace gives you one lockfile, shared dependency versions, shared build cache, and a cleaner separation between components.
workspace 带来的好处很实在:统一的 lockfile、统一的依赖版本、共享构建缓存,以及更清晰的组件边界。
.cargo/config.toml: Project-Level Configuration
.cargo/config.toml:项目级 Cargo 配置
This file lets you put target defaults, custom runners, cargo aliases, build environment variables, and other project-level Cargo behavior in one place.
这个文件可以统一放置默认 target、自定义 runner、cargo alias、构建环境变量等项目级配置。
Common use cases include:
default targets, QEMU runners, alias commands, offline mode, and build-time environment variables.
常见用途包括:默认目标平台、QEMU runner、命令别名、离线模式和构建期环境变量。
Compile-Time Environment Variables: env!() and option_env!()
编译期环境变量:env!() 与 option_env!()
Rust can bake environment variables into the binary at compile time, which is useful for versions, commit hashes, build timestamps, and similar metadata.
Rust 可以在编译期把环境变量直接塞进二进制里,这对版本号、提交哈希、构建时间戳之类元信息特别有用。
#![allow(unused)]
fn main() {
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_SHA: Option<&str> = option_env!("GIT_SHA");
}
cfg_attr: Conditional Attributes
cfg_attr:条件属性
cfg_attr applies an attribute only when a condition is true, which is often cleaner than conditionally including or excluding entire items.cfg_attr 可以在条件成立时才附加一个属性。很多时候,它比直接把整个条目用 #[cfg] 包起来更细腻、更干净。
#![allow(unused)]
fn main() {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct DiagResult {
pub fc: u32,
pub passed: bool,
pub message: String,
}
}
cargo deny and cargo audit: Supply-Chain Security
cargo deny 与 cargo audit:供应链安全
These tools help catch known CVEs, license issues, banned crates, duplicate versions, and risky dependency sources before they become production problems.
这两个工具能在问题进生产前,提前把已知漏洞、许可证问题、被禁用 crate、重复版本和危险依赖源这类坑揪出来。
Doc Tests: Tests Inside Documentation
文档测试:写在文档里的测试
Rust doc comments can contain runnable examples. That means documentation is not just prose; it can be continuously verified as executable truth.
Rust 的文档注释里可以直接塞可运行示例,这意味着文档不只是说明文字,它还能持续被验证成“真能跑的事实”。
Benchmarking with Criterion
用 Criterion 做基准测试
Public crate APIs often deserve dedicated benchmarks in benches/, especially parsers, serializers, validators, and protocol boundaries.
公开 crate 的核心 API 往往值得单独放进 benches/ 里做基准,尤其是解析器、序列化器、校验器和协议边界这些热点部分。
Key Takeaways — Architecture & API Design
本章要点 — 架构与 API 设计
- Accept the most general input type you can reasonably support, and return the most specific meaningful type.
参数尽量接受“合理范围内最泛”的输入类型,返回值尽量给出“语义最明确”的类型。- Parse once at the boundary, then carry validated types throughout the system.
在边界处解析一次,之后在系统内部一直传已验证类型。- Use
#[non_exhaustive]、#[must_use]and sealed traits deliberately to stabilize public APIs.
合理使用#[non_exhaustive]、#[must_use]和 sealed trait,可以显著提升公开 API 的稳定性。- Features, workspaces, and Cargo configuration are part of crate architecture, not just build trivia.
feature、workspace 和 Cargo 配置本身就是 crate 架构的一部分,不只是构建细节。
See also: Ch 10 — Error Handling and Ch 14 — Testing.
延伸阅读: 相关主题还可以接着看 第 10 章:错误处理 和 第 14 章:测试。
Exercise: Crate API Refactoring ★★ (~30 min)
练习:重构 Crate API ★★(约 30 分钟)
Refactor the following stringly-typed API into one that uses TryFrom、newtypes, and the builder pattern:
把下面这个字符串味特别重的 API 重构成使用 TryFrom、newtype 和 builder 模式的版本:
fn create_server(host: &str, port: &str, max_conn: &str) -> Server { ... }
Design a ServerConfig with validated Host、Port and MaxConnections types that reject invalid values at parse time.
设计一个 ServerConfig,并为 Host、Port 和 MaxConnections 定义已验证类型,在解析阶段就把非法值拦下来。
🔑 Solution
🔑 参考答案
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct Host(String);
impl TryFrom<&str> for Host {
type Error = String;
fn try_from(s: &str) -> Result<Self, String> {
if s.is_empty() { return Err("host cannot be empty".into()); }
if s.contains(' ') { return Err("host cannot contain spaces".into()); }
Ok(Host(s.to_string()))
}
}
#[derive(Debug, Clone, Copy)]
struct Port(u16);
impl TryFrom<u16> for Port {
type Error = String;
fn try_from(p: u16) -> Result<Self, String> {
if p == 0 { return Err("port must be >= 1".into()); }
Ok(Port(p))
}
}
}