Skip to content

Commit

Permalink
Mediator: Add Rc<RefCell<..>> approach (#11)
Browse files Browse the repository at this point in the history
I found that `Rc<RefCell<..>>` may actually be useful in some cases.
I decided to move this case into the repo for a quick reference.

* Use Rust 1.53 for GitHub Workflows
* Fix clippy errors
* Update behavioral/mediator/README.md
  • Loading branch information
fadeevab authored May 4, 2023
1 parent d48fecb commit 134c0f6
Show file tree
Hide file tree
Showing 22 changed files with 387 additions and 21 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Use Rust 1.53
run: rustup install 1.53
- name: Run Rustfmt
run: cargo fmt -- --check
- name: Run Clippy
Expand All @@ -23,6 +25,8 @@ jobs:
- run: cargo build --bin command # TUI. It can run on the local machine.
- run: cargo run --bin iterator
- run: cargo run --bin mediator
- run: cargo run --bin mediator-top-down
- run: cargo run --bin mediator-rc-refcell
- run: cargo run --bin memento
- run: cargo run --bin memento-serde
- run: cargo run --bin observer
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ members = [
"behavioral/chain-of-responsibility",
"behavioral/command",
"behavioral/iterator",
"behavioral/mediator",
"behavioral/mediator/mediator-rc-refcell",
"behavioral/mediator/mediator-top-down",
"behavioral/memento",
"behavioral/observer",
"behavioral/state",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Also, the examples contain a **README.md** with instructions and additional expl
cargo run --bin chain-of-responsibility
cargo run --bin command
cargo run --bin iterator
cargo run --bin mediator
cargo run --bin mediator-top-down
cargo run --bin mediator-rc-refcell
cargo run --bin memento
cargo run --bin memento-serde
cargo run --bin observer
Expand Down
123 changes: 106 additions & 17 deletions behavioral/mediator/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
# Mediator Pattern
# Mediator

There is a research and discussion of the **Mediator Pattern in Rust**:
https://github.com/fadeevab/mediator-pattern-rust.
_**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.

Top-Down Ownership approach allows to apply Mediator in Rust as it is
a suitable for Rust's ownership model with strict borrow checker rules. It's not
the only way to implement Mediator, but it's a fundamental one.

## How To Run
## How to Run

```bash
cargo run --bin mediator
cargo run --bin mediator-top-down
cargo run --bin mediator-rc-refcell
```

## Execution Result

Output of the `mediator-top-down`.

```
Passenger train Train 1: Arrived
Freight train Train 2: Arrival blocked, waiting
Expand All @@ -24,12 +23,94 @@ Freight train Train 2: Leaving
'Train 3' is not on the station!
```

## Problem

_*Mediator* is a challenging pattern to be implemented in *Rust*._

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][1] 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](images/problem.png) | ![image](images/solution.png) |

A common implementation in object-oriented languages looks like the following
pseudo-code:

```java
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](images/mediator-mut-problem.png)

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

## Cross-Referencing with `Rc<RefCell<..>>`

```bash
cargo run --bin mediator-rc-refcell
```

`Rc<RefCell<..>>` hides objects from compiler eyes inside of an opaque smart pointer.
In this case, borrow checks move into the runtime that means panicking in case of
borrow rules violation.

There is an example of a [Station Manager example in Go][4]. 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).

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.
```bash
cargo run --bin mediator-top-down
```

☝ The key point is thinking in terms of OWNERSHIP.

![Ownership](images/mediator-rust-approach.jpg)

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.
2. A component doesn't preserve a reference to a mediator. Instead, it gets the
reference via a method call.

```rust
// A train gets a mediator object by reference.
Expand All @@ -46,8 +127,12 @@ The key point is thinking in terms of OWNERSHIP.
}
```

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

```rust
let train1 = PassengerTrain::new("Train 1");
Expand All @@ -68,9 +153,13 @@ The key point is thinking in terms of OWNERSHIP.
station.depart("Train 3");
```

![Top-Down Ownership](https://github.com/fadeevab/mediator-pattern-rust/raw/main/images/mediator-rust-approach.jpg)
A few changes to the direct approach leads to a safe mutability being checked
at compilation time.

## References
👉 A real-world example of such approach: [Cursive (TUI)][5].

1. [Mediator Pattern in Rust](https://github.com/fadeevab/mediator-pattern-rust)
2. [Mediator in Go (Example)](https://refactoring.guru/design-patterns/mediator/go/example)
[1]: https://refactoring.guru/design-patterns/mediator
[2]: https://github.com/rust-unofficial/patterns/issues/233
[3]: https://chercher.tech/rust/mediator-design-pattern-rust
[4]: https://refactoring.guru/design-patterns/mediator/go/example
[5]: https://crates.io/crates/cursive
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added behavioral/mediator/images/problem.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added behavioral/mediator/images/solution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
edition = "2021"
name = "mediator"
name = "mediator-rc-refcell"
version = "0.1.0"

[[bin]]
name = "mediator"
name = "mediator-rc-refcell"
path = "main.rs"
27 changes: 27 additions & 0 deletions behavioral/mediator/mediator-rc-refcell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Mediator with `Rc<RefCell<..>>`

## How To Run

```bash
cargo run --bin mediator-rc-refcell
```

## Mimicking a Typical OOP

`Rc<RefCell<..>>` hides objects from compiler eyes inside of an opaque smart pointer.
In this case, borrow checks move into the runtime that means panicking in case of
borrow rules violation.

There is an example of a [Station Manager example in Go][4]. 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).

Key points:

1. All methods are read-only: immutable `self` and parameters.
2. `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.

