πŸ¦€ Introduction to Object-Oriented Programming in Rust

Created by: Andrew J. Pounds, Ph.D.

Welcome to a comprehensive introduction to Object-Oriented Programming (OOP) concepts in Rust. While Rust is not a traditional OOP language like Java or C++, it provides powerful features that enable you to apply OOP principles effectively. This document explores how Rust approaches encapsulation, polymorphism, and code organization using structs, impl blocks, and traits. You'll learn to use both rustc (the Rust compiler) and Cargo (Rust's build system and package manager).

πŸ¦€ Rust's Philosophy on OOP

Rust takes a different approach to OOP than languages like Java or C++. Instead of class-based inheritance, Rust emphasizes:

  • Composition over Inheritance: Building complex types by combining simpler ones
  • Traits for Polymorphism: Defining shared behavior without traditional inheritance
  • Ownership and Borrowing: Memory safety without garbage collection
  • Zero-Cost Abstractions: High-level features with no runtime overhead

❓ What is Object-Oriented Programming?

Object-Oriented Programming is a paradigm that organizes code around "objects" that contain both data (variables) and functions (methods). Think of an object like a blueprint: a Drink struct defines what properties all drinks have and what operations can be performed on them.

The core principles of OOP are:

πŸ“Š Language Feature Comparison Table

Feature Java C++ Rust
Type Definition class ClassName class ClassName struct StructName
Constructor ClassName() ClassName() fn new() -> Self (convention)
Destructor Automatic (Garbage Collected) ~ClassName() impl Drop trait (automatic)
Inheritance extends : Traits + Composition (no class inheritance)
Access Modifiers public, private, protected public, private, protected pub (public) or private (default)
Interfaces interface keyword Pure virtual classes trait keyword
Polymorphism Virtual by default Must use virtual Trait objects or generics
Memory Management Automatic (Garbage Collection) Manual (new/delete) Ownership system (compile-time)
Method Definition Inside class body Inside or outside class impl StructName { } block
Self Reference this this self, &self, &mut self
Constants final keyword const keyword const keyword
Compilation javac g++ rustc or cargo build

✏️ Example 1: Basic Struct Definition

Let's compare how to define a basic "class" in all three languages:

β˜• Java
public class Drink {
    final int MAXIMUM = 10;
    private String name;
    
    public Drink(String brandName) {
        name = brandName;
    }
    
    public String getName() {
        return name;
    }
}
βš™οΈ C++
class Drink {
private:
    static const int MAXIMUM = 10;
    string name;

public:
    Drink(string brandName) {
        name = brandName;
    }
    
    string getName() {
        return name;
    }
};
πŸ¦€ Rust
const MAXIMUM: i32 = 10;

pub struct Drink {
    name: String,
}

impl Drink {
    pub fn new(brand_name: &str) -> Self {
        Drink {
            name: brand_name.to_string(),
        }
    }
    
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

πŸ” Key Observations:

  • Java: Requires access modifiers on each method; uses final for constants
  • C++: Uses separate private: and public: sections; uses static const for class constants
  • Rust: Uses struct for data and impl block for methods; fields are private by default; uses pub for public visibility; constants are defined with const outside the struct; Self refers to the implementing type

✏️ Example 2: Constructor Patterns

Rust doesn't have special constructor syntaxβ€”instead, it uses associated functions (commonly named new):

β˜• Java
public Drink(String name) {
    this.name = name;
    this.cost = 1.75;
}

public Drink(String name, double cost) {
    this.name = name;
    this.cost = cost;
}
βš™οΈ C++
Drink(string name) {
    this->name = name;
    this->cost = 1.75;
}

Drink(string name, double cost) {
    this->name = name;
    this->cost = cost;
}
πŸ¦€ Rust
impl Drink {
    // Default cost constructor
    pub fn new(name: &str) -> Self {
        Drink {
            name: name.to_string(),
            cost: 1.75,
        }
    }

    // Custom cost constructor
    pub fn with_cost(name: &str, cost: f64) -> Self {
        Drink {
            name: name.to_string(),
            cost,
        }
    }
}

πŸ” Key Observations:

  • Java: Uses this.variable to reference instance variables; supports constructor overloading
  • C++: Uses this->variable pointer notation; supports constructor overloading
  • Rust: Uses different function names instead of overloading (e.g., new vs with_cost); Self is an alias for the struct type; field shorthand (cost instead of cost: cost) when variable name matches field name

✏️ Example 3: Methods and Encapsulation

Here's how methods work in each language, using the vending machine example:

β˜• Java
public void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        System.out.println(name + " Purchased.");
    }
}

