Object-Oriented Programming in Rust

January 5, 2026

What This Chapter Is Really About

Before we write any code, let's understand the question this chapter answers:

"Rust isn't Java or Python. So how do I do the OOP stuff I've heard about?"

The chapter explores three things:

  1. What does "object-oriented" even mean?
  2. Which OOP ideas does Rust support?
  3. Which OOP ideas does Rust intentionally reject (and why)?

Part 1: What Is Object-Oriented Programming?

The Confusion

Here's the thing: there's no single agreed-upon definition of OOP. Ask five programmers, get five answers.

But most people agree on three core ideas:

Concept Plain English
Encapsulation Hiding the messy details, showing only what's needed
Inheritance Creating new types based on existing types
Polymorphism One piece of code working with multiple different types

Let's explore each one individually, slowly.


Part 2: Encapsulation

What Is It?

Encapsulation means two things bundled together:

  1. Bundling data with behavior: A struct contains data, and methods operate on that data
  2. Controlling access: Deciding what outsiders can see and touch

A Real-World Analogy

Think of a coffee machine:

What you see (public interface):

What you don't see (private internals):

You don't need to understand the pump to make coffee. And the manufacturer can change the pump without you knowing, as long as the button still works.

How Rust Does Encapsulation

Rust uses one keyword: pub

The rule is simple:

Let me show you with tiny examples first.

Example: A Private Field

struct Wallet {
    money: u32,  // No pub = private
}

What does "private" mean here?

fn main() {
    let wallet = Wallet { money: 100 };  // ❌ ERROR!
}

This won't compile. Why? Because money is private. Code outside the Wallet's module cannot directly access or set money.

Example: A Public Field

struct Wallet {
    pub money: u32,  // pub = public
}

Now this works:

fn main() {
    let mut wallet = Wallet { money: 100 };  // ✅ Works
    wallet.money = 999999;  // ✅ Can modify directly
}

Why Would You Want Private Fields?

Here's the key insight: private fields let you control how data changes.

With a public field, anyone can do anything:

wallet.money = 999999;  // Cheating!
wallet.money = -50;     // Invalid state! (if it were i32)

With private fields, you force people to go through your methods:

struct Wallet {
    money: u32,  // Private
}

impl Wallet {
    // The ONLY way to create a Wallet
    pub fn new(starting_amount: u32) -> Wallet {
        Wallet { money: starting_amount }
    }
    
    // The ONLY way to see the money
    pub fn balance(&self) -> u32 {
        self.money
    }
    
    // The ONLY way to add money
    pub fn deposit(&mut self, amount: u32) {
        self.money += amount;
    }
    
    // The ONLY way to remove money (with validation!)
    pub fn withdraw(&mut self, amount: u32) -> Result<(), String> {
        if amount > self.money {
            Err(String::from("Not enough money"))
        } else {
            self.money -= amount;
            Ok(())
        }
    }
}

Now look at what this enables:

fn main() {
    let mut wallet = Wallet::new(100);
    
    // wallet.money = 999999;  // ❌ Can't cheat anymore!
    
    wallet.deposit(50);  // ✅ Must use the method
    
    let result = wallet.withdraw(200);  // Tries to withdraw too much
    // result is Err("Not enough money") - we prevented invalid state!
}

The Big Benefit of Encapsulation

Because money is private, you can change how it works internally without breaking any code that uses Wallet.

Maybe later you decide to track money in cents instead of dollars:

struct Wallet {
    cents: u64,  // Changed! But it's private, so no one outside notices
}

impl Wallet {
    pub fn balance(&self) -> u32 {
        (self.cents / 100) as u32  // Convert internally
    }
    
    // ... other methods adapt internally too
}

Everyone using Wallet keeps calling balance() and it still works. They never knew about the change.

When To Use Encapsulation

Use private fields when:

Use public fields when:


Part 3: Inheritance (Or Why Rust Says "No Thanks")

What Is Inheritance?

In traditional OOP (Java, C++, Python), inheritance means:

"I want to create a new type that's based on an existing type, automatically getting all its stuff."

Conceptually:

Animal (parent)
  - has name
  - can eat()
  - can sleep()
  
Dog (child, inherits from Animal)
  - automatically has name, eat(), sleep()
  - adds bark()

Rust's Position

