Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Rust Advanced

Advanced Rust

Rust Advanced

Advanced Design Patterns in Rust

Advanced Design Patterns in Rust

In advanced Rust programming, mastering design patterns opens doors to elegant and efficient solutions. These patterns empower you to structure your code to enhance readability, maintainability, and extensibility. Let's explore some of the advanced design patterns you'll encounter in this book:

Builder Pattern

The Builder pattern is a powerful way to construct complex objects step by step, abstracting away the details of their creation. In Rust, this pattern finds a natural fit due to the language's expressive syntax and emphasis on safety. By creating a builder, you separate the construction logic from the actual object, allowing for cleaner code and enabling the creation of objects with varying configurations. Whether you're building user interfaces, configuration objects, or complex data structures, the Builder pattern clarifies your code.

Let’s see how to use the builder pattern in a real-world scenario:

struct DatabaseConnection {
    host: String,
    port: u16,
    username: String,
    password: String,
    database: String,
}

struct DatabaseConnectionBuilder {
    host: String,
    port: u16,
    username: String,
    password: String,
    database: String,
}

impl DatabaseConnectionBuilder {
    fn new(host: &str) -> Self {
        DatabaseConnectionBuilder { 
            host: host.to_string(), 
            port: 3306, 
            username: String::new(), 
            password: String::new(), 
            database: String::new() 
        }
    }

    fn port(mut self, port:u16) -> Self {
        self.port = port;
        self
    }

    fn username(mut self, username: &str) -> Self {
        self.username = username.to_string();
        self
    }

    
    fn password(mut self, password: &str) -> Self {
        self.password = password.to_string();
        self
    }

    fn database(mut self, database: &str) -> Self {
        self.database = database.to_string();
        self
    }

    fn build(self) -> DatabaseConnection {
        DatabaseConnection { 
            host: self.host, 
            port: self.port, 
            username: self.username, 
            password: self.password, 
            database: self.database, 
        }
    }
}

fn main() {
    let connection = DatabaseConnectionBuilder::new("localhost")
    .port(5432)
    .username("master_user123")
    .password("secretpassword")
    .database("masteringbackenddb")
    .build();

    println!("Database Connection: ");
    println!("Host: {}", connection.host);
    println!("Port: {}", connection.port);
    println!("Username: {}", connection.username);
    println!("Password: {}", connection.password);
    println!("Database: {}", connection.database);
}

Run this with Cargo:

Untitled (23).png

In this example, we define a DatabaseConnection struct and a corresponding DatabaseConnectionBuilder struct. The builder allows step-by-step construction of a DatabaseConnection instance, abstracting away the construction details. This separation of concerns enhances code clarity and enables easy creation of objects with varying configurations. The main function demonstrates how to use the builder to create a customized DatabaseConnection object.

Iterator Patterns

Rust's iterator system is a cornerstone of its expressive power. Beyond the basics, you'll delve into advanced iterator patterns that enable efficient and elegant data processing. Learn to create custom iterators that fit your needs, whether traversing complex data structures, filtering elements, or transforming data. Combining iterators with functional programming concepts will enhance your code's clarity and conciseness.

Now, for the iterator pattern:

struct User {
    id: u32,
    username: String,
    active: bool,
}

struct UserDatabase {
    users: Vec<User>,
}

impl UserDatabase {
    fn new(users: Vec<User>) -> Self {
        UserDatabase { users }
    }

    fn active_users(&self) -> ActiveUsersIterator<'_> {
        ActiveUsersIterator {
            user_db: &self,
            current_index: 0,
        }
    }
}

struct ActiveUsersIterator<'a> {
    user_db: &'a UserDatabase,
    current_index: usize,
}

impl<'a> Iterator for ActiveUsersIterator<'a> {
    type Item = &'a User;

    fn next(&mut self) -> Option<Self::Item> {
        while self.current_index < self.user_db.users.len() {
            let user = &self.user_db.users[self.current_index];
            self.current_index += 1;
            if user.active {
                return Some(user);
            }
        }
        None
    }
}

fn main() {
    let users = vec![
        User {
            id: 1,
            username: "user1".to_string(),
            active: true,
        },
        User {
            id: 2,
            username: "user2".to_string(),
            active: false,
        },
        User {
            id: 3,
            username: "user3".to_string(),
            active: true,
        },
    ];

    let user_db = UserDatabase::new(users);

    println!("Active Users:");
    for user in user_db.active_users() {
        println!("ID: {}, Username: {}", user.id, user.username);
    }
}

Run this with Cargo:

