Struct Design and Encapsulation
Rule
Model meaningful concepts as structs with private fields and behavior-bearing methods; use public fields only for plain data without invariants.
Why
Private fields let a type control construction and mutation, while methods make ownership and behavior explicit.
Do
- Keep invariant-bearing fields private.
- Put behavior on the type that owns the required data.
- Use
&selffor observation,&mut selffor mutation, andselffor consuming transitions. - Expose only the read accessors callers need.
- Use
pub(crate)only for real internal module boundaries. - Use public fields for DTOs, configuration records, snapshots, and other intentional plain data.
Avoid
- Do not make fields public to avoid constructors or accessors.
- Do not put methods on wrappers that do not own the relevant data or invariant.
- Do not generate getters and setters for every field.
- Do not expose production mutation hooks solely for tests.
- Do not split natural behavior into unrelated helper modules.
Public API Notes
Public fields prevent constructors and destructuring from evolving independently. Published invariant-bearing types therefore keep fields private.
Example
#![allow(unused)]
fn main() {
pub struct UserAccount {
email: EmailAddress,
active: bool,
}
impl UserAccount {
pub fn email(&self) -> &EmailAddress {
&self.email
}
pub fn deactivate(&mut self) {
self.active = false;
}
}
pub struct UserSummary {
pub email: EmailAddress,
pub active: bool,
}
}
Exceptions
- Use tuple structs for small semantic wrappers as described by newtypes.
- Use free functions for algorithms with no natural owner type.