Serving Simplicity: Mastering the Servant Pattern in Rust for Easy and Elegant Code Design

Photo by Helena Lopes: https://www.pexels.com/photo/person-pouring-coffee-on-white-ceramic-cup-1394841/

Introduction

The Servant pattern is a way of organizing code where one special object helps out a bunch of other objects. This helper object provides some functions to these objects, so they don’t have to do the same things over and over again. This pattern makes it easier to add new features and test the code.

The difference between the Servant pattern and Dependency Injection

The Servant pattern helps us keep things separate and make our systems more flexible. It adds extra features to existing objects. On the other hand, Dependency Injection reduces connections between different parts of the code. It gives a class the things it needs to work, usually in a specific form, instead of making the class create them.

Implementation in Rust

We’ll make a simple window system using this pattern. There are two types of windows: ones that can rotate and ones that can’t. We create these windows and a helper, the Servant, to make them rotate.

We will start with the necessary imports:

use std::io::{Error,ErrorKind};

Next we define a Rotatable trait, with a rotate(). Note that this returns a Result in case a rotation goes wrong:

trait Rotatable {
    fn rotate(&self,degrees:f64)->Result<&str,Error>;
}

Now we define a RotatableWindow:

struct RotatableWindow {
    title: String,
}

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

    fn open(&self) {
        println!("{} is open", self.title);
    }

    fn close(&self) {
        println!("{} is closed", self.title);
    }
}

impl Rotatable for RotatableWindow {
    fn rotate(&self, degrees: f64) -> Result<&str, Error> {
        println!("{} is rotated by {} degrees", self.title, degrees);
        Ok("Success")
    }
}

Apart from the usual new(), open() and close() methods, we also implement the Rotatable interface, which after printing out a message, returns an Ok result.

The NonRotatableWindow looks like this:

struct NonRotatableWindow {
    title: String,
}

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

    fn open(&self) {
        println!("{} is open", self.title);
    }

    fn close(&self) {
        println!("{} is closed", self.title);
    }
}

impl Rotatable for NonRotatableWindow {
    fn rotate(&self, degrees: f64) -> Result<&str, Error> {
        Err(Error::new(ErrorKind::Other, "Can not rotate non rotatable window"))
    }
}

This more or less the same as the RotatableWindow, the main difference being the return value of the rotate() method, which now is an Error.

Now we come to the RotationServant trait:

trait RotationServant {
    fn rotate<'a>(&self, window:&'a dyn Rotatable,degrees:f64) -> Result<&'a str, Error>;
}

The rotate() method gets two arguments:

  1. A Rotatable instance, the object that we want to rotate
  2. And the number of degrees.

Like the rotate() in the Rotatable trait we return a Result. The lifetime-specifier is there to make sure the lifetime of the input is tied to the lifetime of the output.

Now the implementation of the actual servant:

struct WindowsRotatorServant;

impl RotationServant for WindowsRotatorServant {
    fn rotate<'a>(&self, window: &'a dyn Rotatable,degrees: f64) -> Result<&'a str, Error> {
        window.rotate(degrees)
    }
}

The implementation just calls the rotate() method on the Rotatable object, and returns the result of the method.

Testing

That wasn’t too hard, let’s see if it works:

fn main() {
    let rotator=WindowsRotatorServant{};

    let rotatable_window = RotatableWindow::new("Rotatable Window");
    rotatable_window.open();
    let result=rotator.rotate(&rotatable_window, 90.0);
    match result {
        Ok(_) => println!("Success rotating window"),
        Err(e) => println!("Error rotating window: {}",e),
    }
    rotatable_window.close();

    let non_rotatable_window = NonRotatableWindow::new("Non Rotatable Window");
    non_rotatable_window.open();
    let result=rotator.rotate(&non_rotatable_window, 90.0);
    match result {
        Ok(_) => println!("Success rotating window"),
        Err(e) => println!("Error rotating window: {}",e),
    }
    non_rotatable_window.close();
}

The code is quite clear: we create two windows, one rotatable, one non-rotatable. We open, rotate and close each, and print out the results.

Conclusion

Even though it was a bit tricky to find a good example, the Servant pattern is a neat way to add features to your classes without changing their code. It’s especially useful when you want to keep different tasks separate, like rotating windows in our example. It makes the code easy to put together and maintain.

Leave a Reply

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