Easy delegation in Rust: the delegation pattern

Photo by Thirdman: https://www.pexels.com/photo/person-handing-out-keys-to-another-person-8482876/

Introduction

In delegation, you delegate a certain request to an object to a second object, which we call the delegate. This is almost like a subclass sending a request to its parent class. In Rust we can achieve this by embedding the delegate, that is the object to which the original request is delegated, in the struct. Note that there can be more than one delegate.

Implementation in Rust

When downloading a file from an URL it can be practical to update the caller about the progress and even more importantly about errors.

We will start with the prerequisites:

use std::thread;
use std::time::Duration;
use std::fmt;

Why these? In this example to simulate a long running process, we will need the thread to sleep for a certain amount milliseconds, that is why we need these two modules. The std::fmt module is used to format the errors, and perform general output.

The DownloadError implementation

When downloading a file over a network, or even from local storage, things can go wrong. That is why we need some error handling. We will start by defining a DownloadError enum:

#[derive(Debug, Clone)]
pub enum DownloadError {
    DownloadError(String),
}

Next we implement the Display trait, so the error message can be displayed in a user-friendly way:

impl fmt::Display for DownloadError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DownloadError::DownloadError(msg) => write!(f, "Download error: {}", msg),
        }
    }
}

Finally, to embed the error into Rust’s error handling ecosystem, we need to implement the Error trait:

impl std::error::Error for DownloadError {}

The DownloadDelegate trait

Next we can define the DownloadDelegate:

trait DownloadDelegate {

    fn on_download_start(&self);
    

    fn on_download_progress(&self, progress: f64);
    

    fn on_download_complete(&self, downloader: &FileDownloader, result: Result<(), DownloadError>);
}

Line by line:

  1. The on_download_start() is called when the download starts.
  2. on_download_progress() is called during the download.
  3. When the download is complete or is aborted for some reason, the on_download_complete() is called.

Defining the FileDownloader struct

The FileDownloader struct looks like this:

struct FileDownloader {
    url: String,
    delegate: Option<Box<dyn DownloadDelegate>>,
}

Note that the delegate is of type Option<> as the delegate might be set at a later stage or not at all.

For the implementation we will start with the constructor:

    fn new(url: &str) -> Self {
        Self {
            url: url.to_string(),
            delegate: None,
        }
    }

The delegate field is initally set to to None.

To set the delegate we have the following method:

    fn set_delegate(&mut self, delegate: Box<dyn DownloadDelegate>) {
        self.delegate = Some(delegate);
    }

And the client application might also want to know the url being processed:

    pub fn url(&self) -> &str {
        &self.url
    }

Now we come to the main part, the download() method:

    fn download(&self) -> Result<(), DownloadError> {

        if let Some(delegate) = &self.delegate {
            delegate.on_download_start();
        }


        const TOTAL_STEPS: u32 = 20; 
        for i in 0..=TOTAL_STEPS {
            thread::sleep(Duration::from_millis(100));
            
            let progress = (i as f64 / TOTAL_STEPS as f64) * 100.0;
            if let Some(delegate) = &self.delegate {
                delegate.on_download_progress(progress);
            }
            

        }

        let result=Ok(());        

        if let Some(delegate) = &self.delegate {
            delegate.on_download_complete(self, result.clone());
        }

        result
    }

Line by line:

  1. We start by checking if there is a delegate, and if there is one, we notify the delegate about the start of the download.
  2. Next we simulate the download in 20 steps. Between each step the thread sleeps 100 milliseconds. After that the progress is calculated, and if there is a delegate, the delegate is notified of the progress.
  3. After the loop terminates, the delegate is notified of the completion of the download.

The whole implementation looks like this:

impl FileDownloader {
    
    fn new(url: &str) -> Self {
        Self {
            url: url.to_string(),
            delegate: None,
        }
    }

    
    fn set_delegate(&mut self, delegate: Box<dyn DownloadDelegate>) {
        self.delegate = Some(delegate);
    }

    
    pub fn url(&self) -> &str {
        &self.url
    }

    
    fn download(&self) -> Result<(), DownloadError> {
        // Notify delegate that download is starting
        if let Some(delegate) = &self.delegate {
            delegate.on_download_start();
        }

        
        const TOTAL_STEPS: u32 = 20; 
        for i in 0..=TOTAL_STEPS {
            thread::sleep(Duration::from_millis(100));
            
            let progress = (i as f64 / TOTAL_STEPS as f64) * 100.0;
            if let Some(delegate) = &self.delegate {
                delegate.on_download_progress(progress);
            }
            

        }

        let result=Ok(());        

        if let Some(delegate) = &self.delegate {
            delegate.on_download_complete(self, result.clone());
        }

        result
    }
}

Implementing the delegate

Next comes the DownloadManager, which is the delegate. The DownloadManager struct is quite simple:

struct DownloadManager {
    id: String,
}

The basic implementation of this struct is straightforward:

impl DownloadManager {
    fn new(id: &str) -> Self {
        Self {
            id: id.to_string(),
        }
    }
}

Now implement the delegate for the DownloadManager:

impl DownloadDelegate for DownloadManager {
    fn on_download_start(&self) {
        println!("Download started for manager: {}", self.id);
    }

    fn on_download_progress(&self, progress: f64) {
        let progress_int = progress as usize;
        let bar_length = progress_int / 5; // 20 characters max for 100%
        let bar = "-".repeat(bar_length);
        let empty = " ".repeat(20 - bar_length);
        
        println!(
            "[{}] Progress: [{}{}] {:.1}%", 
            self.id, bar, empty, progress
        );
    }

    fn on_download_complete(&self, downloader: &FileDownloader, result: Result<(), DownloadError>) {
        match result {
            Ok(()) => {
                println!(
                    "[Manager {}] Download of '{}' completed successfully!",
                    self.id, downloader.url()
                );
                println!("[Manager {}] Processing downloaded file...", self.id);
            }
            Err(error) => {
                println!(
                    "X [Manager {}] Download of '{}' failed: {}",
                    self.id, downloader.url(), error
                );
            }
        }
    }
}

Some notes:

  • All three methods of the trait are implemented.
  • Also, all three methods in this case print a status, either the start or the end of the download, or the current status.

Time to test

Now we can test our setup:

fn main() {
    println!("Starting file download demo...\n");
    let downloader = FileDownloader::new("https://example.com/large-file.zip");

    let manager = DownloadManager::new("main_manager");

    let mut downloader = downloader;
    downloader.set_delegate(Box::new(manager));

    let result = downloader.download();
    println!("Download operation completed with result: {:?}\n", result);
    
}

Line by line:

  1. We create a new file downloader, and a download manager.
  2. Next the manager is associated with the downloader, so that it gets notifications from the downloader. Note that the downloader would also work without the delegate.
  3. The download is started and the status messages are printed.

Conclusion

Implementing this pattern was relatively easy. One of the enhancements could be to make it thread-safe, or even run the delegate separate thread.

Also note by using traits, we can make this pattern quite flexible.

The Code Nomad
The Code Nomad
Articles: 167

One comment

  1. this is not delegation, this is not even close to what it is
    the closest we can get to delegation in rust is by dereferencing what we want to delegate

Leave a Reply

Your email address will not be published. Required fields are marked *