For other design patterns in Rust, see https://github.com/fadeevab/design-patterns-rust.
Mediator is a challenging pattern to be implemented in Rust. Here is why and what ways are there to implement Mediator in Rust.
cargo run --bin mediator-dynamic
cargo run --bin mediator-static-recommended
Output of the mediator-static-recommended
.
Passenger train Train 1: Arrived
Freight train Train 2: Arrival blocked, waiting
Passenger train Train 1: Leaving
Freight train Train 2: Arrived
Freight train Train 2: Leaving
'Train 3' is not on the station!
A typical Mediator implementation in other languages is a classic anti-pattern in Rust: many objects hold mutable cross-references on each other, trying to mutate each other, which is a deadly sin in Rust - the compiler won't pass your first naive implementation unless it's oversimplified.
By definition, Mediator restricts direct communications between the objects and forces them to collaborate only via a mediator object. It also stands for a Controller in the MVC pattern. Let's see the nice diagrams from https://refactoring.guru:
Problem | Solution |
---|---|
![]() |
![]() |
A common implementation in object-oriented languages looks like the following pseudo-code:
Controller controller = new Controller();
// Every component has a link to a mediator (controller).
component1.setController(controller);
component2.setController(controller);
component3.setController(controller);
// A mediator has a link to every object.
controller.add(component1);
controller.add(component2);
controller.add(component2);
Now, let's read this in Rust terms: "mutable structures have mutable references to a shared mutable object (mediator) which in turn has mutable references back to those mutable structures".
Basically, you can start to imagine the unfair battle against the Rust compiler and its borrow checker. It seems like a solution introduces more problems:
- Imagine that the control flow starts at point 1 (Checkbox) where the 1st mutable borrow happens.
- The mediator (Dialog) interacts with another object at point 2 (TextField).
- The TextField notifies the Dialog back about finishing a job and that leads to a mutable action at point 3... Bang!
The second mutable borrow breaks the compilation with an error (the first borrow was on the point 1).
In Rust, a widespread Mediator implementation is mostly an anti-pattern.
You might see a reference Mediator examples in Rust like this: the example is too much synthetic - there are no mutable operations, at least at the level of trait methods.
The rust-unofficial/patterns repository doesn't include a referenced Mediator pattern implementation as of now, see the Issue #233.
Nevertheless, we don't surrender.
There is an example of a Station Manager example in Go. Trying to make it with Rust leads to mimicking a typical OOP through reference counting and borrow checking with mutability in runtime (which has quite unpredictable behavior in runtime with panics here and there).
👉 Here is a Rust implementation: mediator-dynamic
🏁 I would recommend this approach for applications that need multi-threaded support: particular components after being added to the mediator can be sent to different threads and modified from there.
📄 Real-world example: indicatif::MultiProgress
(mediates progress bars with support of being used in multiple threads).
Key points:
- All trait methods look like read-only (
&self
): immutableself
and parameters. Rc
,RefCell
are extensively used under the hood to take responsibility for the mutable borrowing from compilation time to runtime. Invalid implementation will lead to panic in runtime.
☝ The key point is thinking in terms of OWNERSHIP.
- A mediator takes ownership of all components.
- A component doesn't preserve a reference to a mediator. Instead, it gets the reference via a method call.
// A train gets a mediator object by reference. pub trait Train { fn name(&self) -> &String; fn arrive(&mut self, mediator: &mut dyn Mediator); fn depart(&mut self, mediator: &mut dyn Mediator); } // Mediator has notification methods. pub trait Mediator { fn notify_about_arrival(&mut self, train_name: &str) -> bool; fn notify_about_departure(&mut self, train_name: &str); }
- Control flow starts from
fn main()
where the mediator receives external events/commands. Mediator
trait for the interaction between components (notify_about_arrival
,notify_about_departure
) is not the same as its external API for receiving external events (accept
,depart
commands from the main loop).let train1 = PassengerTrain::new("Train 1"); let train2 = FreightTrain::new("Train 2"); // Station has `accept` and `depart` methods, // but it also implements `Mediator`. let mut station = TrainStation::default(); // Station is taking ownership of the trains. station.accept(train1); station.accept(train2); // `train1` and `train2` have been moved inside, // but we can use train names to depart them. station.depart("Train 1"); station.depart("Train 2"); station.depart("Train 3");
A few changes to the direct approach leads to a safe mutability being checked at compilation time.
👉 A Train Station primer without Rc
, RefCell
tricks, but with &mut self
and compiler-time borrow checking: https://github.com/fadeevab/mediator-pattern-rust/mediator-static-recommended.
👉 A real-world example of such approach: Cursive (TUI).