Rust does not have struct inheritance.

You cannot write "struct Dog extends Animal" in Rust. It's simply not a feature.

Why Not?

The Rust designers looked at decades of OOP experience and found that inheritance often causes problems:

Problem 1: Tight Coupling

When Dog inherits from Animal, Dog becomes deeply dependent on Animal's internal structure. If Animal changes, Dog might break.

Problem 2: The Fragile Base Class Problem

Imagine Animal has 50 types inheriting from it. Now you need to change Animal. You might accidentally break 20 of those child types without realizing it.

Problem 3: Inheritance Hierarchies Get Messy

What if something is both a FlyingThing and a SwimmingThing? You end up with diamond inheritance, multiple inheritance, and complex hierarchies that are hard to understand.

What Rust Offers Instead

Rust gives you two alternatives that cover most use cases:

  1. Default trait implementations: for sharing code
  2. Trait objects: for polymorphism

Let's look at default implementations now. We'll cover trait objects in the next section.

Default Trait Implementations

You already learned traits can have default method implementations. This is Rust's way of sharing code.

trait Animal {
    // Required: each type must implement this
    fn name(&self) -> &str;
    
    // Default: types get this for free (but can override)
    fn eat(&self) {
        println!("{} is eating", self.name());
    }
    
    // Default
    fn sleep(&self) {
        println!("{} is sleeping", self.name());
    }
}

Now any type implementing Animal:

struct Dog {
    name: String,
}

impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }
    // Gets eat() and sleep() automatically!
}

struct Cat {
    name: String,
}

impl Animal for Cat {
    fn name(&self) -> &str {
        &self.name
    }
    
    // Override the default
    fn sleep(&self) {
        println!("{} sleeps with one eye open", self.name());
    }
}
fn main() {
    let dog = Dog { name: String::from("Buddy") };
    let cat = Cat { name: String::from("Whiskers") };
    
    dog.eat();   // "Buddy is eating" (default)
    dog.sleep(); // "Buddy is sleeping" (default)
    
    cat.eat();   // "Whiskers is eating" (default)
    cat.sleep(); // "Whiskers sleeps with one eye open" (overridden)
}

Inheritance vs Default Implementations

Inheritance (other languages) Default Implementations (Rust)
Child gets parent's data and methods Types get default methods only
Hierarchy: Dog IS-A Animal Relationship: Dog IMPLEMENTS Animal
Changing parent can break children Traits are more loosely coupled
Complex multiple inheritance Can implement multiple traits easily

Part 4: Polymorphism

What Is Polymorphism?

The word comes from Greek: "poly" (many) + "morph" (form) = many forms.

In programming, it means:

"Writing code that can work with multiple different types."

Why Do We Need It?

Let me show you the problem polymorphism solves.

Imagine you're building a simple drawing app. You have shapes:

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

You want each shape to draw itself:

impl Circle {
    fn draw(&self) {
        println!("Drawing a circle");
    }
}

impl Rectangle {
    fn draw(&self) {
        println!("Drawing a rectangle");
    }
}

This works fine for individual shapes:

fn main() {
    let c = Circle { radius: 5.0 };
    let r = Rectangle { width: 10.0, height: 20.0 };
    
    c.draw();  // ✅ Works
    r.draw();  // ✅ Works
}

The Problem

But what if you want to store multiple shapes together?

fn main() {
    // ❌ This doesn't work!
    let shapes = vec![
        Circle { radius: 5.0 },
        Rectangle { width: 10.0, height: 20.0 },
    ];
}

This fails because Vec can only hold one type. Circle and Rectangle are different types.

And what if you want a function that draws any shape?

// What type goes here???
fn draw_shape(shape: ???) {
    shape.draw();
}

This is the problem polymorphism solves: How do we write code that works with multiple different types?

Rust's Two Approaches

Rust gives you two ways to achieve polymorphism:

Approach Name When Type Is Known
Generics Static dispatch Compile time
Trait objects Dynamic dispatch Runtime

Let's understand each one.


Part 5: Generics (Static Dispatch): Quick Review

You already know generics from earlier chapters. Let me briefly review why they're a form of polymorphism.

The Setup

First, we define a trait:

trait Drawable {
    fn draw(&self);
}