Untitled (24).pngIn this example, we define a User struct to represent users in a database and a UserDatabase struct to manage a collection of users. The ActiveUsersIterator struct implements the Iterator trait, allowing us to create a custom iterator that yields only active users. By implementing the next method, we can define how the iterator should traverse the collection and filter out non-active users.

The main function demonstrates how to use the custom iterator over active users in the UserDatabase. This showcases the power of custom iterators to tailor the data processing to specific needs.

Decorator Pattern

The Decorator pattern facilitates dynamic behavior extension without altering existing code. In Rust, you can leverage traits to implement this pattern effectively. By defining a core trait and creating decorators that implement this trait, you can add functionality to objects at runtime, achieving composability and avoiding class explosion. This pattern is useful for augmenting an object's behavior without resorting to complex inheritance hierarchies.

trait DataStore {
    fn store_data(&self, data: &str);
}

struct FileDataStore;

impl DataStore for FileDataStore {
    fn store_data(&self, data: &str) {
        println!("Storing data in file: {}", data);
    }
}

struct EncryptedDataStore {
    inner: Box<dyn DataStore>,
}

impl EncryptedDataStore {
    fn new(inner: Box<dyn DataStore>) -> Self {
        EncryptedDataStore { inner }
    }
}

impl DataStore for EncryptedDataStore {
    fn store_data(&self, data: &str) {
        let encrypted_data = encrypt_data(data);
        self.inner.store_data(&encrypted_data);
    }
}

fn encrypt_data(data: &str) -> String {
    // Simplified encryption for demonstration purposes.
    data.chars()
        .map(|c| (c as u8 + 1) as char)
        .collect()
}

fn main() {
    let file_data_store = FileDataStore;
    let encrypted_data_store = EncryptedDataStore::new(Box::new(file_data_store));

    encrypted_data_store.store_data("Sensitive information");
}

In this example, we define a DataStore trait representing the core behavior of storing data. We then implement this trait for the FileDataStore struct. Next, we create an EncryptedDataStore struct that wraps another DataStore instance using composition.

By implementing the DataStore trait for EncryptedDataStore, we can extend the behavior of the wrapped data store. In this case, the EncryptedDataStore encrypts the data before passing it to the inner data store. This allows us to dynamically add behavior without altering the existing code.

# Output:
Storing data in file: Tfotjujwf!jogpsnbujpo

The main function demonstrates how the EncryptedDataStore can store data, showcasing the Decorator pattern's flexibility and composability.

Singleton Pattern

Implementing the Singleton pattern ensures that a class has only one instance and provides a global access point to that instance. In Rust, you can use the lazy_static crate to achieve thread-safe lazy initialization of global instances. Understanding how to create and manage singletons is crucial when dealing with resource-intensive objects that should be shared across your application.

pub struct DatabaseConn {
    pub url: String,
}

impl DatabaseConn {
    pub fn new(url: String) -> Self {
        DatabaseConn {
            url: url.to_string(),
        }
    }

    pub fn connect(&self) {
        println!("Connecting to database: {}", self.url);
        // Perform actual connection logic...
    }
}

pub struct DatabaseManager {
    pub connection: DatabaseConn,
}

impl DatabaseManager {
    pub fn get_instance() -> &'static Self {
        static mut INSTANCE: Option<DatabaseManager> = None;
        let connection_url = "example.com/db".to_string();
        unsafe {
            INSTANCE.get_or_insert_with(|| DatabaseManager {
                connection: DatabaseConn::new(connection_url),
            })
        }
    }
}

fn main() {
    let db_manager = DatabaseManager::get_instance();
    db_manager.connection.connect();

    // attempt to create another instance will use the existing one.
    let db_manager_2 = DatabaseManager::get_instance();

    println!("Are instances equal? {:?}", std::ptr::eq(db_manager, db_manager_2));
}

When you run the code, you find:

Are instances equal? true

In this example, we use the DatabaseConnection struct to simulate a database connection and the DatabaseManager struct to implement the Singleton pattern. The DatabaseManager holds an instance of DatabaseConnection and ensures that only one instance of itself exists.

Inside the get_instance method of DatabaseManager, we use the static mut construct along with the Option type to create a single instance of DatabaseManager. This approach ensures thread safety by leveraging Rust's ownership and borrowing rules.

The main function showcases how to access the Singleton instance using the get_instance method. It also demonstrates that attempting to create another instance of DatabaseManager using the same method will reuse the existing instance, adhering to the Singleton pattern's principles.

Great! You have learned about some design patterns and how to implement them with advanced Rust features. Let’s move on to meta-programming in Rust.

Exercise

  1. Find out about other design patterns and implement them.

  2. Learn what scenarios it’s convenient to use each of these design patterns.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board