e1c2caf8d5
10 patterns, 514 lines. Full spec compliance. Patterns: one type per file, pub use re-exports, prelude module, feature gates, API/impl separation, test organization, type aliases, workspace multi-crate, conditional compilation, error modules.
515 lines
13 KiB
Markdown
515 lines
13 KiB
Markdown
# Rust Module Organization Patterns
|
|
|
|
Patterns for organizing code into modules, 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:** 220 mod.rs files, 41 lib.rs files, 1,404 module
|
|
declarations, 919 pub(crate) items, 18,430 pub fn.
|
|
|
|
---
|
|
|
|
## 1. One Type Per File (Large Types)
|
|
|
|
### Source:
|
|
|
|
[library/alloc/src/string.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/string.rs) (String), [library/alloc/src/vec/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/vec/mod.rs) (Vec)
|
|
|
|
```text
|
|
alloc/src/
|
|
├── string.rs ← String type + all impls (3000+ lines)
|
|
├── vec/
|
|
│ ├── mod.rs ← Vec type + core methods
|
|
│ ├── drain.rs ← Drain iterator
|
|
│ ├── into_iter.rs ← IntoIter
|
|
│ ├── splice.rs ← Splice iterator
|
|
│ └── ...
|
|
└── collections/
|
|
├── btree/
|
|
│ ├── map.rs
|
|
│ ├── set.rs
|
|
│ └── ...
|
|
└── ...
|
|
```
|
|
|
|
### Why
|
|
|
|
Large types get their own file (or directory). This keeps files
|
|
navigable. When a type has many associated iterators or helpers,
|
|
it becomes a directory with `mod.rs`. The split is by related type,
|
|
not by "kind" (don't put all iterators in `iterators.rs`).
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- A type + impls exceeds ~500 lines
|
|
- A type has multiple associated types (iterators, builders)
|
|
- Multiple people work on the same module (reduce conflicts)
|
|
|
|
**Example — before:**
|
|
```rust
|
|
// src/collections.rs — 5000 lines, everything jammed together
|
|
pub struct HashMap { ... }
|
|
impl HashMap { /* 800 lines */ }
|
|
pub struct HashSet { ... }
|
|
impl HashSet { /* 600 lines */ }
|
|
pub struct BTreeMap { ... }
|
|
// Impossible to navigate
|
|
```
|
|
|
|
**Example — after:**
|
|
```rust
|
|
// src/collections/mod.rs
|
|
pub mod hash_map;
|
|
pub mod hash_set;
|
|
pub mod btree_map;
|
|
|
|
// src/collections/hash_map.rs
|
|
pub struct HashMap { ... }
|
|
impl HashMap { ... }
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The type is small (<100 lines with impls)
|
|
- Related types are tightly coupled and small (keep together)
|
|
|
|
---
|
|
|
|
## 2. Re-exports for Clean Public API (pub use)
|
|
|
|
### Source:
|
|
|
|
[library/std/src/lib.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/lib.rs)
|
|
|
|
```rust
|
|
// Internal organization:
|
|
mod io;
|
|
mod fs;
|
|
mod net;
|
|
|
|
// Public API — re-export the important bits:
|
|
pub use io::{Read, Write, BufRead, Seek};
|
|
pub use fs::{File, OpenOptions};
|
|
```
|
|
|
|
### Why
|
|
|
|
Internal module structure serves implementation needs. Public API
|
|
structure serves user needs. They're often different. `pub use`
|
|
lets you organize internally however makes sense while presenting
|
|
a clean, flat API to users.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Internal modules are deeply nested but users want flat access
|
|
- You want `use my_crate::ImportantType` instead of
|
|
`use my_crate::internal::deep::nested::ImportantType`
|
|
- Reorganizing internals without breaking the public API
|
|
|
|
**Example — before:**
|
|
```rust
|
|
// Users must know internal structure:
|
|
use my_crate::network::http::client::HttpClient;
|
|
use my_crate::network::http::request::Request;
|
|
use my_crate::network::http::response::Response;
|
|
```
|
|
|
|
**Example — after:**
|
|
```rust
|
|
// lib.rs re-exports at the crate root:
|
|
pub use network::http::client::HttpClient;
|
|
pub use network::http::request::Request;
|
|
pub use network::http::response::Response;
|
|
|
|
// Users just write:
|
|
use my_crate::{HttpClient, Request, Response};
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The module hierarchy IS the API (users should know about it)
|
|
- Re-exports would cause name collisions
|
|
- You're re-exporting too much (defeats the purpose of modules)
|
|
|
|
---
|
|
|
|
## 3. Prelude Module for Common Imports
|
|
|
|
### Source:
|
|
|
|
[library/std/src/prelude/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/prelude/mod.rs)
|
|
|
|
```rust
|
|
pub mod prelude {
|
|
pub mod v1 {
|
|
pub use crate::option::Option::{self, Some, None};
|
|
pub use crate::result::Result::{self, Ok, Err};
|
|
pub use crate::iter::Iterator;
|
|
pub use crate::clone::Clone;
|
|
// ... most commonly used traits and types
|
|
}
|
|
}
|
|
```
|
|
|
|
### Why
|
|
|
|
A prelude gathers the most commonly used items so users can do
|
|
`use my_crate::prelude::*` instead of 20 individual imports. The
|
|
stdlib prelude is auto-imported; library preludes are opt-in.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your library has 5+ types/traits that users import together
|
|
- Most users need the same set of imports
|
|
- Reducing boilerplate matters for ergonomics
|
|
|
|
**Example — before:**
|
|
```rust
|
|
// Every file in the project:
|
|
use my_framework::{App, Context, Request, Response, Handler};
|
|
use my_framework::middleware::{Logger, Cors, Auth};
|
|
use my_framework::error::Error;
|
|
// 3 lines of imports every single time
|
|
```
|
|
|
|
**Example — after:**
|
|
```rust
|
|
// my_framework/src/prelude.rs
|
|
pub use crate::{App, Context, Request, Response, Handler};
|
|
pub use crate::middleware::{Logger, Cors, Auth};
|
|
pub use crate::error::Error;
|
|
|
|
// Every file in the project:
|
|
use my_framework::prelude::*;
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The library is small (few imports anyway)
|
|
- Items would conflict with common names (String, Error)
|
|
- You want explicit imports for clarity
|
|
|
|
---
|
|
|
|
## 4. Feature Gates (#[cfg(feature = "...")])
|
|
|
|
### Source:
|
|
|
|
The stdlib uses extensive feature gating for unstable features.
|
|
Libraries use it for optional dependencies.
|
|
|
|
```rust
|
|
#[cfg(feature = "serde")]
|
|
impl Serialize for MyType { ... }
|
|
|
|
#[cfg(feature = "async")]
|
|
pub mod async_client { ... }
|
|
```
|
|
|
|
### Why
|
|
|
|
Feature gates let you provide optional functionality without
|
|
imposing dependencies on all users. Users opt in via Cargo.toml.
|
|
This keeps the default compilation fast and the dependency tree
|
|
small.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Optional dependencies (serde, tokio, etc.)
|
|
- Optional functionality that not all users need
|
|
- Platform-specific code
|
|
- Unstable/experimental APIs
|
|
|
|
**Example — before:**
|
|
```rust
|
|
// Everyone pays the compile cost of serde even if they don't use it
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Config { ... }
|
|
```
|
|
|
|
**Example — after:**
|
|
```rust
|
|
// Cargo.toml: [features] serde = ["dep:serde"]
|
|
#[derive(Debug, Clone)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
pub struct Config { ... }
|
|
// Only compiled with serde when the feature is enabled
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The functionality is core (everyone needs it)
|
|
- Feature interaction is complex (too many combinations to test)
|
|
- The dependency is tiny (not worth the complexity)
|
|
|
|
---
|
|
|
|
## 5. Separating Public API from Implementation
|
|
|
|
### Source:
|
|
|
|
```text
|
|
std/src/io/
|
|
├── mod.rs ← public API, re-exports, module docs
|
|
├── buffered/ ← implementation details
|
|
│ ├── bufreader.rs
|
|
│ ├── bufwriter.rs
|
|
│ └── linewriter.rs
|
|
├── cursor.rs ← Cursor implementation
|
|
├── error.rs ← Error types
|
|
└── impls.rs ← trait implementations
|
|
```
|
|
|
|
### Why
|
|
|
|
The `mod.rs` (or parent module) defines WHAT users see. Sub-modules
|
|
contain HOW things work. This separation means you can refactor
|
|
internals without changing the public API.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Module has both public API and complex implementation
|
|
- You want to reorganize implementation without breaking users
|
|
- The implementation has multiple sub-components
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The module is simple (one file is fine)
|
|
- Implementation IS the API (no separation needed)
|
|
|
|
---
|
|
|
|
## 6. tests/ Subdirectory for Test Organization
|
|
|
|
### Source:
|
|
|
|
```text
|
|
library/
|
|
├── core/
|
|
│ └── src/
|
|
│ └── option.rs ← implementation
|
|
├── coretests/
|
|
│ └── tests/
|
|
│ └── option.rs ← tests for core::option
|
|
└── alloctests/
|
|
└── tests/
|
|
└── vec.rs ← tests for alloc::vec
|
|
```
|
|
|
|
### Why
|
|
|
|
The stdlib separates tests into their own crates (`coretests`,
|
|
`alloctests`) because `core` can't depend on `alloc` for testing.
|
|
For normal projects, `tests/` directory provides the same separation
|
|
for integration tests.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Integration tests (testing the public API)
|
|
- Tests that need external resources
|
|
- Tests that should be separate binaries (parallelism)
|
|
|
|
---
|
|
|
|
## 7. Type Aliases for Complex Generics
|
|
|
|
### Source:
|
|
|
|
```rust
|
|
// Instead of writing Result<T, io::Error> everywhere:
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
// Instead of writing Box<dyn Fn(Request) -> Response + Send + Sync>:
|
|
pub type Handler = Box<dyn Fn(Request) -> Response + Send + Sync>;
|
|
```
|
|
|
|
### Why
|
|
|
|
Type aliases reduce visual noise for complex generic types. They
|
|
make function signatures readable and provide a single point of
|
|
change if the underlying type evolves.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- A generic type appears in 3+ signatures
|
|
- The full type is > 40 characters
|
|
- You want a domain-specific name for a generic composition
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The alias hides important information (what IS that thing?)
|
|
- The alias is used in only one place
|
|
- The original type is already short and clear
|
|
|
|
---
|
|
|
|
## 8. Workspace Organization (Multi-Crate Projects)
|
|
|
|
### Source:
|
|
|
|
The Rust stdlib itself is organized as multiple crates:
|
|
|
|
```text
|
|
library/
|
|
├── core/ ← no allocator, no OS (can run on bare metal)
|
|
├── alloc/ ← needs an allocator (Vec, String, Box)
|
|
├── std/ ← needs an OS (files, threads, networking)
|
|
└── proc_macro/ ← procedural macro support
|
|
```
|
|
|
|
### Why
|
|
|
|
Multiple crates within one workspace provide:
|
|
- Separate compilation (parallel builds)
|
|
- Clear dependency boundaries (core doesn't depend on alloc)
|
|
- Different feature sets per crate
|
|
- Independent versioning
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Different parts of your project have different dependencies
|
|
- You want parallel compilation (workspace members build in parallel)
|
|
- Some code is reusable independently (separate crate = separate publish)
|
|
- Clear architectural boundaries
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The project is small (<2000 lines)
|
|
- Everything depends on everything else (no natural boundaries)
|
|
- The complexity of multiple crates outweighs the benefits
|
|
|
|
---
|
|
|
|
## 9. Conditional Compilation (#[cfg])
|
|
|
|
### Source:
|
|
|
|
Extensive use of `#[cfg(target_os)]`, `#[cfg(feature)]`,
|
|
`#[cfg(test)]` throughout the stdlib.
|
|
|
|
```rust
|
|
#[cfg(unix)]
|
|
mod unix_impl;
|
|
#[cfg(windows)]
|
|
mod windows_impl;
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
|
type Repr = repr_bitpacked::Repr;
|
|
#[cfg(not(target_pointer_width = "64"))]
|
|
type Repr = repr_unpacked::Repr;
|
|
```
|
|
|
|
### Why
|
|
|
|
`#[cfg]` compiles code conditionally. Dead code is eliminated at
|
|
compile time — no runtime cost. This is how one codebase supports
|
|
multiple platforms, feature sets, and configurations.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Platform-specific implementations (OS, architecture)
|
|
- Feature-gated code
|
|
- Test-only code (`#[cfg(test)]`)
|
|
- Debug-only code (`#[cfg(debug_assertions)]`)
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- Runtime feature detection is needed (cfg is compile-time only)
|
|
- The conditional logic is simple enough for `if cfg!(...)` macro
|
|
- You're using cfg to hide broken code (fix it instead)
|
|
|
|
---
|
|
|
|
## 10. Error Modules
|
|
|
|
### Source:
|
|
|
|
```text
|
|
std/src/io/
|
|
├── error.rs ← Error type definition
|
|
├── error/
|
|
│ ├── repr_bitpacked.rs ← compact representation
|
|
│ └── repr_unpacked.rs ← standard representation
|
|
└── mod.rs ← re-exports Error
|
|
```
|
|
|
|
### Why
|
|
|
|
Error types often need their own module when they have:
|
|
- Multiple representations
|
|
- Many From implementations
|
|
- Complex display logic
|
|
- Associated helper types
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Error type exceeds 100 lines
|
|
- Error has multiple variants with different data
|
|
- Error needs platform-specific representations
|
|
|
|
---
|
|
|
|
## Summary: Module Organization Decision Tree
|
|
|
|
```
|
|
How big is the module?
|
|
├── <200 lines → single file
|
|
├── 200-1000 lines → file with sections
|
|
└── >1000 lines → directory (mod.rs + sub-modules)
|
|
|
|
What should users see?
|
|
├── Everything → pub mod (expose structure)
|
|
├── Flat API → pub use (re-export from depth)
|
|
└── Common subset → prelude module
|
|
|
|
Platform-specific code?
|
|
├── cfg(unix) / cfg(windows) → parallel mod files
|
|
└── cfg(feature) → optional modules
|
|
|
|
Test organization?
|
|
├── Unit tests → #[cfg(test)] mod tests (same file)
|
|
├── Integration → tests/ directory
|
|
└── Shared helpers → tests/common/mod.rs
|
|
```
|
|
|
|
| Pattern | Use when |
|
|
|---|---|
|
|
| One type per file | Large types (>500 lines with impls) |
|
|
| `pub use` re-exports | Clean API surface hiding internal structure |
|
|
| Prelude module | 5+ commonly imported items |
|
|
| Feature gates | Optional functionality/dependencies |
|
|
| API/impl separation | Public surface ≠ implementation structure |
|
|
| `tests/` directory | Integration testing |
|
|
| Type aliases | Complex generics appearing multiple times |
|
|
| Workspace (multi-crate) | Clear architectural boundaries |
|
|
| `#[cfg]` | Platform/feature conditional compilation |
|
|
| Error modules | Complex error types |
|
|
|
|
See also:
|
|
- [api-design.md](api-design.md) — Public API design patterns
|
|
- [documentation.md](documentation.md) — Module-level documentation
|
|
|
|
<!-- PATTERN_COMPLETE -->
|