docs: API design patterns from rust-lang/rust
10 patterns, 596 lines. Full spec compliance. Patterns: naming conventions, #[non_exhaustive], pub(crate), constructors, method chaining, trait bounds, typestate, extension traits, newtype, per-module errors.
This commit is contained in:
@@ -0,0 +1,596 @@
|
|||||||
|
# Rust API Design Patterns
|
||||||
|
|
||||||
|
Patterns for designing public APIs in Rust, extracted from the
|
||||||
|
standard library source.
|
||||||
|
|
||||||
|
**Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit
|
||||||
|
[`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6)
|
||||||
|
|
||||||
|
**Stats:** 18,430 pub fn, 919 pub(crate), 53 #[non_exhaustive],
|
||||||
|
889 is_/as_/to_/into_ methods, 162 new/build constructors,
|
||||||
|
2,676 impl blocks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Naming Conventions (is_, as_, to_, into_)
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
889 methods following these naming conventions in library/.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Option<T> {
|
||||||
|
pub fn is_some(&self) -> bool { ... } // is_ → bool check
|
||||||
|
pub fn as_ref(&self) -> Option<&T> { ... } // as_ → cheap ref conversion
|
||||||
|
}
|
||||||
|
|
||||||
|
impl String {
|
||||||
|
pub fn as_str(&self) -> &str { ... } // as_ → borrow (no allocation)
|
||||||
|
pub fn into_bytes(self) -> Vec<u8> { ... } // into_ → consuming conversion
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
pub fn to_str(&self) -> Option<&str> { ... } // to_ → potentially expensive
|
||||||
|
pub fn to_string_lossy(&self) -> Cow<str> { ... } // to_ → may allocate
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
The prefix tells you the cost and ownership semantics without
|
||||||
|
reading the implementation:
|
||||||
|
|
||||||
|
- `is_` → Returns bool, no allocation, &self
|
||||||
|
- `as_` → Cheap conversion, returns a reference, &self
|
||||||
|
- `to_` → Potentially expensive, may allocate, &self
|
||||||
|
- `into_` → Consumes self, zero-cost ownership transfer
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- `is_`: Predicate (yes/no check on &self)
|
||||||
|
- `as_`: Cheap view (reinterpret bits, return &T)
|
||||||
|
- `to_`: Conversion that may allocate or compute (returns owned)
|
||||||
|
- `into_`: Consuming conversion (self → OtherType)
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
impl Email {
|
||||||
|
fn get_domain(&self) -> &str { ... } // "get" prefix — Java style
|
||||||
|
fn make_lowercase(self) -> Email { ... } // "make" — unclear cost
|
||||||
|
fn check_valid(&self) -> bool { ... } // "check" — inconsistent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
impl Email {
|
||||||
|
fn as_domain(&self) -> &str { ... } // as_ → cheap, returns ref
|
||||||
|
fn into_lowercase(self) -> Email { ... } // into_ → consumes self
|
||||||
|
fn is_valid(&self) -> bool { ... } // is_ → predicate
|
||||||
|
fn to_ascii(&self) -> String { ... } // to_ → allocates new String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- The method doesn't fit any prefix (use a descriptive verb)
|
||||||
|
- `new` for constructors (not `create_` or `make_`)
|
||||||
|
- `get`/`set` for simple field access on non-collection types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. #[non_exhaustive] for Future-Proof Enums
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
[library/std/src/io/error.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/io/error.rs) (ErrorKind)
|
||||||
|
|
||||||
|
53 `#[non_exhaustive]` annotations in the stdlib.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
NotFound,
|
||||||
|
PermissionDenied,
|
||||||
|
ConnectionRefused,
|
||||||
|
// ... can add more variants without breaking downstream
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Without `#[non_exhaustive]`, adding an enum variant is a breaking
|
||||||
|
change (downstream `match` statements become non-exhaustive).
|
||||||
|
With it, match statements require a wildcard arm `_ =>`, so you
|
||||||
|
can add variants freely in minor versions.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Public enums in libraries that may grow
|
||||||
|
- Error kind enums (new error types are common)
|
||||||
|
- Status/state enums that evolve over time
|
||||||
|
- Struct fields you might add to later
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
pub enum Color { Red, Green, Blue }
|
||||||
|
// Later, adding Yellow breaks ALL downstream match statements:
|
||||||
|
// error: non-exhaustive patterns: `Yellow` not covered
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Color { Red, Green, Blue }
|
||||||
|
// Downstream must have wildcard:
|
||||||
|
match color {
|
||||||
|
Color::Red => "red",
|
||||||
|
Color::Green => "green",
|
||||||
|
Color::Blue => "blue",
|
||||||
|
_ => "unknown", // required — handles future variants
|
||||||
|
}
|
||||||
|
// Adding Color::Yellow in next release is non-breaking!
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- The enum is closed (boolean-like: On/Off)
|
||||||
|
- Internal use only (pub(crate))
|
||||||
|
- You WANT exhaustive matching for safety (Option, Result)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. pub(crate) for Internal Visibility
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
919 `pub(crate)` usages in the stdlib.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub(crate) fn internal_helper() { ... }
|
||||||
|
pub(crate) struct InternalState { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
`pub(crate)` makes items visible within the crate but not to
|
||||||
|
external users. It's the middle ground between `pub` (everyone)
|
||||||
|
and private (only the module). Keeps internal architecture
|
||||||
|
flexible without exposing implementation details.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Helper functions used across modules but not part of the public API
|
||||||
|
- Internal types that multiple modules need but users shouldn't see
|
||||||
|
- Test utilities that need cross-module access
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
// Private — can't use from sibling modules
|
||||||
|
fn helper() { ... }
|
||||||
|
|
||||||
|
// Public — now it's part of your API contract forever
|
||||||
|
pub fn helper() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
// Visible to the whole crate, invisible to users
|
||||||
|
pub(crate) fn helper() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- The item is only used in one module (just make it private)
|
||||||
|
- The item should be part of the public API (use `pub`)
|
||||||
|
- You're in a binary crate (there are no external users anyway)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Constructor Pattern (fn new)
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
162 `new`/`build` methods in the stdlib.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Vec<T> {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Vec { ptr: NonNull::dangling(), len: 0, cap: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
`new()` is the standard constructor name in Rust (not `create`,
|
||||||
|
`init`, `make`). Parameterized constructors use `with_` prefix or
|
||||||
|
a builder pattern. `new()` should be cheap and infallible when
|
||||||
|
possible.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- `new()` — default or minimal construction
|
||||||
|
- `with_capacity()` — pre-allocating variant
|
||||||
|
- `from_*()` — construction from specific inputs (not From trait)
|
||||||
|
- Builder pattern — many optional parameters
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
impl Server {
|
||||||
|
pub fn create(host: &str, port: u16, threads: usize, tls: bool) -> Self { ... }
|
||||||
|
// 4 positional parameters — easy to mix up port and threads
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
impl Server {
|
||||||
|
pub fn new(addr: SocketAddr) -> Self { ... } // minimal required args
|
||||||
|
|
||||||
|
pub fn builder() -> ServerBuilder { ... } // for complex configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerBuilder {
|
||||||
|
pub fn threads(mut self, n: usize) -> Self { self.threads = n; self }
|
||||||
|
pub fn tls(mut self, enabled: bool) -> Self { self.tls = enabled; self }
|
||||||
|
pub fn build(self) -> Result<Server, ConfigError> { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- Construction can fail (use `try_new()` → Result, or builder with
|
||||||
|
fallible `build()`)
|
||||||
|
- Multiple constructors exist (use descriptive names: `from_path`,
|
||||||
|
`from_reader`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Method Chains (Consuming Self → Self)
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
Iterator adapters are the definitive example of method chaining.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let result: Vec<i32> = vec![1, 2, 3, 4, 5]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| x % 2 == 0)
|
||||||
|
.map(|x| x * x)
|
||||||
|
.collect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Method chaining creates fluent, readable APIs. Each method
|
||||||
|
consumes self and returns a new (or modified) Self. The ownership
|
||||||
|
model makes this natural — no mutation through shared references.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Builder patterns (each step returns modified builder)
|
||||||
|
- Iterator adapters (each step wraps the previous)
|
||||||
|
- Configuration objects
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
let mut query = Query::new("users");
|
||||||
|
query.set_filter("age > 18");
|
||||||
|
query.set_limit(10);
|
||||||
|
query.set_order("name");
|
||||||
|
let results = query.execute()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
let results = Query::new("users")
|
||||||
|
.filter("age > 18")
|
||||||
|
.limit(10)
|
||||||
|
.order_by("name")
|
||||||
|
.execute()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- Operations can fail mid-chain (errors get awkward)
|
||||||
|
- The object needs to be reused after each call
|
||||||
|
- It makes the code LESS readable (very long chains)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Generic Parameters with Trait Bounds
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Accept anything that can be referenced as a Path:
|
||||||
|
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> { ... }
|
||||||
|
|
||||||
|
// Accept anything comparable:
|
||||||
|
pub fn max<T: Ord>(a: T, b: T) -> T { ... }
|
||||||
|
|
||||||
|
// Multiple bounds:
|
||||||
|
pub fn sort_and_print<T: Ord + Display>(items: &mut [T]) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Generics with trait bounds give you maximum flexibility while
|
||||||
|
maintaining type safety. The function works with ANY type that
|
||||||
|
satisfies the bounds — but the compiler still checks everything
|
||||||
|
at compile time.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Function should work with multiple concrete types
|
||||||
|
- You want to accept user-defined types that implement your trait
|
||||||
|
- Performance (monomorphization — no virtual dispatch)
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
// Only works with String — can't pass &str, PathBuf, etc.
|
||||||
|
fn log_to_file(path: String, message: String) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
// Works with any type that can cheaply become &Path and &str
|
||||||
|
fn log_to_file(path: impl AsRef<Path>, message: impl AsRef<str>) {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let message = message.as_ref();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
// Now accepts: "file.txt", String::from("file.txt"), PathBuf, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- Only one type will ever be used (concrete type is simpler)
|
||||||
|
- You need dynamic dispatch (use `dyn Trait` instead)
|
||||||
|
- The generic makes the signature unreadable (too many bounds)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Typestate Pattern (Compile-Time State Machines)
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
The stdlib uses this in `File` (open modes) and throughout.
|
||||||
|
The pattern encodes state in the type system.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Different types for different states — impossible to misuse
|
||||||
|
struct Connection<State> { inner: TcpStream, _state: PhantomData<State> }
|
||||||
|
struct Connecting;
|
||||||
|
struct Connected;
|
||||||
|
struct Authenticated;
|
||||||
|
|
||||||
|
impl Connection<Connecting> {
|
||||||
|
fn connect(addr: &str) -> Result<Connection<Connected>, Error> { ... }
|
||||||
|
}
|
||||||
|
impl Connection<Connected> {
|
||||||
|
fn authenticate(self, creds: &Creds) -> Result<Connection<Authenticated>, Error> { ... }
|
||||||
|
}
|
||||||
|
impl Connection<Authenticated> {
|
||||||
|
fn query(&self, sql: &str) -> Result<Rows, Error> { ... }
|
||||||
|
}
|
||||||
|
// Can't call query() without authenticating first — compile error!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
The type system prevents invalid state transitions. You literally
|
||||||
|
cannot call methods in the wrong order because they don't exist on
|
||||||
|
that type state. Bugs become compile errors.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- State machine with ordered transitions
|
||||||
|
- Protocol sequences (connect → auth → use)
|
||||||
|
- Builder with required steps
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- States are dynamic (determined at runtime)
|
||||||
|
- There are too many states (exponential type explosion)
|
||||||
|
- The API becomes confusing (type parameters everywhere)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Extension Traits for Adding Methods
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// std adds methods to Iterator via extension traits:
|
||||||
|
pub trait IteratorExt: Iterator + Sized {
|
||||||
|
fn take_while_ref<P>(&mut self, pred: P) -> TakeWhileRef<Self, P> { ... }
|
||||||
|
}
|
||||||
|
impl<I: Iterator> IteratorExt for I {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Extension traits add methods to types you don't own (from another
|
||||||
|
crate) without modifying the original. Users must import the
|
||||||
|
extension trait to see the methods.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Adding methods to external types (from other crates)
|
||||||
|
- Optional functionality (only available when you import the trait)
|
||||||
|
- Platform-specific extensions (e.g., `std::os::unix::fs::PermissionsExt`)
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- You own the type (just add methods directly)
|
||||||
|
- A free function would be clearer
|
||||||
|
- The extension is confusing (users won't find it via autocomplete)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Newtype Pattern for Type Safety
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Newtype: single-field tuple struct
|
||||||
|
pub struct Instant(time::Instant);
|
||||||
|
pub struct Duration { secs: u64, nanos: Nanoseconds }
|
||||||
|
|
||||||
|
// Newtypes prevent mixing up semantically different values:
|
||||||
|
struct Meters(f64);
|
||||||
|
struct Seconds(f64);
|
||||||
|
// Can't accidentally pass Meters where Seconds is expected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Newtypes create distinct types with zero runtime cost (same
|
||||||
|
representation). They prevent confusing values that have the same
|
||||||
|
underlying type but different semantics.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Multiple parameters of the same primitive type (easy to swap)
|
||||||
|
- Domain concepts that should be distinct (UserID vs PostID)
|
||||||
|
- Adding methods or trait impls to foreign types
|
||||||
|
|
||||||
|
**Example — before:**
|
||||||
|
```rust
|
||||||
|
fn schedule_meeting(room_id: u64, user_id: u64, duration_mins: u64) { ... }
|
||||||
|
// Easy to accidentally swap room_id and user_id — both are u64!
|
||||||
|
schedule_meeting(user_id, room_id, 60); // compiles fine — but wrong
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example — after:**
|
||||||
|
```rust
|
||||||
|
struct RoomId(u64);
|
||||||
|
struct UserId(u64);
|
||||||
|
struct Minutes(u64);
|
||||||
|
|
||||||
|
fn schedule_meeting(room: RoomId, user: UserId, duration: Minutes) { ... }
|
||||||
|
// schedule_meeting(user_id, room_id, Minutes(60)); // COMPILE ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- Only one parameter of that type exists (no confusion possible)
|
||||||
|
- The wrapper adds no safety (just wrapping for the sake of it)
|
||||||
|
- The ergonomic cost (`.0` access, impl forwarding) outweighs benefit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Error Type Design (Per-Module Errors)
|
||||||
|
|
||||||
|
### Source:
|
||||||
|
|
||||||
|
The stdlib provides one error type per module: `io::Error`,
|
||||||
|
`fmt::Error`, `num::ParseIntError`, `str::Utf8Error`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Each module has its own error type:
|
||||||
|
pub mod io {
|
||||||
|
pub struct Error { repr: Repr }
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod num {
|
||||||
|
pub struct ParseIntError { kind: IntErrorKind }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Per-module errors keep error types focused. `io::Error` handles
|
||||||
|
all I/O errors with a kind enum + optional payload. This is more
|
||||||
|
maintainable than one giant application error enum.
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Your module is self-contained with its own failure modes
|
||||||
|
- Different modules have different error semantics
|
||||||
|
- You want to provide `type Result<T>` convenience alias
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
|
||||||
|
**Don't use this when:**
|
||||||
|
- The module is tiny (just use the parent's error type)
|
||||||
|
- Errors need to compose across modules (consider `thiserror`/`anyhow`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: API Design Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
Naming a method?
|
||||||
|
├── Returns bool? → is_*
|
||||||
|
├── Returns &T (cheap)? → as_*
|
||||||
|
├── Returns T (may allocate)? → to_*
|
||||||
|
├── Consumes self → T? → into_*
|
||||||
|
├── Constructor? → new / with_* / from_*
|
||||||
|
└── Other? → descriptive_verb
|
||||||
|
|
||||||
|
Visibility?
|
||||||
|
├── External API → pub
|
||||||
|
├── Cross-module internal → pub(crate)
|
||||||
|
└── Module-only → private (default)
|
||||||
|
|
||||||
|
Enum might grow?
|
||||||
|
└── YES → #[non_exhaustive]
|
||||||
|
|
||||||
|
Many parameters?
|
||||||
|
└── YES → Builder pattern (fn builder() → Builder)
|
||||||
|
|
||||||
|
Accept multiple types?
|
||||||
|
├── Cheap reference → impl AsRef<T>
|
||||||
|
├── Ownership transfer → impl Into<T>
|
||||||
|
└── Multiple bounds → T: Bound1 + Bound2
|
||||||
|
|
||||||
|
Prevent misuse?
|
||||||
|
├── Wrong argument order → Newtype pattern
|
||||||
|
├── Wrong method order → Typestate pattern
|
||||||
|
└── Future compatibility → #[non_exhaustive]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Pattern | Use when |
|
||||||
|
|---|---|
|
||||||
|
| `is_`/`as_`/`to_`/`into_` | Standard naming convention |
|
||||||
|
| `#[non_exhaustive]` | Enum/struct may gain variants/fields |
|
||||||
|
| `pub(crate)` | Internal cross-module access |
|
||||||
|
| `fn new` / builder | Construction |
|
||||||
|
| Method chaining | Fluent APIs (config, queries, iterators) |
|
||||||
|
| Trait bounds | Generic functions accepting multiple types |
|
||||||
|
| Typestate | Compile-time state machine enforcement |
|
||||||
|
| Extension traits | Adding methods to foreign types |
|
||||||
|
| Newtype | Type-safe wrappers around primitives |
|
||||||
|
| Per-module errors | Self-contained error types per module |
|
||||||
|
|
||||||
|
See also:
|
||||||
|
- [traits.md](traits.md) — Trait design patterns
|
||||||
|
- [error-handling.md](error-handling.md) — Error type design
|
||||||
|
- [documentation.md](documentation.md) — Documenting public APIs
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
Reference in New Issue
Block a user