fadeevab / mediator-pattern-rust

Mediator Pattern in Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

For other design patterns in Rust, see https://github.com/fadeevab/design-patterns-rust.

Mediator Pattern in Rust

Mediator is a challenging pattern to be implemented in Rust. Here is why and what ways are there to implement Mediator in Rust.

How to Run

cargo run --bin mediator-dynamic
cargo run --bin mediator-static-recommended

Execution Result

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!

Problem

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
image image

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:

image

  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 the 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 the 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 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 wouldn't recommend this approach, however, I think it's a good reference of how the Rust compiler could be tricked.

Key points:

  1. All trait methods are read-only: 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.

Ownership

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

About

Mediator Pattern in Rust

License:MIT License


Languages

Language:Rust 100.0%