Object-Oriented Programming (OOP) is a programming paradigm based on the concept of 'objects', which can contain data and code: data in the form of fields (attributes or properties), and code in the form of procedures (methods). The primary goal of OOP is to increase the flexibility and maintainability of programs. Here are the core characteristics:
1. Encapsulation:
* Explanation: Encapsulation is the bundling of data (attributes) and the methods that operate on that data into a single unit, or object. It also involves 'information hiding', meaning that the internal state of an object is hidden from the outside world and can only be accessed and modified through a public interface (methods). This protects data from accidental corruption and allows for changes to the internal implementation without affecting external code that uses the object.
* In Rust: Rust achieves encapsulation through structs and their associated `impl` blocks. By default, fields within a struct are private, meaning they cannot be directly accessed from outside the module where the struct is defined. Public methods (`pub fn`) provide controlled access to these private fields, demonstrating encapsulation.
2. Inheritance:
* Explanation: Inheritance is a mechanism where a new class (subclass or derived class) is created from an existing class (superclass or base class). The subclass inherits properties and behaviors (data and methods) from the superclass, allowing for code reuse and establishing an 'is-a' relationship (e.g., 'A Dog is an Animal').
* In Rust: Rust does not have traditional class-based inheritance like C++ or Java. Instead, Rust favors composition (where one struct contains another struct) and trait objects for achieving polymorphism. While composition allows code reuse by delegating functionality, trait objects provide dynamic dispatch and a way to define shared behavior across different types without a hierarchical relationship based on inheritance.
3. Polymorphism:
* Explanation: Polymorphism, meaning 'many forms', is the ability of an object to take on many forms. It allows objects of different classes to be treated as objects of a common type, enabling a single interface to represent different underlying forms (data types).
* Static Polymorphism (Compile-time): Achieved through method overloading (different methods with the same name but different parameters) or operator overloading. In Rust, generics and static dispatch using traits often achieve this.
* Dynamic Polymorphism (Runtime): Achieved through method overriding, where a subclass provides its own implementation of a method that is already defined in its superclass. This is typically done through virtual functions or interfaces.
* In Rust: Rust primarily achieves polymorphism through traits. Traits define a shared set of behaviors that types can implement. Dynamic polymorphism is realized using 'trait objects' (`Box<dyn Trait>`), which allow you to store and operate on different concrete types as long as they implement the specified trait. Static polymorphism is achieved through generics that are bounded by traits.
4. Abstraction:
* Explanation: Abstraction is the concept of hiding complex implementation details and showing only the essential features of an object. It focuses on 'what' an object does rather than 'how' it does it. This simplifies the user interface for complex systems and allows developers to focus on higher-level logic without getting bogged down in low-level specifics.
* In Rust: Traits are Rust's primary mechanism for abstraction. They define a contract or an interface without specifying the implementation details. Public and private visibility modifiers (e.g., `pub` vs. default private) also contribute to abstraction by exposing only necessary parts of a module or struct to the outside world, hiding internal complexity.
Example Code
```rust
// --- 1. Encapsulation Example ---
// A BankAccount struct with private fields and public methods
struct BankAccount {
account_number: String, // Private by default (only accessible within the module)
balance: f64, // Private by default
}
impl BankAccount {
// Public constructor to create a new BankAccount
pub fn new(account_number: String, initial_balance: f64) -> BankAccount {
BankAccount {
account_number,
balance: initial_balance,
}
}
// Public method to deposit funds, controlling access to 'balance'
pub fn deposit(&mut self, amount: f64) {
if amount > 0.0 {
self.balance += amount;
println!("Deposited {:.2}. New balance: {:.2}", amount, self.balance);
} else {
println!("Deposit amount must be positive.");
}
}
// Public method to withdraw funds, controlling access to 'balance'
pub fn withdraw(&mut self, amount: f64) -> bool {
if amount > 0.0 && self.balance >= amount {
self.balance -= amount;
println!("Withdrew {:.2}. New balance: {:.2}", amount, self.balance);
true
} else {
println!("Insufficient funds or invalid amount.");
false
}
}
// Public method to get the current balance (read-only access to private data)
pub fn get_balance(&self) -> f64 {
self.balance
}
// Public method to get the account number (read-only access to private data)
pub fn get_account_number(&self) -> &str {
&self.account_number
}
}
// --- 3. Abstraction and 4. Polymorphism Example using Traits ---
// A Trait defines an interface or a contract (Abstraction).
// Any type that implements this trait promises to provide these behaviors.
trait Shape {
fn area(&self) -> f64; // An abstract method that *must* be implemented by concrete types
// A default method implementation that can be optionally overridden
fn describe(&self) -> String {
format!("This is a generic shape.")
}
}
// Concrete type: Circle
struct Circle {
radius: f64,
}
// Implement the Shape trait for Circle
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// Override the default describe method to provide specific details
fn describe(&self) -> String {
format!("This is a Circle with radius {}.", self.radius)
}
}
// Concrete type: Rectangle
struct Rectangle {
width: f64,
height: f64,
}
// Implement the Shape trait for Rectangle
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
// Override the default describe method
fn describe(&self) -> String {
format!("This is a Rectangle with width {} and height {}.", self.width, self.height)
}
}
fn main() {
println!("--- Encapsulation Example ---");
let mut account = BankAccount::new("12345".to_string(), 100.0);
account.deposit(50.0);
account.withdraw(30.0);
account.withdraw(200.0); // This will fail due to insufficient funds, demonstrating controlled access
println!("Account {} current balance: {:.2}\n", account.get_account_number(), account.get_balance());
// Attempting to directly access a private field would result in a compile error:
// account.balance = 500.0; // ERROR: field `balance` of `BankAccount` is private
println!("--- Abstraction and Polymorphism Example ---");
// Create a vector of trait objects (runtime polymorphism).
// `Box<dyn Shape>` allows us to store different concrete types (Circle, Rectangle)
// that all implement the Shape trait, and call the `area` and `describe` methods polymorphically.
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 10.0 }),
Box::new(Rectangle { width: 5.0, height: 4.0 }),
];
for shape in shapes {
// The specific implementation of `area` and `describe` is called at runtime
// based on the actual type of the object stored in the Box.
println!("{} Area: {:.2}", shape.describe(), shape.area());
}
// --- Note on Inheritance in Rust ---
// As mentioned in the explanation, Rust does not have traditional class-based inheritance.
// We typically achieve similar goals like code reuse (via composition) and defining
// shared behavior/interfaces (via traits) instead of a direct 'is-a' hierarchy.
}
```








Characteristics of Object-Oriented Languages