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

The journey through implementing the Servant pattern in Rust demonstrates a powerful technique for achieving simple, flexible, and elegant code design.

By introducing the WindowsRotatorServant, we successfully decoupled the common behavior (rotation) from the specific clients (RotatableWindow and NonRotatableWindow). This separation brings several key advantages to our Rust code:

  • Clean Abstraction: The Rotatable trait defines the core capability, while the RotationServant trait centralizes the interaction logic. The windows themselves only need to implement the Rotatable trait, without needing to handle the broader “service” context.
  • Enhanced Flexibility: If the rotation logic needed to change (e.g., adding logging, security checks, or complex physics), we would only need to modify the WindowsRotatorServant implementation. The individual window structs would remain untouched, adhering to the Open/Closed Principle.
  • Easy Testing: The servant object is an isolated unit, making it simpler to test the service logic independently of the client objects.

In essence, the Servant pattern, utilized with Rust’s robust Traits and Dynamic Dispatch (dyn Rotatable), allows us to inject functionality into client objects indirectly. It serves as a middleman, providing a common set of operations while keeping the client objects focused on their primary responsibilities.

Mastering this pattern in Rust is a valuable step towards writing highly maintainable, less coupled, and ultimately more beautiful code.

The Code Nomad
The Code Nomad
Articles: 162

Leave a Reply

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