Skip to content

Latest commit

 

History

History
169 lines (117 loc) · 6.02 KB

modeling.md

File metadata and controls

169 lines (117 loc) · 6.02 KB

Overview

  1. How to make rustc enforce your invariants at compile time
  2. See abstraction doc
  3. For comparison, see the golang version of this doc

Key Concepts

  1. Goal: Make illegal states in the domain model unrepresentable (compiler enforced)
  2. Languages with strong modeling tools represent invariants with types
    1. Compiler is responsible for enforcing invariants
  3. Languages with weak modeling tools represent invariants with methods/logic
    1. Runtime is responsible for enforcing invariants

Patterns

Absence

  1. Use None
let favorite_book: Option<Book> = None;
  1. NOTE: safe rust never allows null pointers

At-most-one (aka "Maybe")

  1. Use Option
struct Employee {
    favorite_book: Option<Book>, // at most one
}

Zero-or-more

  1. Use Vec
struct Employee {
    previous_employers: Vec<Employer>, // zero or more
    ...
}

Exactly-one

  1. Use a field on a struct or enum
struct Employee {
    id: u16,      // exactly one
    name: String, // exactly one
    ...
}

Exactly-one of bounded set (Sum Algebraic data type)

  1. Use an enum
  2. Use Pattern matching, more info

At-least-one

  • In "Algebraic type" language
    • Product type with restrictions
    • Sum type with combinations

... of the same thing

... of different things

  • struct/tuple with optional fields with runtime enforcement
  • bit fields
  • builder struct + validated impl struct hidden behind pub trait

One of Two peers

Non-negative value

  1. Use u8, u16, u32, u64, or u128
  2. rustc will enforce range on numeric literals
let age: u8 = 30; // compiler enforced non-negative

Floats in specific range

Group of related fields (Product Algebraic data type)

  1. Use a struct
  2. (Less common) Use a tuple
  1. Use a field on a struct or enum
struct Employee {
    department: Department,      // has-a
    ...
}
  1. Implement a trait

Visibility/Encapsulation

Mutability

  1. Prefer immutability, Use mut (only when required)
    1. Clone might be applicable here too (rust's version of a defensive copy)
  1. See fearless concurrency doc
  2. Send and Sync marker traits may help

Augmenting external types (new methods)

TypeState pattern

  1. Goal: make illegal state unrepresentable
    • not always possible
  2. Steps:
    1. Draw out the state transition diagram
    2. Make struct for each state
    3. Add methods on each (non-terminal) state struct for each state transition
    4. each transition method must consume self
      • guarantees transitions happen at-most once
    5. limit how intermediate & terminal state structs are created
    6. If you must manage lifecycle in a field, wrap field in an Rc

Things the type system cannot directly enforce

  1. Each of these requires imperative validation code in a ::new or builder

Cases

  1. Given: struct Foo {a :u32, b :u32}, ensure a < b
  2. Given: struct Foo {left :T, right :T}, ensure a != b
  3. Ensure Path represents a file (or a directory), fs might change over time
  4. Ensure Path exists, fs might change over time
  5. Ensure Collection has at least one value
  6. Ensure u8 is under 100 (and similar numeric constraints with the representable numeric type)
  7. Ensure string matches regex (and similar constraints like "contains x", "is trimmed", "starts with", "has length", etc)
  8. Ensure csv string has no duplicates

Other Resources

  1. https://markoengelman.com/ddd-anemic-vs-rich-domain-model/
  2. https://doc.rust-lang.org/std/option/
  3. https://hugotunius.se/2020/05/16/making-invalid-state-unrepresentable.html