public double profit() {
    return moneyMade;
}
βš™οΈ C++
void vend() {
    if (available > 0) {
        available -= 1;
        consumed += 1;
        moneyMade += cost;
        cout << name << " Purchased." << endl;
    }
}

double profit() {
    return moneyMade;
}
πŸ¦€ Rust
pub fn vend(&mut self) {
    if self.available > 0 {
        self.available -= 1;
        self.consumed += 1;
        self.money_made += self.cost;
        println!("{} Purchased.", self.name);
    }
}

pub fn profit(&self) -> f64 {
    self.money_made
}

πŸ” Key Observations:

  • Java: Uses System.out.println() for output; implicit this
  • C++: Uses cout and endl from iostream; implicit this
  • Rust: Uses println!() macro for output; &mut self for methods that modify the struct; &self for read-only access; explicit self. prefix required; no semicolon on last expression returns that value

πŸ¦€ Understanding self in Rust

In Rust, the first parameter of a method determines how the method can access the struct:

Parameter Meaning Use Case
&self Immutable borrow Read-only methods (getters)
&mut self Mutable borrow Methods that modify the struct
self Takes ownership Methods that consume the struct
(no self) Associated function Constructors, utility functions

πŸ”§ The Complete Vending Machine Example

Now let's look at a complete, working example using Rust. This program implements a simple vending machine that tracks drink sales and profits. We'll show both the single-file approach (using rustc) and the Cargo project approach.

⚠️ Important: Rust's Module System

Rust organizes code into modules. In a single file, you can use mod blocks. For larger projects, you use separate files and Cargo.toml to manage dependencies and build configuration.

Method 1: Single File with rustc

For simple programs, you can put everything in one file and compile directly with rustc:

πŸ¦€ Rust - machine.rs (Single File)
// machine.rs
// Complete Vending Machine implementation in Rust

const MAXIMUM_CAPACITY: i32 = 10;
const DEFAULT_DRINK_COST: f64 = 1.75;

pub struct Drink {
    name: String,
    available: i32,
    consumed: i32,
    cost: f64,
    money_made: f64,
}

impl Drink {
    // Constructor with default cost
    pub fn new(brand_name: &str) -> Self {
        Drink {
            name: brand_name.to_string(),
            available: 0,
            consumed: 0,
            cost: DEFAULT_DRINK_COST,
            money_made: 0.0,
        }
    }

    // Constructor with custom cost
    pub fn with_cost(brand_name: &str, cost: f64) -> Self {
        Drink {
            name: brand_name.to_string(),
            available: 0,
            consumed: 0,
            cost,
            money_made: 0.0,
        }
    }

    // Vend a drink
    pub fn vend(&mut self) {
        if self.available > 0 {
            self.available -= 1;
            self.consumed += 1;
            self.money_made += self.cost;
            println!("{} Purchased.", self.name);
        } else {
            println!();
            println!("        SOLD OUT        ");
            println!();
        }
    }

    // Refill the drink to maximum capacity
    pub fn refill(&mut self) {
        self.available = MAXIMUM_CAPACITY;
    }

    // Get the profit (rounded to 2 decimal places)
    pub fn profit(&self) -> f64 {
        (self.money_made * 100.0).round() / 100.0
    }

    // Get the number of drinks sold
    pub fn drinks_sold(&self) -> i32 {
        self.consumed
    }

    // Get the drink name
    pub fn get_name(&self) -> &str {
        &self.name
    }

    // Get the amount needed to restock
    pub fn restock_amount(&self) -> i32 {
        MAXIMUM_CAPACITY - self.available
    }
}