Then we implement it for our types:

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing circle with radius {}", self.radius);
    }
}

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("Drawing rectangle {}x{}", self.width, self.height);
    }
}

Generic Function

Now we can write a function that works with any Drawable type:

fn draw_shape<T: Drawable>(shape: &T) {
    shape.draw();
}

Let's break this down:

Using It

fn main() {
    let circle = Circle { radius: 5.0 };
    let rect = Rectangle { width: 10.0, height: 20.0 };
    
    draw_shape(&circle);  // T = Circle
    draw_shape(&rect);    // T = Rectangle
}

What "Static Dispatch" Means

When you compile this code, Rust generates two separate functions:

// The compiler creates these behind the scenes:
fn draw_shape_circle(shape: &Circle) { ... }
fn draw_shape_rectangle(shape: &Rectangle) { ... }

The word "static" means the decision happens at compile time. The compiler knows exactly which function to call.

The Limitation of Generics

Generics are powerful, but they have one limitation:

The concrete type must be known at compile time.

This means you still can't do this:

fn main() {
    let circle = Circle { radius: 5.0 };
    let rect = Rectangle { width: 10.0, height: 20.0 };
    
    // ❌ Still doesn't work!
    let shapes: Vec<???> = vec![circle, rect];
}

Why? Because Vec<T> needs one specific type for T. Even with generics, Circle and Rectangle are still different types.

This is where trait objects come in.


Part 6: Trait Objects (Dynamic Dispatch): The New Concept

The Core Idea

A trait object lets you say:

"I don't care what the exact type is, as long as it implements this trait."

The Syntax

Here's how you write a trait object:

&dyn Drawable

Let's break this into pieces:

Part Meaning
& A reference (borrowing)
dyn "Dynamic": the type is determined at runtime
Drawable The trait it must implement

Using a Trait Object in a Function

fn draw_shape(shape: &dyn Drawable) {
    shape.draw();
}

Compare to the generic version:

// Generic (static dispatch)
fn draw_shape<T: Drawable>(shape: &T)

// Trait object (dynamic dispatch)  
fn draw_shape(shape: &dyn Drawable)

Both work! Both let you pass in any type that implements Drawable. But they work differently under the hood.

What "Dynamic Dispatch" Means

With trait objects, there's only one function, not multiple generated copies.

But how does Rust know which draw() to call: Circle's or Rectangle's?

At runtime, Rust looks up the correct method in something called a vtable (virtual table). It's like a lookup table:

"Hey, this thing is a Circle. Let me check the vtable... 
 ah, Circle's draw() is at this memory address. Call it!"

The word "dynamic" means the decision happens at runtime, not compile time.

Solving Our Original Problem

Now we can put different types in the same Vec!

fn main() {
    let shapes: Vec<&dyn Drawable> = vec![
        &Circle { radius: 5.0 },
        &Rectangle { width: 10.0, height: 20.0 },
    ];
    
    for shape in shapes {
        shape.draw();
    }
}

It works! The Vec holds &dyn Drawable: references to anything that implements Drawable.


Part 7: The Box<dyn Trait> Pattern

The Problem with References

The previous example used references:

let shapes: Vec<&dyn Drawable> = vec![
    &Circle { radius: 5.0 },
    // ...
];

But references have lifetimes. What if you want the Vec to own the shapes?

// ❌ This doesn't work
let shapes: Vec<dyn Drawable> = vec![...];

Why not? Because dyn Drawable has no known size. A Circle might be 8 bytes. A Rectangle might be 16 bytes. Rust needs to know sizes at compile time to allocate stack space.

The Solution: Box

Box puts data on the heap and gives you a fixed-size pointer.

let shapes: Vec<Box<dyn Drawable>> = vec![
    Box::new(Circle { radius: 5.0 }),
    Box::new(Rectangle { width: 10.0, height: 20.0 }),
];

Let's break down Box<dyn Drawable>:

Part Meaning
Box<...> A heap-allocated smart pointer (fixed size: just a pointer)
dyn Drawable Could be any type implementing Drawable

Full Example

trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("⭕ Circle, radius: {}", self.radius);
    }
}

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("🟦 Rectangle: {} x {}", self.width, self.height);
    }
}

