Master the Art of Object-Oriented Programming with Rust

Learn the fundamentals and advanced techniques of Rust, the blazing-fast and memory-safe programming language

Samrat Kumar Das
4 min readApr 22, 2024
cover image

Introduction

Object-oriented programming (OOP) is a programming paradigm that revolves around the concept of objects. An object is a data structure that contains data and methods, which are functions that operate on the data. OOP is widely used in software development as it allows for the creation of modular and reusable code.

Rust is a modern programming language that is particularly well-suited for OOP due to its strong typing system and focus on memory safety. In this blog post, we will explore the fundamental concepts of OOP in Rust and provide practical examples to help you master this programming paradigm.

Defining Objects and Methods

The syntax for defining an object in Rust is as follows:

struct ObjectName {
data_members: data_types,
methods: fn() -> return_types,
}

For example, let’s define an object representing a person that contains data members for name, age, and occupation, and a method to greet someone:

struct Person {
name: String,
age: u8,
occupation: String,

fn greet(&self, name: &str) {
println!("Hello, {}! My name is {}.", name, self.name);
}
}

Creating and Using Objects

To create an object in Rust, you use the new keyword:

let person = Person {
name: "John Doe".to_string(),
age: 30,
occupation: "Software Engineer".to_string(),
};

You can then access the data members and methods of the object using the dot operator:

person.name; // "John Doe"
person.greet("Jane"); // Prints "Hello, Jane! My name is John Doe."

Inheritance

Inheritance is a mechanism that allows you to create new objects based on existing ones. The derived object inherits the data members and methods of the base object, and can also define its own additional members and methods.

In Rust, inheritance is implemented using the trait keyword. A trait defines a set of methods that a type must implement. You can then use the impl keyword to specify that a particular type implements a trait:

trait Greeter {
fn greet(&self, name: &str);
}

impl Greeter for Person {
fn greet(&self, name: &str) {
println!("Hello, {}! My name is {}.", name, self.name);
}
}

Polymorphism

Polymorphism is the ability for objects of different types to respond to the same message in a uniform way. This is achieved through method overriding, where derived objects can provide their own implementation of methods inherited from the base object.

For example, consider the following code:

trait Animal {
fn make_sound(&self);
}

struct Dog {
name: String,
}

impl Animal for Dog {
fn make_sound(&self) {
println!("Woof!");
}
}

struct Cat {
name: String,
}

impl Animal for Cat {
fn make_sound(&self) {
println!("Meow!");
}
}

fn main() {
let dog = Dog { name: "Buddy".to_string() };
let cat = Cat { name: "Kitty".to_string() };

let animals: Vec<Box<dyn Animal>> = vec![Box::new(dog), Box::new(cat)];

for animal in animals {
animal.make_sound();
}
}

In this example, the make_sound method is overridden in both the Dog and Cat structs. When we call the make_sound method on the animals vector, each animal object responds with its own unique sound.

Encapsulation

Encapsulation is the principle of bundling data and methods together into a single unit, and hiding the implementation details from the outside world. This promotes information hiding and prevents direct access to the internal state of an object.

In Rust, encapsulation is achieved through the use of access modifiers:

  • pub: Makes the item public and accessible from anywhere.
  • priv: Makes the item private and accessible only within the module where it is defined.
  • pub(crate): Makes the item public within the current crate, but private to other crates.

For example, consider the following code:

struct Person {
name: String,
age: u8,

fn get_name(&self) -> &str {
&self.name
}

fn set_name(&mut self, new_name: &str) {
self.name = new_name.to_string();
}
}

In this example, the name and age fields are private, while the get_name and set_name methods are public. This encapsulation ensures that the internal state of the Person object can only be accessed or modified through these methods.

Abstraction

Abstraction is the process of hiding the implementation details of an object while still providing access to its essential functionality. This allows users to interact with the object without worrying about how it works.

In Rust, abstraction is achieved through the use of interfaces and traits. Interfaces define a set of methods that a type must implement, while traits define a set of methods that a type can implement.

For example, consider the following code:

trait Shape {
fn area(&self) -> f32;
}

struct Circle {
radius: f32,
}

impl Shape for Circle {
fn area(&self) -> f32 {
std::f32::consts::PI * self.radius.powi(2)
}
}

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

impl Shape for Rectangle {
fn area(&self) -> f32 {
self.width * self.height
}
}

In this example, the Shape trait defines the area method, which must be implemented by any type that implements the trait. The Circle and Rectangle structs both implement the Shape trait by providing their own implementations of the area method.

By using traits, we can create a generic function that takes any type that implements the Shape trait and calculates its area:

fn calculate_area(shape: &dyn Shape) -> f32 {
shape.area()
}

Composition

Composition is the principle of creating objects by combining existing objects. This allows you to create complex objects by reusing simpler objects.

In Rust, composition is achieved through the use of structs and modules. Structs can contain other structs as fields, while modules can contain other modules as

--

--