The Hardest Pattern in Rust: Mediator

A typical Mediator pattern implementation with 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.

The Hardest Pattern in Rust: Mediator
Top-down ownership approach: a mediator takes ownership of all components. Control flow starts from the fn main() where the mediator receives external events/commands.

πŸ‘‰ A code and a full explanation are here: https://github.com/fadeevab/mediator-pattern-rust Β (a Train Station example).

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 (Model-View-Controller) pattern.

Problem Solution
Mediator Problem Mediator Solution

Problem

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 unequal battle against the Rust compiler and its borrow checker. It seems like a solution introduces more problems:

  1. Imagine that the control flow starts at point 1 (Checkbox) where the 1st mutable borrow happens.
  2. The mediator (Dialog) interacts with another object at point 2 (TextField).
  3. 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 point 1).

In Rust, a widespread Mediator implementation is mostly an anti-pattern.

Existing Primers

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 Issue #233.

Nevertheless, we don't surrender.

🎌 Cross-Referencing with Rc<RefCell<..>>

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).

πŸ‘‰ That's how it works: mediator-dynamic

🏁 I would recommend this approach for applications that need multi-threaded support. (Also, it's a good reference of how the Rust compiler could be tricked).

πŸ“„ Real-world example: indicatif::MultiProgress (mediates progress bars with support of being used in multiple threads).

Key points:

  1. All trait methods look like read-only (&self): immutable self and immutable parameters.
  2. Rc, RefCell are extensively used under the hood to take responsibility for the mutable borrowing from compiler to runtime. Invalid implementation will lead to panic in runtime.

‡ Top-Down Ownership

The key point is thinking in terms of OWNERSHIP.

A top-down ownership approach to implementing Mediator in Rust

1. A mediator takes ownership of all components.

2. 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);
}

3. Control flow starts from fn main() where the mediator receives external events/commands.

4. 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 Train Station example 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 the approach: Cursive (TUI).

At least yet, what is not covered is asynchronous approaches, and queue-based event handling with bubble-up capability.