fn main() {
    // Create drinks using different constructors
    let mut drink1 = Drink::new("Coke");
    let mut drink2 = Drink::new("Sprite");
    let mut drink3 = Drink::new("Doctor Pepper");
    let mut drink4 = Drink::with_cost("V8 Splash", 4.25);

    // Refill all drinks
    drink1.refill();
    drink2.refill();
    drink3.refill();
    drink4.refill();

    // Purchase 8 Cokes
    for _ in 0..8 {
        drink1.vend();
    }

    // Purchase 6 Sprites
    for _ in 0..6 {
        drink2.vend();
    }

    // Purchase 2 Doctor Peppers
    drink3.vend();
    drink3.vend();

    // Purchase 11 V8 Splash (will exceed capacity)
    for _ in 0..11 {
        drink4.vend();
    }

    // Calculate totals
    let total_sold = drink1.drinks_sold() + drink2.drinks_sold() 
                   + drink3.drinks_sold() + drink4.drinks_sold();
    let total_profit = drink1.profit() + drink2.profit() 
                     + drink3.profit() + drink4.profit();

    // Display results
    println!("A Total of {} drinks were sold for a profit of ${:.2}", 
             total_sold, total_profit);
    println!();

    println!("{} {} needed to restock machine", 
             drink1.restock_amount(), drink1.get_name());
    println!("{} {} needed to restock machine", 
             drink2.restock_amount(), drink2.get_name());
    println!("{} {} needed to restock machine", 
             drink3.restock_amount(), drink3.get_name());
    println!("{} {} needed to restock machine", 
             drink4.restock_amount(), drink4.get_name());
}

Method 2: Cargo Project (Recommended)

For larger projects, use Cargo to manage your Rust code:

βœ… The Recommended Approach

Create a Cargo project with cargo new vending_machine, then organize your code into a library module (src/lib.rs) and a binary (src/main.rs).

Project Structure:

vending_machine/
β”œβ”€β”€ Cargo.toml
└── src/
    β”œβ”€β”€ lib.rs      (library with Drink struct)
    └── main.rs     (main program)

Cargo.toml - Project Configuration

πŸ“¦ Cargo.toml
[package]
name = "vending_machine"
version = "0.1.0"
edition = "2021"
authors = ["Your Name"]

[dependencies]

src/lib.rs - The Library Module

πŸ¦€ Rust - src/lib.rs
// src/lib.rs
// Library module containing the Drink struct

pub const MAXIMUM_CAPACITY: i32 = 10;
pub const DEFAULT_DRINK_COST: f64 = 1.75;

pub struct Drink {
    name: String,
    available: i32,
    consumed: i32,
    cost: f64,
    money_made: f64,
}

impl Drink {
    /// Creates a new Drink with the default cost
    pub fn new(brand_name: &str) -> Self {
        Drink {
            name: brand_name.to_string(),
            available: 0,
            consumed: 0,
            cost: DEFAULT_DRINK_COST,
            money_made: 0.0,
        }
    }

    /// Creates a new Drink with a custom cost
    pub fn with_cost(brand_name: &str, cost: f64) -> Self {
        Drink {
            name: brand_name.to_string(),
            available: 0,
            consumed: 0,
            cost,
            money_made: 0.0,
        }
    }

    /// Vends a drink if available, otherwise displays "SOLD OUT"
    pub fn vend(&mut self) {
        if self.available > 0 {
            self.available -= 1;
            self.consumed += 1;
            self.money_made += self.cost;
            println!("{} Purchased.", self.name);
        } else {
            println!();
            println!("        SOLD OUT        ");
            println!();
        }
    }

    /// Refills the drink to maximum capacity
    pub fn refill(&mut self) {
        self.available = MAXIMUM_CAPACITY;
    }

    /// Returns the profit rounded to 2 decimal places
    pub fn profit(&self) -> f64 {
        (self.money_made * 100.0).round() / 100.0
    }

    /// Returns the number of drinks sold
    pub fn drinks_sold(&self) -> i32 {
        self.consumed
    }

    /// Returns the drink name
    pub fn get_name(&self) -> &str {
        &self.name
    }

    /// Returns the amount needed to restock
    pub fn restock_amount(&self) -> i32 {
        MAXIMUM_CAPACITY - self.available
    }
}

src/main.rs - The Main Program

πŸ¦€ Rust - src/main.rs
// src/main.rs
// Main driver program for the Vending Machine

use vending_machine::Drink;

