File: README.md
This repository contains resources to learn Low Level Design (LLD) / Object Oriented Design (OOD) and prepare for interviews. It covers OOP fundamentals, design patterns, UML, concurrency and commonly asked interview questions.
๐ For a better and more comprehensive experience, checkout the LLD page at AlgoMaster.io
๐งฑ OOP Fundamentals
๐ Class Relationships
๐งญ Design Principles
- DRY Principle
- YAGNI Principle
- KISS Principle
- SOLID Principles with Pictures
- SOLID Principles with Code
๐งฉ Design Patterns
| Creational Patterns | Structural Patterns | Behavioral Patterns |
|---|---|---|
| Singleton | Adapter | Iterator |
| Factory Method | Bridge | Observer |
| Abstract Factory | Composite | Strategy |
| Builder | Decorator | Command |
| Prototype | Facade | State |
| Flyweight | Template Method | |
| Proxy | Visitor | |
| Mediator | ||
| Memento | ||
| Chain of Responsibility |
๐๏ธ UML
โฑ๏ธ Concurrency and Multi-threading Concepts
Concurrency 101
- Introduction to Concurrency
- Concurrency vs Parallelism
- Processes vs Threads
- Thread Lifecycle and States
- Race Conditions and Critical Sections
Synchronization Primitives
- Mutex (Mutual Exclusion)
- Semaphores
- Condition Variables
- Coarse-grained vs Fine-grained Locking
- Reentrant Locks
- Try-Lock and Timed Locking
- Compare-and-Swap (CAS)
Concurrency Challenges
Concurrency Patterns
โ [How to Answer a LLD Interview Problem](https://blog.algomaster.io/p/how-to-answer-a-lld-interview-problem)
๐ป Low Level Design Interview Problems
Easy Problems
- Design Parking Lot
- Design Stack Overflow
- Design a Vending Machine
- Design Logging Framework
- Design Traffic Signal Control System
- Design Coffee Vending Machine
- Design a Task Management System
Medium Problems
- Design ATM
- Design LinkedIn
- Design LRU Cache
- Design Tic Tac Toe Game
- Design Pub Sub System
- Design an Elevator System
- Design Car Rental System
- Design an Online Auction System
- Design Hotel Management System
- Design a Digital Wallet Service
- Design Airline Management System
- Design a Library Management System
- Design a Social Network like Facebook
- Design Restaurant Management System
- Design a Concert Ticket Booking System
Hard Problems
- Design CricInfo
- Design Splitwise
- Design Chess Game
- Design a Snake and Ladder game
- Design Ride-Sharing Service like Uber
- Design Course Registration System
- Design Movie Ticket Booking System
- Design Online Shopping System like Amazon
- Design Online Stock Brokerage System
- Design Music Streaming Service like Spotify
- Design Online Food Delivery Service like Swiggy
โฑ๏ธ Concurrency and Multi-threading Problems
- Print FooBar Alternately
- Print Zero Even Odd
- Fizz Buzz Multithreaded
- Building H2O Molecule
- Design Thread-Safe Cache with TTL
- Design Concurrent HashMap
- Design Thread-Safe Blocking Queue
- Design Concurrent Bloom Filter
- Multi-threaded Merge Sort
๐ Courses
๐ Books
๐ฉ Newsletter
Additional resources
๐ค Contributing
Contributions are welcome! If you'd like to add a new problem, improve existing content, or fix errors:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - Commit your changes:
git commit -m 'Add some feature' - Push to the branch:
git push origin feature/your-feature-name - Submit a pull request
Please make sure to update Readme files and documentation as appropriate.
File: design-patterns/python/adapter/README.md
Adapter Design Pattern in Python
Problem Statement
The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of one class into another interface that clients expect.
Core Components
1. Target Interface (PaymentProcessor)
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float, currency: str) -> None:
pass2. Adaptee (LegacyGateway)
class LegacyGateway:
def make_payment(self, currency: str, amount: float) -> None:
# Legacy payment processing logic
pass3. Adapter (LegacyGatewayAdapter)
class LegacyGatewayAdapter(PaymentProcessor):
def __init__(self, legacy_gateway: LegacyGateway):
self.legacy_gateway = legacy_gateway
def process_payment(self, amount: float, currency: str) -> None:
self.legacy_gateway.make_payment(currency, amount)4. Client (CheckoutService)
class CheckoutService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
def checkout(self, amount: float, currency: str) -> None:
self.payment_processor.process_payment(amount, currency)Example Usage
def main():
# Using the in-house payment processor
in_house_processor = InHousePaymentProcessor()
checkout_service = CheckoutService(in_house_processor)
checkout_service.checkout(100, "USD")
# Using the legacy payment gateway through adapter
legacy_processor = LegacyGatewayAdapter(LegacyGateway())
checkout_service = CheckoutService(legacy_processor)
checkout_service.checkout(100, "USD")Benefits
Single Responsibility Principle:
- Separates the interface conversion logic from the business logic
- Each class has a single responsibility
Open/Closed Principle:
- New adapters can be added without modifying existing code
- System remains open for extension but closed for modification
Reusability:
- Existing classes can be reused with new interfaces
- Reduces code duplication
Flexibility:
- Multiple adapters can be created for different purposes
- Easy to switch between different implementations
When to Use
When integrating incompatible interfaces:
- Working with legacy systems
- Using third-party libraries
- Migrating between different versions
When creating a reusable class:
- The class needs to work with unrelated classes
- The class needs to work with classes that don't have compatible interfaces
When implementing multiple interfaces:
- A class needs to implement multiple interfaces
- Different clients need different views of the same class
Best Practices
Keep it Simple:
- Adapters should be simple and focused
- Avoid adding unnecessary functionality
Documentation:
- Clearly document the purpose of the adapter
- Explain the conversion logic
Testing:
- Test the adapter thoroughly
- Ensure it works with both the target and adaptee
Error Handling:
- Handle conversion errors gracefully
- Provide meaningful error messages
Common Pitfalls
Over-engineering:
- Creating adapters when not needed
- Adding unnecessary complexity
Tight Coupling:
- Creating dependencies between adapters
- Making the system less flexible
Performance Issues:
- Adding too many layers of adaptation
- Impacting system performance
File: design-patterns/python/README.md
Design Patterns in Python
This directory contains Python implementations of various design patterns, following the same examples and structure as the Java implementations.
Implemented Patterns
Creational Patterns
- Adapter Pattern - Convert interface of a class into another interface
- Factory Method Pattern - Create objects without specifying exact classes
- Builder Pattern - Construct complex objects step by step
- Singleton Pattern - Ensure only one instance of a class exists
Behavioral Patterns
- Observer Pattern - Define one-to-many dependency between objects
- Strategy Pattern - Define family of algorithms and make them interchangeable
Structural Patterns
- Abstract Factory Pattern - Create families of related objects
- Bridge Pattern - Separate abstraction from implementation
- Composite Pattern - Compose objects into tree structures
- Decorator Pattern - Add behavior to objects dynamically
- Facade Pattern - Provide simplified interface to complex subsystem
- Flyweight Pattern - Minimize memory usage with shared objects
- Proxy Pattern - Provide placeholder/surrogate for another object
Additional Behavioral Patterns
- Chain of Responsibility - Pass requests along handler chain
- Command Pattern - Encapsulate requests as objects
- Iterator Pattern - Access elements sequentially without exposing structure
- Mediator Pattern - Define how objects interact with each other
- Memento Pattern - Capture and restore object state
- State Pattern - Allow object to alter behavior when state changes
- Template Method Pattern - Define skeleton of algorithm in base class
- Visitor Pattern - Define new operations without changing classes
Running the Examples
Each pattern directory contains demo files that can be run independently:
# Run adapter pattern demo
python -m design-patterns.python.adapter.ecommerce_app
# Run factory pattern demo
python -m design-patterns.python.factory.factory_method_demo
# Run builder pattern demo
python -m design-patterns.python.builder.http_app_builder
# Run singleton pattern demo
python -m design-patterns.python.singleton.singleton_demo
# Run observer pattern demo
python -m design-patterns.python.observer.fitness_app_observer_demo
# Run strategy pattern demo
python -m design-patterns.python.strategy.ecommerce_app_demoKey Features
- Same Examples: Uses identical examples as Java implementations for consistency
- Pythonic Code: Follows Python conventions and best practices
- Type Hints: Includes type annotations for better code clarity
- Separate Files: Each class is in its own file for better organization
- Abstract Base Classes: Uses ABC for interfaces and abstract classes
- Documentation: Comprehensive docstrings and comments
Pattern Structure
Each pattern follows this structure:
pattern_name/
โโโ __init__.py
โโโ interface_or_abstract.py # Abstract base classes/interfaces
โโโ concrete_implementations.py # Concrete classes
โโโ context_or_client.py # Context classes or clients
โโโ demo.py # Demonstration of the pattern
โโโ README.md # Pattern-specific documentationDesign Principles Demonstrated
- Open/Closed Principle: Open for extension, closed for modification
- Dependency Inversion: Depend on abstractions, not concretions
- Single Responsibility: Each class has one reason to change
- Composition over Inheritance: Favor object composition
- Encapsulation: Hide implementation details behind interfaces
Contributing
When adding new patterns or modifying existing ones:
- Follow the existing file structure and naming conventions
- Include comprehensive type hints
- Add detailed docstrings
- Create meaningful demo examples
- Maintain consistency with Java implementations
- Include README documentation for each pattern
File: oop/rust/abstraction/README.md
Abstraction in Rust
Abstraction is the design pattern of hiding complex implementation details and exposing only the necessary functionality to the user.
Simple Definition: "Show what it does, hide how it does it."
๐๏ธ Prerequisites: The Building Blocks
Before understanding Abstraction in Rust, we must understand two fundamental concepts that replace "Classes" in other languages: Structs and Traits.
1. What is a Struct?
In Java/C++, a Class holds both data and methods. In Rust, data is separated.
A Struct is strictly for holding Data. It has no behavior on its own.
Example:
// Just data. No logic here.
struct CoffeeMachine {
water_level: u32,
beans: u32,
}2. What is a Trait?
A Trait is Rust's version of an Interface. It defines Behavior.
It tells us what functionality an object must provide, but not how it does it.
Example:
// This is the "Contract" or "Interface"
trait Brewable {
fn make_coffee(&self); // abstract method (no body)
}3. What is `impl`?
The impl keyword is the glue. It implements the Trait (Behavior) for a specific Struct (Data).
โ Abstraction: The Real-World Example
Let's apply these concepts to build a Coffee Machine.
The Problem (Without Abstraction)
If we don't use abstraction, the user has to manually handle the machine's internal parts. This is dangerous and complex.
let mut machine = CoffeeMachine { water: 0, beans: 0 };
// BAD: User has to know how the machine works internally!
machine.water = 100; // User manually filling water?
machine.beans = 50; // User manually loading beans?
machine.grind_beans(); // User manually starting the grinder?
// This is too complex for a normal user.The Solution (With Abstraction)
We want to hide the grinding and heating logic. The user should only see one simple button: brew().
Step 1: Define the Abstraction (The Interface)
We create a public Trait. This is the only thing the user needs to understand.
// PUBLIC: The user sees this.
pub trait CoffeeMaker {
fn add_ingredients(&mut self);
fn press_button(&mut self);
}Step 2: Implement the Hidden Logic
We hide the complex steps (grind_beans, heat_water) inside private helper methods. The user cannot call these directly.
pub struct PremiumMachine {
water: u32,
beans: u32,
}
impl PremiumMachine {
// PRIVATE: Internal logic (Hidden details)
fn grind_beans(&self) {
println!("Grinding beans... ๐");
}
fn heat_water(&self) {
println!("Heating water... ๐ฅ");
}
}
// CONNECTING THEM: Implementing the Public Trait
impl CoffeeMaker for PremiumMachine {
fn add_ingredients(&mut self) {
self.water = 100;
self.beans = 50;
}
fn press_button(&mut self) {
// The abstraction happens here!
// One simple call triggers complex internal logic.
self.grind_beans();
self.heat_water();
println!("Coffee is ready! โ");
}
}๐ Comparison: Rust vs. OOP Languages
Rust handles abstraction differently than Java or Python because it emphasizes memory safety and separation of concerns.
| Feature | Java / C++ / Python | Rust |
|---|---|---|
| Container | Class (Data + Methods together) |
Struct (Data) + impl (Methods) separated |
| Interface | interface or abstract class |
trait |
| Hiding Data | private / protected keywords |
Struct fields are private by default to other modules |
| Override | @Override annotation |
Explicit implementation of Trait methods |
๐ง Why is this useful?
- Safety: The user cannot accidentally change the
water_levelto a wrong value because the field is private. - Simplicity: The user doesn't need to know how to grind beans; they just press the button.
- Flexibility: We can swap the
PremiumMachinewith aCheapMachine, and as long as they both implement theCoffeeMakertrait, the user code doesn't change.
File: oop/rust/aggregation/README.md
Aggregation in Rust (References & Lifetimes)
Introduction
Aggregation is a specific type of association in Object-Oriented Programming (OOP) representing a "Has-A" relationship.
- Crucial Distinction: The lifecycle of the contained object (Child) is independent of the container object (Parent).
- In plain English: If the University closes down (is destroyed), the Professors do not die; they go home.
Does Rust support Aggregation?
Yes, but it is stricter than Java. In Rust, Aggregation is implemented using References (&T) and Lifetimes ('a).
**1. The Core Concept: Borrowing vs. Owning**
To understand Aggregation, you must understand Rust's memory model:
- Composition (Strong): The Parent OWNS the Child.
- Rust:
struct Parent { child: Child } - Result: If Parent dies, Child dies immediately.
- Rust:
- Aggregation (Weak): The Parent BORROWS the Child.
- Rust:
struct Parent<'a> { child: &'a Child } - Result: Parent holds a pointer. If Parent dies, the Child is unaffected because the Parent never owned it.
- Rust:
**2. The "Lifetime" Problem (`'a`)**
This is the most unique part of Rust. If a struct holds a reference, the borrow checker will reject invalid lifetimes at compile time:
"What if the University holds a pointer to a Professor, but the Professor has already been deleted? The University would be pointing to garbage memory!"
To prevent this, we use Lifetime Annotations ('a).
// <'a> reads as: "This struct lives for a duration called 'a"
struct University<'a> {
// This reference must live AT LEAST as long as 'a
professor: &'a Professor,
}What this tells the compiler:
"I promise that the Professor will stay alive longer than (or at least as long as) the University."
**3. Code Implementation**
**Step 1: The Independent Object**
The Professor owns its own data. It doesn't know about the University.
struct Professor {
name: String,
}**Step 2: The Container (With Lifetimes)**
The University holds a Reference (&) to the Professor.
struct University<'a> {
name: String,
// Aggregation: We borrow the professor
staff: Vec<&'a Professor>,
}**Step 3: Usage**
fn main() {
// 1. Create the Child (Owner)
let prof = Professor { name: "Dr. Smith".into() };
{
// 2. Create the Parent (Borrower)
let mut uni = University {
name: "MIT".into(),
staff: vec![&prof] // Borrowing here
};
// uni is destroyed here...
}
// 3. PROOF OF AGGREGATION:
// The Professor is still alive!
println!("{} is currently unemployed but alive.", prof.name);
}**4. Advanced: Shared Ownership (`Rc`)**
Sometimes, you don't know who will die first. Maybe the Professor belongs to 3 Universities at once.
For this, Rust uses Reference Counting (Rc).
Rc<T>: A smart pointer that keeps track of how many owners exist.- The object is only deleted when the count reaches 0.
use std::rc::Rc;
struct Company {
// Shared Ownership Aggregation
employee: Rc,
}**Comparison: Aggregation vs Composition**
| Feature | Aggregation (Weak) | Composition (Strong) |
|---|---|---|
| Relationship | "Uses a" / "Has access to" | "Consists of" |
| Rust Syntax | &'a T (Reference) |
T (Direct Value) |
| Ownership | Borrowed (Parent does not own) | Owned (Parent owns Child) |
| Lifecycle | Independent (Child survives Parent) | Dependent (Child dies with Parent) |
| Drop Rule | Dropping Parent does nothing to Child. | Dropping Parent drops Child. |
๐ New Terminology
| Term | Detailed Explanation |
|---|---|
Lifetime ('a) |
A generic parameter that restricts how long a Reference is valid. It ensures a struct never points to dead memory. |
| Borrow Checker | The part of the Rust compiler that enforces Aggregation rules. It ensures the "Child" lives longer than the "Parent". |
| Dangling Pointer | A pointer that points to invalid memory. Rust's Aggregation rules exist specifically to prevent this. |
Vec<&T> |
A Vector that stores addresses of objects, not the objects themselves. |
File: oop/rust/association/README.md
Association in Rust (Shared Ownership & Weak Pointers)
Introduction
Association represents a "Knows-a" or "Uses-a" relationship. Unlike Composition (where objects die together) or Aggregation (where one borrows the other), Association often implies that multiple objects interact as peers.
The Rust Challenge:
In Java, a Teacher can hold a list of Student objects, and those same Student objects can be held by another Teacher.
In Rust, a value can strictly have only one owner.
To allow multiple Teachers to "know" the same Student, we must use Shared Ownership.
**1. Unidirectional Association (Shared Ownership)**
When multiple parents need to access the same child, we use Rc<T> (Reference Counting).
Rckeeps track of how many things are pointing to the data.- The data is only deleted when the count hits 0.
**Example: Many Teachers, One Student**
Teacher A and Teacher B both teach "Alice". They both need to point to the same Alice in memory.
use std::rc::Rc;
struct Student { name: String }
struct Teacher {
name: String,
// Rc allows multiple teachers to own the same student
students: Vec<Rc<Student>>,
}**2. Bidirectional Association (The "Cycle" Problem)**
This is the hardest concept in Rust OOP.
If a Library owns a Book (Rc), and the Book owns the Library (Rc<Library>), the Reference Count for both will never reach zero.
- Library waits for Book to die.
- Book waits for Library to die.
- Result: Memory Leak.
**The Solution: Weak Pointers (`Weak`)**
To fix this, the Child (Book) must hold a Weak Reference to the Parent (Library).
- Strong Reference (
Rc): "I own this. Don't delete it." - Weak Reference (
Weak): "I point to this, but I don't own it. If it gets deleted, I'll just point to nothing."
**3. Implementation Details (RefCell)**
In Association, we often need to modify an object (like adding a student to a teacher) even though it is shared. Rust doesn't allow mutating Rc data directly.
We use RefCell<T> for interior mutability. It enforces borrowing rules at runtime, which allows mutation through shared references but can panic if you violate the rules.
**The Pattern for Bidirectional Association**
- Parent: Holds
Rc<RefCell<Child>>(Strong ownership, Mutable). - Child: Holds
Weak<RefCell>(Weak link, Mutable).
๐ New Terminology (Advanced)
| Term | Java Equivalent | Detailed Explanation |
|---|---|---|
Rc<T> |
Shared Reference | "Reference Counted". Allows multiple parts of code to own the same data in the Heap. |
Weak<T> |
Weak Reference | A pointer that does not increase the reference count. Used to break infinite loops (cycles) in bidirectional relationships. |
RefCell<T> |
N/A (Magic) | "Interior Mutability". Allows you to modify data even if you only have an immutable reference to it. Essential for linked data structures. |
upgrade() |
Checking for null | The method used to convert a Weak pointer into a real Rc. It returns None if the parent object has already been destroyed. |
File: oop/rust/classes_and_objects/README.md
Classes and Objects
Classes and objects form the foundation of Object-Oriented Programming (OOP).
What is a Class?
In traditional OOP languages (like Java/C++), a Class is a blueprint that bundles data (attributes) and behavior (methods) together.
Rust is different. It separates these two concepts:
- Data is defined in a
struct. - Behavior is defined in an
impl(implementation) block.
Defining a Class (Struct + Impl) in Rust
To define the equivalent of a class in Rust, we use the struct keyword for the data layout and the impl keyword to define methods.
Here's the Car example translated to idiomatic Rust:
// 1. The Blueprint (Data only)
// We use 'pub' to make the struct and fields accessible to other modules.
pub struct Car {
// Attributes
pub color: String,
pub make: String,
pub model: String,
pub year: u32,
}
// 2. The Behavior (Methods)
impl Car {
// Constructor-like function (Associated Function)
// Rust does not have a 'new' keyword. We create a static function typically named 'new'.
pub fn new(color: String, make: String, model: String, year: u32) -> Self {
Car {
color, // Shorthand initialization (same as color: color)
make,
model,
year,
}
}
// Method to display car details
// '&self' is a reference to the current object instance (like 'this' in Java)
pub fn display_info(&self) {
println!("Car Make: {}", self.make);
println!("Car Model: {}", self.model);
println!("Car Year: {}", self.year);
println!("Car Color: {}", self.color);
}
}๐ง New Rust Terminology
struct: Defines the "shape" of the object (the fields).impl: A block where we define functions and methods for a specific struct.&self: A reference to the instance itself. It means "read-only access to my own data". It is equivalent tothisin Java.- Associated Function (
new): Functions defined insideimplthat do not takeself. They are likestaticmethods in Java. We use them as constructors.
What is an Object?
An object is an instance of a struct. When you create an instance, you are allocating memory for that specific blueprint and filling it with real data.
Creating Objects in Rust
In Rust, we don't use the new keyword to instantiate. Instead, we call the associated function we created (Car::new) or initialize the struct directly.
Here's how you can instantiate objects from the Car struct:
fn main() {
// Creating an object (Instance) of the Car struct
// We use "::" to call static methods (associated functions)
// Note: We use .to_string() because "Red" is a string slice (&str),
// but our Struct expects an owned String.
let car1 = Car::new(
"Red".to_string(),
"Toyota".to_string(),
"Corolla".to_string(),
2020
);
let car2 = Car::new(
"Blue".to_string(),
"Ford".to_string(),
"Mustang".to_string(),
2021
);
// Displaying information of each car
// We use "." to call instance methods
car1.display_info();
println!("-----------------");
car2.display_info();
}Key Differences from Java/Go
- Instantiation: No
new Car(...). We callCar::new(...)which is just a normal function we wrote. - Reference: Variables (
car1) own the data by default. In Java, variables are just references (pointers). - Strings: We use
.to_string()because string literals ("Red") are string slices (&str), but our struct wants an ownedString.
๐ก Why Rust prefers Structs over Classes?
If you are coming from Java or C++, you might wonder: "Why didn't Rust just add Classes?"
The answer lies in Composition over Inheritance.
1. The "Banana in the Jungle" Problem
In traditional OOP (Classes), you often create deep inheritance hierarchies (Animal -> Mammal -> Dog).
Joe Armstrong (creator of Erlang) famously said:
"The problem with object-oriented languages is theyโve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
In Rust:
- Structs are just data (The Banana).
- Traits are just behavior (Eating).
- You don't inherit the "Gorilla" just to get the "Banana."
2. No "Diamond Problem"
In languages like C++, allowing a class to inherit from two parents leads to conflicts (The Diamond Problem). Rust avoids this entirely because Structs cannot inherit from other Structs. You strictly use Traits (Interfaces) to share behavior, which is much cleaner and safer.
3. Better Memory Control
- Classes (in Java/Python) are usually references (pointers) to memory on the Heap. This is slower and causes "Garbage Collection" pauses.
- Structs in Rust are values; they can live on the stack or heap depending on how you store them (
Box,Rc, etc.). Some fields (likeString) include heap pointers, so layout depends on the data you put inside.
Summary
Rust replaces "Is-A" relationships (Inheritance) with "Has-A" and "Can-Do" relationships (Composition and Traits).
- Bad (OOP): A
Caris aVehicle. (Forces rigid hierarchy) - Good (Rust): A
CarhasEnginedata (Struct) and candrive(Trait).
File: oop/rust/composition/README.md
Composition in Rust (Ownership & Embedding)
Introduction
Composition is a fundamental Object-Oriented Programming (OOP) principle that represents a "Has-A" relationship. It allows complex objects to be built from simpler ones.
In Rust, Composition is not just a design choice; it is the primary way to build data structures since Rust does not support Inheritance (extends).
The Core Rule of Composition in Rust:
When Struct A contains Struct B, A owns B.
- They share the same lifecycle.
- They share the same memory region.
- When A is dropped (freed), B is automatically dropped.
**What is Composition?**
Composition is when one struct embeds another struct as a field.
**Example: A Car and its Components**
In this example, a Car is composed of an Engine, Wheels, and Transmission.
Unlike Aggregation (where we used references &), here we use the concrete types directly. This means the Car takes full ownership of these components.
struct Engine { horsepower: u32 }
struct Car {
// Direct Composition (Ownership)
// The Engine actually lives INSIDE the Car struct in memory.
engine: Engine,
model: String,
}**Memory Layout Difference**
- Java (Reference):
Car-> (pointer) ->Engine(Heap) - Rust (Composition):
Car[Enginedata ...modeldata ] (Stack or Heap, but contiguous)
**Why Prefer Composition Over Inheritance?**
Since Rust does not have Inheritance, Composition is the standard. However, even in languages that do have Inheritance, Composition is preferred because:
- Flexibility: You can swap components (e.g., change a
GasEngineto anElectricEngine) without changing theCarclass hierarchy. - Encapsulation: The wrapper (
Car) controls exactly how the inner objects (Engine) are accessed. It can hide dangerous methods. - Loose Coupling: The
Cardoesn't need to know the internal details of howEngineworks; it just calls public methods.
**Composition with Traits (Polymorphism)**
To achieve the flexibility of Java interfaces (e.g., swapping different engine types), Rust uses Trait Objects inside Box.
Since different engines (Petrol vs Diesel) might have different sizes in memory, we cannot compose them directly on the stack if we want to swap them dynamically. We must put them in a Box (Pointer).
trait Engine {
fn start(&self);
}
struct PetrolEngine;
impl Engine for PetrolEngine { ... }
struct Car {
// Box<dyn Engine> allows us to hold ANY struct that implements Engine
engine: Box<dyn Engine>,
}**Composition vs Aggregation (The "Drop" Difference)**
The most important detail in Rust is Lifecycle.
| Feature | Composition (Strong) | Aggregation (Weak) |
|---|---|---|
| Rust Syntax | struct A { b: B } |
struct A<'a> { b: &'a B } |
| Ownership | Owned (A owns B) | Borrowed (A borrows B) |
| Memory | B is stored inside A. | B is stored elsewhere; A points to it. |
| Destruction | If A dies, B dies too. | If A dies, B lives on. |
๐ New Terminology
| Term | Explanation |
|---|---|
| Ownership | The Rust rule that says a value can only have one owner. In composition, the container is the owner. |
Box<T> |
A smart pointer that stores data on the Heap. Required when composing Dynamic Trait Objects (dyn Trait) because their size is unknown at compile time. |
| Contiguous Memory | In Rust composition, fields are stored side-by-side in memory. This improves CPU cache performance compared to Java's scattered pointers. |
| Delegate | A pattern where the wrapper struct (Car) calls a method on the inner struct (Engine). car.start() simply calls self.engine.start(). |
File: oop/rust/encapsulation/README.md
Encapsulation in Rust (Modules & Visibility)
Introduction
Encapsulation is one of the four fundamental principles of Object-Oriented Programming (OOP). It is the practice of bundling data (variables) and methods into a single unit while restricting direct access to the internal details.
Does Rust support Encapsulation?
Yes, but it works differently than Java.
- Java: Uses classes and access modifiers (
private,protected,public) to hide data. - Rust: Uses Modules (
mod) and thepubkeyword. Encapsulation boundaries are set at the Module level, not the Struct level.
**What is Encapsulation?**
Encapsulation means wrapping data and code together and restricting access.
**Key Benefits of Encapsulation**
- Data Protection: Prevents external code from corrupting internal state (e.g., negative bank balance).
- Modularity: Changes to internal logic (like how interest is calculated) don't break the code that uses it.
- Interface Clarity: Users only interact with
pubmethods, ignoring the complex details inside.
**Encapsulation Using Visibility Modifiers (`pub`)**
In Java, you have private, protected, public, and package-private.
In Rust, everything is private by default.
- Private (Default): Accessible only within the current module (and its sub-modules).
pub: Accessible from outside the module.pub(crate): Accessible anywhere inside the same project (crate), but not by external users.
**Example: Encapsulation with Modules**
// We define a module to create a privacy boundary
mod bank {
pub struct BankAccount {
pub owner: String, // Public: Anyone can read/write
balance: f64, // Private: Only this 'bank' module can see it
}
impl BankAccount {
pub fn new(owner: &str, balance: f64) -> Self {
BankAccount {
owner: owner.to_string(),
balance,
}
}
pub fn get_balance(&self) -> f64 {
self.balance
}
}
}
fn main() {
let account = bank::BankAccount::new("Alice", 1000.0);
// Allowed: 'owner' is pub
println!("{}", account.owner);
// ERROR: 'balance' is private!
// println!("{}", account.balance);
}Why Use Encapsulation?
- It ensures you cannot accidentally set
balance = -500.0. You must go through a method that validates the input.
**Encapsulation Using Getters and Setters**
Since Rust fields are often private, we access them using methods.
Rust Idiom:
- Getter: naming convention is
field_name(), notget_field_name(). - Setter: naming convention is
set_field_name().
**Example: Getters and Setters in Rust**
pub struct Employee {
name: String,
age: u8,
}
impl Employee {
// Setter
pub fn set_age(&mut self, age: u8) {
if age > 18 {
self.age = age;
} else {
println!("Age must be greater than 18");
}
}
// Getter (Note: no 'get_' prefix)
pub fn age(&self) -> u8 {
self.age
}
}**Encapsulation and Data Hiding**
We hide implementation details so the user doesn't need to worry about complex validation logic.
**Example: Hiding Implementation Details**
mod finance {
pub struct Account {
balance: f64,
}
impl Account {
pub fn new(balance: f64) -> Self {
Account { balance }
}
// Private helper method (Not 'pub', so invisible outside)
fn validate(&self, amount: f64) -> bool {
amount > 0.0 && amount <= self.balance
}
// Public API
pub fn withdraw(&mut self, amount: f64) {
if self.validate(amount) {
self.balance -= amount;
println!("Withdrawal Successful: {}", amount);
} else {
println!("Invalid transaction");
}
}
}
}Why Hide Data?
- Safety: The
validatelogic cannot be bypassed by external code. - Simplicity: The user just calls
withdraw; they don't need to know how validation works.
**Real-World Example: Payment System**
A common real-world use case is processing payments where sensitive data (like card numbers) should never be exposed directly.
mod payment_system {
pub struct CreditCard {
number: String, // Private!
amount: f64,
}
impl CreditCard {
pub fn new(number: &str, amount: f64) -> Self {
CreditCard {
number: CreditCard::mask(number), // Internal logic
amount,
}
}
// Private utility function
fn mask(real_number: &str) -> String {
let last_4 = &real_number[real_number.len()-4..];
format!("****-****-****-{}", last_4)
}
pub fn process(&self) {
println!("Charging {} to card {}", self.amount, self.number);
}
}
}๐ New Terminology (Java vs. Rust)
| Term | Java Equivalent | Detailed Explanation |
|---|---|---|
Module (mod) |
Package / Class | In Rust, the boundary for privacy is the Module, not the Class. Private items in a struct are visible to everything in the same mod. |
pub |
public |
Makes an item visible to other modules. |
| (No modifier) | private / package-private |
By default, everything in Rust is private. It is only visible within the current module and its children. |
pub(crate) |
N/A | Visible to the entire project (crate), but hidden from anyone who uses your project as a library. |
&mut self |
this (implicitly mutable) |
In Java, you can always modify this. In Rust, you must explicitly say &mut self if a method changes data (like a Setter). |
| Getter convention | getName() |
In Rust, we simply use the field name: name(). |
File: oop/rust/inheritance/README.md
Inheritance in Rust (Traits & Composition)
Introduction
Inheritance is one of the core principles of Object-Oriented Programming (OOP). It allows a class (subclass or child class) to acquire the properties and behaviors of another class (superclass or parent class). This promotes code reuse, scalability, and maintainability.
Does Rust support Inheritance?
No, Rust does not support classical inheritance (e.g., class Dog extends Animal). Rust takes a different approach called Composition over Inheritance.
Instead of inheriting fields and methods from a parent class, Rust uses:
- Traits: To share behavior (methods).
- Composition: To share state (data/fields).
**What is Inheritance?**
In traditional OOP (Java), Inheritance is a mechanism where a child class derives properties and behaviors from a parent class.
In Rust, since we cannot use extends, we achieve the same functionality by:
- Implementing Traits: A struct implements a specific trait to gain its behavior.
- Embedding Structs: A struct contains another struct as a field to reuse its data.
**Key Benefits of the Rust Approach**
- Avoids the "Diamond Problem": Multiple inheritance of state is impossible, preventing complex conflicts.
- Explicit Dependencies: You clearly see what data a struct depends on (it's listed in the fields).
- Flexible Behavior: You can implement many traits for a single struct, offering more flexibility than single inheritance.
**How to Implement "Inheritance" in Rust**
Since Rust does not have extends, we use Traits for behavior and Composition for data.
**Step 1: Create a Parent Behavior (Trait)**
The parent trait contains common method signatures and default implementations.
// Parent Trait (Behavior)
pub trait Animal {
fn name(&self) -> &String; // Abstract method (getter)
// Default implementation (Shared Behavior)
fn eat(&self) {
println!("{} is eating...", self.name());
}
}**Step 2: Create a Child Struct using `impl`**
The child struct implements the properties and methods of the parent trait.
// Child Struct (Data)
pub struct Dog {
pub name: String,
pub breed: String,
}
// Implementing the Trait (The "Inheritance" link)
impl Animal for Dog {
fn name(&self) -> &String {
&self.name
}
// 'eat()' is inherited automatically from the Trait
}
impl Dog {
// Dog-specific method
fn bark(&self) {
println!("{} is barking...", self.name);
}
}**Step 3: Use the Child Struct**
Now, let's create an object and use the inherited methods.
fn main() {
let my_dog = Dog {
name: "Buddy".to_string(),
breed: "Golden Retriever".to_string(),
};
// Inherited from Animal trait
my_dog.eat();
// Defined in Dog impl block
my_dog.bark();
}**Output:**
Buddy is eating...
Buddy is barking...**Types of "Inheritance" Patterns in Rust**
Although Rust lacks class inheritance, we can replicate standard OOP patterns.
**1. Single Inheritance (Trait Implementation)**
A struct implements a single trait.
trait Parent {
fn show(&self) {
println!("This is the parent behavior");
}
}
struct Child;
impl Parent for Child {} // Inherits 'show'**2. Multilevel Inheritance (Supertraits)**
A trait inherits from another trait. This is called a Supertrait.
trait Grandparent {
fn show_grand(&self);
}
// Parent requires Grandparent to be implemented first
trait Parent: Grandparent {
fn show_parent(&self);
}
struct Child;
// You must implement both
impl Grandparent for Child {
fn show_grand(&self) { println!("Grandparent"); }
}
impl Parent for Child {
fn show_parent(&self) { println!("Parent"); }
}**3. Hierarchical Inheritance**
A single parent trait is implemented by multiple different structs.
trait Entity {
fn id(&self) -> u32;
}
struct User { id: u32 }
impl Entity for User { fn id(&self) -> u32 { self.id } }
struct Product { id: u32 }
impl Entity for Product { fn id(&self) -> u32 { self.id } }**Method Overriding in Rust**
Method overriding allows a struct to redefine a default method provided by a Trait.
trait Animal {
fn make_sound(&self) {
println!("Generic Animal Sound");
}
}
struct Dog;
impl Animal for Dog {
// Overriding the default method
fn make_sound(&self) {
println!("Dog barks! ๐ถ");
}
}**Usage**
fn main() {
let my_animal = Dog;
my_animal.make_sound(); // Runs overridden version
}**Output:**
Dog barks! ๐ถ**The `super` Concept in Rust (Accessing Parent)**
In Java, super calls the parent class. In Rust, we use Composition to access "parent" data explicitly.
- Composition: Embedding one struct inside another.
- Delegation: Calling the inner struct's methods.
// The "Parent" Data
struct Organism {
age: u8,
}
impl Organism {
fn breathe(&self) { println!("Breathing..."); }
}
// The "Child" Struct
struct Dog {
organism: Organism, // Composition (Has-a)
}
impl Dog {
fn breathe(&self) {
// Explicitly calling the "Parent" (inner struct)
self.organism.breathe();
println!("...and panting!");
}
}**Summary Table**
| Concept | Java (Inheritance) | Rust (Idiomatic) |
|---|---|---|
| Relationship | Dog extends Animal |
Dog implements Animal (Trait) |
| Data Sharing | Inherited automatically | Composition (dog.animal.field) |
| Method Sharing | Inherited from Parent Class | Default Trait Methods |
| Overriding | @Override |
impl Trait block |
| Philosophy | Is-a relationship | Has-a & Can-do relationship |