See the full article: [README.md](../README.md).

[4]: https://refactoring.guru/design-patterns/mediator/go/example
25 changes: 25 additions & 0 deletions behavioral/mediator/mediator-rc-refcell/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
mod train_station;
mod trains;

use std::{cell::RefCell, rc::Rc};

use train_station::StationManager;
use trains::{FreightTrain, PassengerTrain, Train};

fn main() {
let station = Rc::new(RefCell::new(StationManager::default()));

let train1 = Rc::new(PassengerTrain::new("Train 1".into(), station.clone()));
let train2 = Rc::new(FreightTrain::new("Train 2".into(), station.clone()));

{
let mut station = station.borrow_mut();
station.register(train1.clone());
station.register(train2.clone());
}

train1.arrive();
train2.arrive();
train1.depart();
train2.depart();
}
54 changes: 54 additions & 0 deletions behavioral/mediator/mediator-rc-refcell/train_station.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
rc::Rc,
};

use crate::trains::Train;

pub trait Mediator {
fn notify_about_arrival(&self, train: &dyn Train) -> bool;
fn notify_about_departure(&self, train: &dyn Train);
}

#[derive(Default)]
pub struct StationManager {
trains: HashMap<String, Rc<dyn Train>>,
train_queue: RefCell<VecDeque<String>>,
train_on_platform: RefCell<Option<String>>,
}

impl StationManager {
pub fn register(&mut self, train: Rc<dyn Train>) {
self.trains.insert(train.name().clone(), train);
}
}

impl Mediator for StationManager {
fn notify_about_arrival(&self, train: &dyn Train) -> bool {
let train_name = train.name().clone();

self.trains.get(&train_name).expect("A train should exist");

if self.train_on_platform.borrow().is_some() {
self.train_queue.borrow_mut().push_back(train_name);
return false;
}

self.train_on_platform.replace(Some(train_name));
true
}

fn notify_about_departure(&self, train: &dyn Train) {
if Some(train.name().clone()) != self.train_on_platform.replace(None) {
return;
}

let next_train = self.train_queue.borrow_mut().pop_front();

if let Some(next_train_name) = next_train {
let next_train = self.trains.get(&next_train_name).unwrap();
next_train.arrive();
}
}
}
35 changes: 35 additions & 0 deletions behavioral/mediator/mediator-rc-refcell/trains/freight_train.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::{cell::RefCell, rc::Rc};

use super::Train;
use crate::train_station::Mediator;

pub struct FreightTrain {
name: String,
mediator: Rc<RefCell<dyn Mediator>>,
}

impl FreightTrain {
pub fn new(name: String, mediator: Rc<RefCell<dyn Mediator>>) -> Self {
Self { name, mediator }
}
}

impl Train for FreightTrain {
fn name(&self) -> &String {
&self.name
}

fn arrive(&self) {
if !self.mediator.borrow().notify_about_arrival(self) {
println!("Freight train {}: Arrival blocked, waiting", self.name);
return;
}

println!("Freight train {}: Arrived", self.name);
}

fn depart(&self) {
println!("Freight train {}: Leaving", self.name);
self.mediator.borrow().notify_about_departure(self);
}
}
11 changes: 11 additions & 0 deletions behavioral/mediator/mediator-rc-refcell/trains/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mod freight_train;
mod passenger_train;

pub use freight_train::FreightTrain;
pub use passenger_train::PassengerTrain;

pub trait Train {
fn name(&self) -> &String;
fn arrive(&self);
fn depart(&self);
}
Loading

0 comments on commit 134c0f6

Please sign in to comment.