fn main() {
    // Create drinks using different constructors
    let mut drink1 = Drink::new("Coke");
    let mut drink2 = Drink::new("Sprite");
    let mut drink3 = Drink::new("Doctor Pepper");
    let mut drink4 = Drink::with_cost("V8 Splash", 4.25);

    // Refill all drinks
    drink1.refill();
    drink2.refill();
    drink3.refill();
    drink4.refill();

    // Purchase 8 Cokes
    for _ in 0..8 {
        drink1.vend();
    }

    // Purchase 6 Sprites
    for _ in 0..6 {
        drink2.vend();
    }

    // Purchase 2 Doctor Peppers
    drink3.vend();
    drink3.vend();

    // Purchase 11 V8 Splash (will exceed capacity)
    for _ in 0..11 {
        drink4.vend();
    }

    // Calculate totals
    let total_sold = drink1.drinks_sold() + drink2.drinks_sold() 
                   + drink3.drinks_sold() + drink4.drinks_sold();
    let total_profit = drink1.profit() + drink2.profit() 
                     + drink3.profit() + drink4.profit();

    // Display results
    println!("A Total of {} drinks were sold for a profit of ${:.2}", 
             total_sold, total_profit);
    println!();

    println!("{} {} needed to restock machine", 
             drink1.restock_amount(), drink1.get_name());
    println!("{} {} needed to restock machine", 
             drink2.restock_amount(), drink2.get_name());
    println!("{} {} needed to restock machine", 
             drink3.restock_amount(), drink3.get_name());
    println!("{} {} needed to restock machine", 
             drink4.restock_amount(), drink4.get_name());
}

⚑ Compilation and Execution Instructions

Option A: Using rustc (Single File)

  1. Create machine.rs with the complete single-file code (shown above)
  2. Open a terminal and navigate to the directory containing the file
  3. Compile the program using rustc:
    rustc machine.rs
  4. Run the program:
    ./machine
    (On Windows: machine.exe)

Option B: Using Cargo (Recommended)

  1. Create a new Cargo project:
    cargo new vending_machine
    cd vending_machine
  2. Replace src/main.rs with the main program code
  3. Create src/lib.rs with the library module code
  4. Build the project:
    cargo build
  5. Run the program:
    cargo run

πŸ’‘ Cargo Commands Quick Reference

Command Description
cargo new project_name Create a new project
cargo build Compile the project (debug mode)
cargo build --release Compile with optimizations
cargo run Build and run the project
cargo check Check for errors without building
cargo test Run tests
cargo doc --open Generate and view documentation

Sample Output:

Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Coke Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Sprite Purchased. Doctor Pepper Purchased. Doctor Pepper Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. V8 Splash Purchased. SOLD OUT A Total of 26 drinks were sold for a profit of $70.50 8 Coke needed to restock machine 6 Sprite needed to restock machine 2 Doctor Pepper needed to restock machine 10 V8 Splash needed to restock machine

πŸ“š Understanding the Code

Struct and impl Block Organization

In Rust, you separate data (struct) from behavior (impl block):

Component Purpose Example
struct Drink { } Defines the data fields name, available, cost, etc.
impl Drink { } Defines methods and associated functions new(), vend(), profit(), etc.
const Compile-time constants MAXIMUM_CAPACITY, DEFAULT_DRINK_COST

Visibility and Encapsulation

Rust provides strong encapsulation through its visibility system:

βœ… This is encapsulation in Rust

The struct fields (name, available, money_made, etc.) are private. Users interact through public methods like vend(), refill(), and profit().

Ownership and Borrowing

Rust's unique ownership system ensures memory safety:

Concept Description Example
Ownership Each value has exactly one owner let drink = Drink::new("Coke");
Immutable Borrow Read-only reference &self in methods
Mutable Borrow Read-write reference &mut self in methods
Move Transfer ownership self (takes ownership)

❌ Common Mistakes and Solutions

Mistake 1: Forgetting mut for Mutable Variables

❌ Wrong:
let drink = Drink::new("Coke");
drink.vend();  // ERROR: cannot borrow `drink` as mutable
βœ… Correct:
let mut drink = Drink::new("Coke");
drink.vend();  // Now we can call methods that take &mut self

Mistake 2: Wrong Self Parameter

❌ Wrong:
impl Drink {
    pub fn vend(&self) {  // ERROR: cannot modify with &self
        self.available -= 1;
    }
}
βœ… Correct:
impl Drink {
    pub fn vend(&mut self) {  // Use &mut self to modify
        self.available -= 1;
    }
}

Mistake 3: Missing self. Prefix

❌ Wrong:
impl Drink {
    pub fn profit(&self) -> f64 {
        money_made  // ERROR: cannot find value `money_made`
    }
}
βœ… Correct:
impl Drink {
    pub fn profit(&self) -> f64 {
        self.money_made  // Access field through self
    }
}

Mistake 4: Returning References to Owned Data

❌ Wrong:
pub fn get_name(&self) -> String {
    self.name  // ERROR: cannot move out of borrowed content
}
βœ… Correct (return reference):
pub fn get_name(&self) -> &str {
    &self.name  // Return a reference (borrow)
}
βœ… Also Correct (clone):
pub fn get_name(&self) -> String {
    self.name.clone()  // Return a copy
}