fn main() {
    // A Vec that OWNS different shapes
    let shapes: Vec<Box<dyn Drawable>> = vec![
        Box::new(Circle { radius: 3.0 }),
        Box::new(Rectangle { width: 4.0, height: 5.0 }),
        Box::new(Circle { radius: 1.5 }),
    ];
    
    for shape in shapes.iter() {
        shape.draw();
    }
}

Output:

⭕ Circle, radius: 3
🟦 Rectangle: 4 x 5
⭕ Circle, radius: 1.5

Part 8: When To Use Which?

This is the key practical question.

Use Generics (Static Dispatch) When:

You know all types at compile time.

fn print_twice<T: Display>(item: T) {
    println!("{}", item);
    println!("{}", item);
}

Performance matters a lot.

Static dispatch has zero runtime overhead. The compiler generates optimized code for each type.

You're writing library code that should be maximally fast.

Use Trait Objects (Dynamic Dispatch) When:

You need different types in one collection.

let shapes: Vec<Box<dyn Drawable>> = vec![...];

Types are determined at runtime.

For example, a user clicks "add circle" or "add rectangle"; you don't know which at compile time.

You're building plugin systems or extensible architectures.

New types can be added without recompiling existing code.

The Trade-Off

Aspect Generics Trait Objects
Performance Faster (no lookup) Slight overhead (vtable lookup)
Binary size Larger (code duplicated per type) Smaller (one copy of code)
Flexibility Types fixed at compile time Types can vary at runtime
Collections One type per collection Mixed types in one collection

Part 9: Object Safety

The Catch

Not every trait can be used as a trait object. A trait must be "object-safe" to be used with dyn.

Why?

Think about what a trait object does: it says "I'm some type that implements this trait, but you don't know which one."

Some trait features require knowing the concrete type. Those features break the abstraction.

Rule 1: No Self in Return Types

❌ Not object-safe:

trait Cloneable {
    fn clone(&self) -> Self;
}

Why? If you have a &dyn Cloneable, what type does clone() return? The compiler doesn't know the concrete type, so it can't determine the return type's size.

✅ Object-safe alternatives:

trait Cloneable {
    fn clone(&self) -> Box<dyn Cloneable>;  // Returns a trait object
}

// Or just don't return Self
trait Drawable {
    fn draw(&self);  // Returns nothing - fine!
}

Rule 2: No Generic Type Parameters on Methods

❌ Not object-safe:

trait Processor {
    fn process<T>(&self, input: T);
}

Why? For each different T, you'd need a different entry in the vtable. But the vtable is fixed at compile time.

✅ Object-safe:

trait Processor {
    fn process_string(&self, input: String);
    fn process_number(&self, input: i32);
}

Simple Checklist

Before using dyn Trait, ask:

  1. Do any methods return Self? → Not object-safe
  2. Do any methods have generic parameters <T>? → Not object-safe
  3. Neither of those? → Object-safe ✅

Common Object-Safe Patterns

trait Drawable {
    fn draw(&self);                    // ✅ No return
    fn area(&self) -> f64;             // ✅ Returns concrete type
    fn name(&self) -> &str;            // ✅ Returns reference
    fn set_color(&mut self, c: Color); // ✅ Takes concrete parameter
}

Quick Summary So Far

Concept What It Is Rust's Approach
Encapsulation Hiding internals Private by default, pub for public
Inheritance Types based on other types No struct inheritance; use default trait methods
Polymorphism Code working with multiple types Generics (static) or trait objects (dynamic)
Syntax Meaning
&dyn Trait Borrowed trait object
Box<dyn Trait> Owned trait object
fn foo<T: Trait>(x: T) Generic with trait bound
fn foo(x: &dyn Trait) Function taking trait object

Part 10: The State Pattern

What Problem Does It Solve?

Sometimes an object needs to behave differently depending on what state it's in.

Think about a traffic light:

The traffic light is one object, but its behavior changes based on its internal state.

A Real-World Example: Blog Posts

Let's use something more relatable for programming: a blog post that goes through a workflow.

A blog post might have these states:

Draft → PendingReview → Published

And the rules are:

State What You Can Do
Draft Add/edit text, request review
PendingReview Approve or reject
Published Read the content

Here's the key insight: the same action has different results depending on the state.

