Codementor Events

Design Patterns in Rust: Chain of Responsibility: there is more than one way to do it

Published Jun 03, 2023

The Chain of Responsibility (CoC) pattern describes a chain of command/request receivers. The client has no idea which one of the receivers will handle the request. The beauty of the pattern is again uncoupling: the sender and the receiver do not know about each other. A danger is that no receiver in the chain will handle the request, this has to be taken into account when implementing this pattern.

An excellent example of this could be the way that middleware is implemented in some web-frameworks (Gin, but also ASP.NET core are examples).

So, what does it look like?

ChainOfResponsibility.drawio.png

Let us break this down:

  1. We have a Client who does a request to a Handler
  2. The Handler has a list of Receivers, and succesively sends the request to a receiver until one handles the request

As you can see, this is not a very complicated pattern

Open your terminal or commandline in an empty director and type:

cargo new rust_chain_of_responsibility
cd rust_chain_of_responsibility

Now open your favorite IDE in this directory and edit the main.rs file in the src directory.

The Handler trait looks very simple:

trait Handler {
    fn set_next(&mut self, next: Box<dyn Handler>);
    fn handle(&self, request: &str)->Option<&str>;
}

A short explanation

  1. The set_next() method is used to set the next handler in the chain. Since this could change the implementing structure, &mut self is used.
  2. In the handle() method the request is either handled, or put through to the next handler in the chain, if there is any. The

In the concrete handlers we will implement this logic. The first handler, CarHandler, looks like this:

struct CarHandler {
    next: Option<Box<dyn Handler>>,
}

impl Handler for CarHandler {
    fn handle(&self, request: &str)->Option<&str> {
        if request == "Car" {
            Some("Handled Car")
        } else {
            match &self.next {
                Some(handler) => handler.handle(request),
                None => Some("Handler not found or end of chain"),
            }
        }
    }

    fn set_next(&mut self, next: Box<dyn Handler>) {
        self.next=Some(next);
    }
}

Again, some explanation is needed

  • The CarHandler structure contains one field in this case: a reference to the next handler in the chain. It is an option, which means that the value ‘None‘ could be provided, indicating the end of the chain.
  • In the handle() method, we handle the request, if it matches some criteria. If the request can not be handled, the request must be dispatched onto the next handler if there is a next handler. That is what the rest of the method is about.
  • Next in the set_next() method we can set the next Handler. As you can see, a Chain of Responsibility can be changed dynamically.

The second handler looks similar:

struct BikeHandler {
    next: Option<Box<dyn Handler>>,
}

impl Handler for BikeHandler {
    fn handle(&self, request: &str)->Option<&str> {
        if request == "Bike" {
            Some("Handled Bike")
        } else {
            match &self.next {
                Some(handler) => handler.handle(request),
                None => Some("Handler not found or end of chain"),
            }
        }
    }

    fn set_next(&mut self, next: Box<dyn Handler>) {
        self.next=Some(next);
    }
}

Now we can test it in the main method:

fn main() {
    let mut car_handler = CarHandler { next: None };
    let bike_handler = BikeHandler { next: None };

    car_handler.set_next(Box::new(bike_handler));
    let result = car_handler.handle("Car");
    println!("{}", result.unwrap());
}

Line by line

  1. Create a car_handler and bike_handler, both with no next handler set (i.e. None)
  2. Now set the bike_handler as the handler after the car_handler
  3. Now try out a request, and print it out. In this case it should print out “Handled Car”

Again, like with many things in Rust, it was entirely straightforward implementing this pattern. However, with a lots of help from the compiler, and some Googling, I managed to get this implementation together.

As you can see, the chain can be changed dynamically. Possible extensions however could be the following:

  1. Better error-handling
  2. Asynchronous handlers.

I will address both issues in a later blog.

Discover and read more posts from Iede Snoek
get started
post commentsBe the first to share your opinion
Show more replies