Mistake 5: Forgetting the Semicolon (or Adding One When Returning)

❌ Wrong:
pub fn profit(&self) -> f64 {
    self.money_made;  // ERROR: returns () because of semicolon
}
βœ… Correct:
pub fn profit(&self) -> f64 {
    self.money_made  // No semicolon - expression is returned
}

Mistake 6: Using :: vs . Incorrectly

❌ Wrong:
let drink = Drink.new("Coke");  // ERROR: expected `::`
drink::vend();  // ERROR: expected `.`
βœ… Correct:
let mut drink = Drink::new("Coke");  // :: for associated functions
drink.vend();  // . for methods on instances

Mistake 7: String Type Confusion

❌ Wrong:
pub fn new(brand_name: String) -> Self {  // Forces caller to allocate
    // ...
}
βœ… Better (more flexible):
pub fn new(brand_name: &str) -> Self {  // Accepts &str or &String
    Drink {
        name: brand_name.to_string(),  // Convert to owned String
        // ...
    }
}

Mistake 8: Forgetting use Statement

❌ Wrong (src/main.rs):
fn main() {
    let drink = Drink::new("Coke");  // ERROR: cannot find type `Drink`
}
βœ… Correct (src/main.rs):
use vending_machine::Drink;  // Import from lib.rs

fn main() {
    let drink = Drink::new("Coke");  // Now Drink is in scope
}

✏️ Your Programming Exercise

Objective:

Modify the vending machine program to replace the V8 Splash drink with a new beverage of your choice.

Assignment:

The current program includes V8 Splash at $4.25 as the fourth drink. Your task is to:

  1. Replace V8 Splash: Change the drink name to something different (e.g., "Orange Juice", "Apple Cider", "Iced Tea", "Lemonade", etc.) and choose a new price (different from $4.25)
  2. Modify the vending operations: Change how many times each drink is purchased by modifying the loop ranges
  3. Update the output display: Ensure all the drink names and restock amounts are printed correctly
  4. Test your program: Compile with rustc or cargo run and ensure it works properly

Specific Changes to Make:

  • In main.rs (or machine.rs), change the line:
    let mut drink4 = Drink::with_cost("V8 Splash", 4.25);
    to your new drink with a new price, for example:
    let mut drink4 = Drink::with_cost("Orange Juice", 3.50);
  • Keep all four drinks in the program
  • Keep all the vend() calls (you can change the loop ranges)
  • Keep the final output section that displays totals and restocking information

Testing Your Solution:

Using rustc:

  1. Make your changes to machine.rs
  2. Compile: rustc machine.rs
  3. Run: ./machine

Using Cargo:

  1. Make your changes to src/main.rs
  2. Run: cargo run

🎯 Challenge:

Before running your program, calculate what the total profit should be. Does your output match your prediction?

πŸ¦€ Bonus Challenge:

Add a new method is_sold_out(&self) -> bool to the Drink struct that returns true if available == 0. Use this method in main to print which drinks are sold out after all purchases.

⭐ Key Takeaways About Object-Oriented Programming in Rust

  • Rust uses struct for data and impl blocks for methods
  • There is no class inheritanceβ€”use traits and composition instead
  • Constructors are just associated functions (conventionally named new)
  • Fields are private by default; use pub for public visibility
  • Use &self for read-only methods, &mut self for mutating methods
  • Associated functions (no self) are called with Struct::function()
  • Methods are called with instance.method()
  • Variables must be declared mut to be mutable
  • The last expression without a semicolon is the return value
  • Use &str for string parameters, String for owned strings
  • Use println!() macro for output with {} placeholders
  • Use rustc file.rs for single files, cargo build/run for projects
  • Cargo projects use src/lib.rs for libraries and src/main.rs for binaries
  • Import items with use crate_name::ItemName;
  • Rust's ownership system provides memory safety without garbage collection

πŸ† Language Strengths Summary

Language Strengths Best For
Java Write once, run anywhere; Strong ecosystem; Great for large teams Enterprise applications, web backends, Android apps
C++ High performance; Maximum control; Close to hardware System software, games, performance-critical apps
Rust Memory safety without GC; Zero-cost abstractions; Fearless concurrency; Modern tooling Systems programming, WebAssembly, CLI tools, embedded systems, safety-critical software

πŸ¦€ Why Choose Rust for OOP-Style Programming?