The Naive Approach (And Why It's Messy)

You might think: "I'll just use an enum and match on it!"

enum PostState {
    Draft,
    PendingReview,
    Published,
}

struct Post {
    state: PostState,
    content: String,
}

impl Post {
    fn content(&self) -> &str {
        match self.state {
            PostState::Draft => "",
            PostState::PendingReview => "",
            PostState::Published => &self.content,
        }
    }
    
    fn request_review(&mut self) {
        match self.state {
            PostState::Draft => self.state = PostState::PendingReview,
            PostState::PendingReview => {},  // Already pending
            PostState::Published => {},       // Can't un-publish
        }
    }
    
    fn approve(&mut self) {
        match self.state {
            PostState::Draft => {},           // Can't approve draft
            PostState::PendingReview => self.state = PostState::Published,
            PostState::Published => {},       // Already published
        }
    }
}

The Problem With This Approach

This works, but imagine:

The code becomes a tangled mess. Every method knows about every state. Adding a new state means editing every method.

The State Pattern Solution

The State Pattern says:

"Instead of one object that changes behavior, have separate state objects that each know their own behavior."

Each state becomes its own type:

They all implement a common trait, so the main Post can hold any of them.


Part 11: Building the State Pattern Step by Step

Let's build this slowly.

Step 1: Define What All States Can Do

First, we need a trait that defines what actions exist:

trait PostState {
    fn request_review(self: Box<Self>) -> Box<dyn PostState>;
    fn approve(self: Box<Self>) -> Box<dyn PostState>;
    fn content<'a>(&self, post: &'a Post) -> &'a str;
}

Wait: there's new syntax here. Let me explain each method signature.

Understanding self: Box<Self>

fn request_review(self: Box<Self>) -> Box<dyn PostState>;

This is unusual. Let's break it down:

Part Meaning
self: Box<Self> This method takes ownership of a boxed self
-> Box<dyn PostState> Returns a (possibly different) boxed state

Why this pattern?

When you transition from Draft to PendingReview, you're consuming the old state and creating a new one. The old Draft is destroyed, and a new PendingReview takes its place.

self: Box<Self> lets us take ownership (and destroy) the current state.

Think of it like this:

Before: Post holds Box<Draft>
        ↓ request_review() consumes the Draft
After:  Post holds Box<PendingReview>

Understanding the content Signature

fn content<'a>(&self, post: &'a Post) -> &'a str;

This method:

Why pass post in?

The state objects don't store the content; the Post does. So when Published wants to return the content, it needs access to the Post.

Step 2: Define the Draft State

struct Draft {}

impl PostState for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn PostState> {
        // Draft → PendingReview
        Box::new(PendingReview {})
    }
    
    fn approve(self: Box<Self>) -> Box<dyn PostState> {
        // Can't approve a draft - stay as Draft
        self
    }
    
    fn content<'a>(&self, _post: &'a Post) -> &'a str {
        // Drafts don't show content
        ""
    }
}

Notice how Draft knows:

Step 3: Define the PendingReview State

struct PendingReview {}

impl PostState for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn PostState> {
        // Already pending - stay the same
        self
    }
    
    fn approve(self: Box<Self>) -> Box<dyn PostState> {
        // PendingReview → Published
        Box::new(Published {})
    }
    
    fn content<'a>(&self, _post: &'a Post) -> &'a str {
        // Still not showing content
        ""
    }
}

Step 4: Define the Published State

struct Published {}

impl PostState for Published {
    fn request_review(self: Box<Self>) -> Box<dyn PostState> {
        // Already published - stay the same
        self
    }
    
    fn approve(self: Box<Self>) -> Box<dyn PostState> {
        // Already published - stay the same
        self
    }
    
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        // NOW we return the actual content!
        &post.content
    }
}

This is the key moment: Only Published returns real content. The behavior is encoded in the state itself.

Step 5: Define the Post Struct

struct Post {
    state: Option<Box<dyn PostState>>,
    content: String,
}

Why Option?

This is a Rust workaround. When we transition states, we need to:

  1. Take the old state out
  2. Call a method on it
  3. Put the new state back

We can't move out of &mut self directly. Using Option lets us take() the value temporarily.

Step 6: Implement Post Methods

impl Post {
    fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
    
    fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
    
    fn content(&self) -> &str {
        // Delegate to whatever state we're in
        self.state.as_ref().unwrap().content(self)
    }
    
