- How to make rustc enforce your invariants at compile time
- See abstraction doc
- For comparison, see the golang version of this doc
- Goal: Make illegal states in the domain model unrepresentable (compiler enforced)
- Languages with strong modeling tools represent invariants with types
- Compiler is responsible for enforcing invariants
- Languages with weak modeling tools represent invariants with methods/logic
- Runtime is responsible for enforcing invariants
- Use
None
let favorite_book: Option<Book> = None;
- NOTE: safe rust never allows null pointers
At-most-one (aka "Maybe")
- Use
Option
struct Employee {
favorite_book: Option<Book>, // at most one
}
- Use
Vec
struct Employee {
previous_employers: Vec<Employer>, // zero or more
...
}
struct Employee {
id: u16, // exactly one
name: String, // exactly one
...
}
Exactly-one of bounded set (Sum Algebraic data type)
- Use an
enum
- Use Pattern matching, more info
- In "Algebraic type" language
- Product type with restrictions
- Sum type with combinations
- non empty vec library
- Vec with runtime enforcement
- struct/tuple with optional fields with runtime enforcement
- bit fields
- builder struct + validated impl struct hidden behind pub trait
let age: u8 = 30; // compiler enforced non-negative
Group of related fields (Product Algebraic data type)
struct Employee {
department: Department, // has-a
...
}
- Implement a trait
- See modules doc
- Prefer immutability, Use mut (only when required)
Clone
might be applicable here too (rust's version of a defensive copy)
- See fearless concurrency doc
- Send and Sync marker traits may help
- See extensions doc
- Goal: make illegal state unrepresentable
- not always possible
- Steps:
- Draw out the state transition diagram
- Make
struct
for each state - Add methods on each (non-terminal) state struct for each state transition
- each transition method must consume
self
- guarantees transitions happen at-most once
- limit how intermediate & terminal state structs are created
::new
method consumes previous state struct- or
#[non_exhaustive]
- or private
PhantomData
field on intermediate state struct
- If you must manage lifecycle in a field, wrap field in an
Rc
- Each of these requires imperative validation code in a
::new
or builder
- Given:
struct Foo {a :u32, b :u32}
, ensurea < b
- Given:
struct Foo {left :T, right :T}
, ensurea != b
- Ensure Path represents a file (or a directory), fs might change over time
- Ensure Path exists, fs might change over time
- Ensure Collection has at least one value
- Ensure u8 is under 100 (and similar numeric constraints with the representable numeric type)
- Ensure string matches regex (and similar constraints like "contains x", "is trimmed", "starts with", "has length", etc)
- Ensure csv string has no duplicates