Design Patterns in Rust: Mediator, or uncoupling objects made easy
The mediator pattern is a pettern used when you want to simplify communication i.e. message dispatching in complex application. It involves building a mediator where each object can delegate its communication, it also routes the messages to the right receivers(s).
So, what does it look like? Well, like this:
An explanation of this diagram is needed:
- First there is the _Mediator _interface, which defines the message which can be routed through this Mediator.
- The _ConcreteMediator _implements the Mediator interface, and coordinates the communication between the _ConcreteWorker _objects. It knows about the _ConcreteWorker _objects and how they should communicate
- The _Worker _is an interface (or in some languages an abstract class) which is an interface for the communication between the ConcreteWorkers
- The ConcreteWorker(x) classes implement the Worker interface, and communicate with other _ConcreteWorker _classes through the Mediator.
In an empty directory open your commandline or terminal and type:
cargo new mediator_pattern
cd mediator_pattern
Open this directory in your favourite IDE, and edit the main.rs file. In this example we will build some sort of very simple chat-system.
Add the following as the first line of your main.rs file:
use std::{rc::Rc, cell::RefCell, collections::HashMap};
We will need this in the rest of this example.
The Mediator trait is very simple:
trait Mediator {
fn notify(&self, sender: &str, event: &str);
}
In it, we see one method: notify() which notifies all the registered receivers, that is the receivers which belong to this Mediator.
The Worker trait looks as follows:
trait Worker {
fn set_mediator(&mut self, mediator: Rc<RefCell<dyn Mediator>>);
fn send(&self,message:&str);
fn receive(&self,sender:&str,message:&str);
}
It has three methods, send() and receive() are self-explanatory.
The set_mediator() method gets two parameters:
- &self which is a reference to the object
- mediator, which is of type Rc<RefCell<dyn Mediator>>
Let us start with the dyn Mediator: because Mediator is a trait, we need to specify that we need dynamic dispatch.
Then we get to the _Rc_and RefCell. Since the mediator is shared among implementations, which is quite un-idiomatic for Rust, we need some form of reference counting, and that is what this does.
During the making of this example, this was the biggest problem I encountered, and even this solution is not quite elegant in my opinion.
The ChatMediator trait looks as follows:
struct ChatMediator {
workers: HashMap<String,Rc<RefCell<dyn Worker>>>,
}
We are using a HashMap here with a String as key, which is logical as each User has a name. The value however is Rc<RefCell<dyn Worker>>>. This is because Worker implementation might be shared among different Mediators and we need some kind of reference counting mechanism.
We need to implement only one method here, namely the notify() method:
impl Mediator for ChatMediator {
fn notify(&self, sender: &str, event: &str) {
for (key,worker) in self.workers.iter() {
if key!=sender {
worker.borrow().receive(sender, event);
}
}
}
}
All this method does is iterate over all the workers, then test if the sender and receiver are not the same. If so, then a message can be sent.
Have a look at the User struct :
struct User {
name: String,
mediator: Option<Rc<RefCell<dyn Mediator>>>,
}
The User struct has two fields:
- Each User has a name, as mentioned before.
- Each User has a Mediator object to send messages to. This is of the Rc<RefCell<dyn Mediator>>, as we have seen before, as the Mediator can be shared among more workers, and some kind of reference counting is needed. However, we wrapped this in an Option simply because when a User is constructed, the mediator can be None.
Implementing the Worker trait for User is quite straightforward:
impl Worker for User {
fn set_mediator(&mut self, mediator: Rc<RefCell<dyn Mediator>>) {
self.mediator = Some(mediator);
}
fn send(&self, message: &str) {
if let Some(mediator)=&self.mediator {
mediator.borrow().notify(&self.name,message);
}
}
fn receive(&self, sender: &str, message: &str) {
println!("{} received from {}: {}", self.name, sender, message);
}
}
A short explanation:
- The set_mediator() method simply sets the mediator of the User to the passed-in parameter.
- The send() method first checks if there is a mediator to talk to. If there is, it explicitly borrows the mediator, and calls the notify() method, with the sender’s name and the message.
- The receive() method just gets the name of the sender and the message and prints it out.
Now we can do some simple tests in the main() method:
fn main() {
let mediator = Rc::new(RefCell::new(ChatMediator {
workers: HashMap::new(),
}));
let alice = Rc::new(RefCell::new(User {
name: String::from("Alice"),
mediator: None,
}));
alice.borrow_mut().set_mediator(Rc::clone(&mediator) as Rc<RefCell<dyn Mediator>>);
let bob = Rc::new(RefCell::new(User {
name: String::from("Bob"),
mediator: None,
}));
bob.borrow_mut().set_mediator(Rc::clone(&mediator) as Rc<RefCell<dyn Mediator>>);
mediator.borrow_mut().workers.insert(String::from("Alice"), Rc::clone(&alice) as Rc<RefCell<dyn Worker>>);
mediator.borrow_mut().workers.insert(String::from("Bob"), Rc::clone(&bob) as Rc<RefCell<dyn Worker>>);
alice.borrow().send("Hello, Bob!");
bob.borrow().send("Hi, Alice!");
}
A short breakdown:
- First we construct an implementation of the Mediator trait , which is a ChatMediator in our case. We initialize it with an empty HashMap
- Next we construct a User, named ‘alice’, with her name, and no mediator yet
- We explicitly do a mutable borrow of alice, which we can so since it is an Rc, and set the mediator, to a clone of the mediator. The as clause is needed since Rc::clone does not return a trait but the concrete struct.
- We do the same for user ‘bob’
- Next we insert the workers, but first we need to do a mutable borrow of the mediator. It is also in an Rc so we can do that. We insert not only the name, but also an Rc::clone of the worker, which we need to cast, since Rc::clone does not return a trait.
- Now it is time to send message, using the explicitly borrowed instances of alice and bob
This is possibly one of the hardest patterns I had to implement, mainly because of the inherent object sharing in this pattern, which due to ownership-rules is quite unidiomatic for Rust. It took some research (using sites like ChatGPT and phind) but I finally managed to get it right. However, the implementation is not very elegant and certainly not thread-safe. There are alternatives: there is a crate called mediator which could be of help, I also understand it supports async scenarios.
If there is such a crate why take the trouble? Well, implementing these patterns I consider to be training, as I am still learning the language, and implementing such a thing by yourself can be quite instructive.