    fn request_review(&mut self) {
        // Take the state, transition it, put the new state back
        if let Some(state) = self.state.take() {
            self.state = Some(state.request_review());
        }
    }
    
    fn approve(&mut self) {
        if let Some(state) = self.state.take() {
            self.state = Some(state.approve());
        }
    }
}

Understanding the State Transition Pattern

Let's zoom in on this:

fn request_review(&mut self) {
    if let Some(state) = self.state.take() {
        self.state = Some(state.request_review());
    }
}

Step by step:

  1. self.state.take(): Removes the state from the Option, leaving None temporarily
  2. state.request_review(): Consumes old state, returns new state
  3. self.state = Some(...): Puts the new state back

It's like swapping out a battery:

  1. Remove old battery
  2. Old battery transforms into new battery (magically)
  3. Insert new battery

Part 12: Seeing It Work

fn main() {
    let mut post = Post::new();
    
    // State: Draft
    post.add_text("Hello, this is my blog post!");
    println!("Content: '{}'", post.content());  // Empty!
    
    // State: Draft → PendingReview
    post.request_review();
    println!("Content: '{}'", post.content());  // Still empty!
    
    // State: PendingReview → Published
    post.approve();
    println!("Content: '{}'", post.content());  // Now shows content!
}

Output:

Content: ''
Content: ''
Content: 'Hello, this is my blog post!'

What Just Happened?

The Post doesn't have any match statements or if checks for state. It just delegates to whatever state it currently holds:

fn content(&self) -> &str {
    self.state.as_ref().unwrap().content(self)
}

The state object itself decides the behavior. This is the essence of the State Pattern.


Part 13: Why Is This Better?

Benefit 1: Each State Is Self-Contained

All the logic for "what does Draft do?" is in the Draft struct. You don't have to hunt through giant match statements.

Benefit 2: Adding New States Is Easy

Want to add a "Rejected" state?

struct Rejected {}

impl PostState for Rejected {
    fn request_review(self: Box<Self>) -> Box<dyn PostState> {
        // Can go back to pending
        Box::new(PendingReview {})
    }
    
    fn approve(self: Box<Self>) -> Box<dyn PostState> {
        self  // Can't approve rejected post
    }
    
    fn content<'a>(&self, _post: &'a Post) -> &'a str {
        ""
    }
}

You add a new struct. You don't touch any existing code.

Benefit 3: Invalid Transitions Are Handled Gracefully

What if you call approve() on a Draft? In the naive approach, you need to remember to handle it.

With the State Pattern, Draft::approve simply returns self, staying in Draft. The state itself enforces the rules.


Part 14: An Alternative: Type-State Pattern

There's another way to model states in Rust: using the type system itself.

Instead of one Post type with internal state, you have separate types for each state:

struct DraftPost {
    content: String,
}

struct PendingReviewPost {
    content: String,
}

struct PublishedPost {
    content: String,
}

Different Types, Different Methods

impl DraftPost {
    fn new() -> DraftPost {
        DraftPost { content: String::new() }
    }
    
    fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
    
    // Returns a DIFFERENT TYPE
    fn request_review(self) -> PendingReviewPost {
        PendingReviewPost { content: self.content }
    }
}

impl PendingReviewPost {
    // Returns a DIFFERENT TYPE
    fn approve(self) -> PublishedPost {
        PublishedPost { content: self.content }
    }
}

impl PublishedPost {
    fn content(&self) -> &str {
        &self.content
    }
}

The Key Difference

Notice: only PublishedPost has a content() method.

fn main() {
    let mut post = DraftPost::new();
    post.add_text("My blog post");
    
    // post.content();  // ❌ WON'T COMPILE - DraftPost has no content()
    
    let post = post.request_review();
    // post.content();  // ❌ WON'T COMPILE - PendingReviewPost has no content()
    
    let post = post.approve();
    println!("{}", post.content());  // ✅ Works!
}

Compile-Time Enforcement

With this approach, invalid operations won't even compile.

The compiler catches bugs, not runtime behavior.


Part 15: Comparing the Two Approaches

Aspect Trait Objects (State Pattern) Type-State Pattern
Invalid operations Handled at runtime (returns empty, does nothing) Won't compile
Single collection Yes: Vec<Box<dyn PostState>> possible No: different types can't mix
Adding new states Easy: new struct, implement trait New type + new methods
Flexibility More flexible (runtime decisions) More rigid but safer
When types change Same variable can change state Variable type changes (rebinding)

Type-State Example of Rebinding

Notice how the variable gets rebound:

let post = DraftPost::new();           // post: DraftPost
let post = post.request_review();       // post: PendingReviewPost (shadows old)
let post = post.approve();              // post: PublishedPost (shadows old)

Each let post = shadows the previous one with a new type.


Part 16: When To Use Which?

Use Trait Objects (State Pattern) When:

States can change dynamically at runtime.

Example: A game character that can switch between "Idle", "Walking", "Running", "Attacking" based on player input.

You need to store multiple items with different states together.

let posts: Vec<Box<dyn PostState>> = vec![...];

The workflow isn't strictly linear.

Example: Posts can go Draft → Review → Rejected → Draft → Review → Published (complex transitions).

Use Type-State Pattern When:

You want compile-time guarantees.

"It should be impossible to read content from an unpublished post": enforced by the compiler.

The workflow is linear and predictable.

Draft → Review → Published, no going back.

Maximum safety is more important than flexibility.


Part 17: Summary of Chapter 18

Let's recap everything:

The Three OOP Concepts

Concept Traditional OOP Rust's Approach
Encapsulation Classes with private fields Structs with private fields, pub keyword
Inheritance Class extends another class No struct inheritance; use default trait methods
Polymorphism Base class references Generics (static) or dyn Trait (dynamic)

Trait Objects

Syntax What It Means
dyn Trait A trait object (unknown concrete type)
&dyn Trait Borrowed trait object
Box<dyn Trait> Owned trait object on heap
self: Box<Self> Method that consumes boxed self

Object Safety Rules

A trait is object-safe if:

When To Use What

Situation Use
Types known at compile time, need speed Generics
Different types in one collection Trait objects
Types determined at runtime Trait objects
Want compiler to prevent invalid states Type-state pattern
Complex state transitions at runtime State pattern with trait objects

Chapter 18 Practice Exercises

Exercise 1: Encapsulation

Create a BankAccount struct with a private balance field. Provide methods to:

Users should NOT be able to modify balance directly.

// Your code here

fn main() {
    let mut account = BankAccount::new(100.0);
    println!("Balance: ${}", account.balance()); // 100
    
    account.deposit(50.0);
    println!("Balance: ${}", account.balance()); // 150
    
    account.deposit(-20.0); // Should do nothing (negative)
    println!("Balance: ${}", account.balance()); // Still 150
}

Exercise 2: Default Trait Implementations

Create an Animal trait with:

Create two structs:

// Your code here

fn main() {
    let dog = Dog::new("Buddy");
    let cat = Cat::new("Whiskers");
    
    dog.speak(); // Buddy says woof!
    cat.speak(); // Whiskers makes a sound
}

Exercise 3: Trait Objects with Box<dyn Trait>

Create a Shape trait with a method area(&self) -> f64.

Implement it for Circle (has radius) and Square (has side).

Create a Vec<Box<dyn Shape>> containing different shapes and print each area.

// Your code here

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle::new(2.0)),  // area = π * 4 ≈ 12.57
        Box::new(Square::new(3.0)),  // area = 9
        Box::new(Circle::new(1.0)),  // area = π ≈ 3.14
    ];
    
    for shape in shapes.iter() {
        println!("Area: {:.2}", shape.area());
    }
}

Exercise 4: Object Safety

Look at these two traits. One is object-safe, one is not.

trait Cloneable {
    fn clone_self(&self) -> Self;
}

trait Displayable {
    fn display(&self) -> String;
}

Task:

  1. Which trait is object-safe and why?
  2. Which trait is NOT object-safe and why?
  3. Write code that proves your answer by creating Box<dyn TraitName> for the object-safe one.

Exercise 5: Simple State Pattern

Create a Door that can be Open or Closed.

Use trait objects for the state pattern.

// Your code here

fn main() {
    let mut door = Door::new(); // Starts Closed
    
    println!("Door open? {}", door.is_open()); // false
    
    door.toggle();
    println!("Door open? {}", door.is_open()); // true
    
    door.toggle();
    println!("Door open? {}", door.is_open()); // false
}