Skip to content

Commit 134c0f6

Browse files
authored
Mediator: Add Rc<RefCell<..>> approach (#11)
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
1 parent d48fecb commit 134c0f6

File tree

22 files changed

+387
-21
lines changed

22 files changed

+387
-21
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515

1616
steps:
1717
- uses: actions/checkout@v2
18+
- name: Use Rust 1.53
19+
run: rustup install 1.53
1820
- name: Run Rustfmt
1921
run: cargo fmt -- --check
2022
- name: Run Clippy
@@ -23,6 +25,8 @@ jobs:
2325
- run: cargo build --bin command # TUI. It can run on the local machine.
2426
- run: cargo run --bin iterator
2527
- run: cargo run --bin mediator
28+
- run: cargo run --bin mediator-top-down
29+
- run: cargo run --bin mediator-rc-refcell
2630
- run: cargo run --bin memento
2731
- run: cargo run --bin memento-serde
2832
- run: cargo run --bin observer

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ members = [
44
"behavioral/chain-of-responsibility",
55
"behavioral/command",
66
"behavioral/iterator",
7-
"behavioral/mediator",
7+
"behavioral/mediator/mediator-rc-refcell",
8+
"behavioral/mediator/mediator-top-down",
89
"behavioral/memento",
910
"behavioral/observer",
1011
"behavioral/state",

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ Also, the examples contain a **README.md** with instructions and additional expl
4242
cargo run --bin chain-of-responsibility
4343
cargo run --bin command
4444
cargo run --bin iterator
45-
cargo run --bin mediator
45+
cargo run --bin mediator-top-down
46+
cargo run --bin mediator-rc-refcell
4647
cargo run --bin memento
4748
cargo run --bin memento-serde
4849
cargo run --bin observer

behavioral/mediator/README.md

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
# Mediator Pattern
1+
# Mediator
22

3-
There is a research and discussion of the **Mediator Pattern in Rust**:
4-
https://github.com/fadeevab/mediator-pattern-rust.
3+
_**Mediator** restricts direct communications between the objects and forces them
4+
to collaborate only via a mediator object_. It also stands for a Controller in the MVC (Model-View-Controller) pattern.
55

6-
Top-Down Ownership approach allows to apply Mediator in Rust as it is
7-
a suitable for Rust's ownership model with strict borrow checker rules. It's not
8-
the only way to implement Mediator, but it's a fundamental one.
9-
10-
## How To Run
6+
## How to Run
117

128
```bash
13-
cargo run --bin mediator
9+
cargo run --bin mediator-top-down
10+
cargo run --bin mediator-rc-refcell
1411
```
1512

1613
## Execution Result
1714

15+
Output of the `mediator-top-down`.
16+
1817
```
1918
Passenger train Train 1: Arrived
2019
Freight train Train 2: Arrival blocked, waiting
@@ -24,12 +23,94 @@ Freight train Train 2: Leaving
2423
'Train 3' is not on the station!
2524
```
2625

26+
## Problem
27+
28+
_*Mediator* is a challenging pattern to be implemented in *Rust*._
29+
30+
A typical Mediator implementation in other languages is a classic anti-pattern
31+
in Rust: many objects hold mutable cross-references on each other, trying to
32+
mutate each other, which is a deadly sin in Rust - the compiler won't pass your
33+
first naive implementation unless it's oversimplified.
34+
35+
By definition, [Mediator][1] restricts direct communications between the objects
36+
and forces them to collaborate only via a mediator object. It also stands for
37+
a Controller in the MVC pattern. Let's see the nice diagrams from
38+
https://refactoring.guru:
39+
40+
| Problem | Solution |
41+
| ---------------------------- | ----------------------------- |
42+
| ![image](images/problem.png) | ![image](images/solution.png) |
43+
44+
A common implementation in object-oriented languages looks like the following
45+
pseudo-code:
46+
47+
```java
48+
Controller controller = new Controller();
49+
50+
// Every component has a link to a mediator (controller).
51+
component1.setController(controller);
52+
component2.setController(controller);
53+
component3.setController(controller);
54+
55+
// A mediator has a link to every object.
56+
controller.add(component1);
57+
controller.add(component2);
58+
controller.add(component2);
59+
```
60+
61+
Now, let's read this in **Rust** terms: _"**mutable** structures have
62+
**mutable** references to a **shared mutable** object (mediator) which in turn
63+
has mutable references back to those mutable structures"_.
64+
65+
Basically, you can start to imagine the unfair battle against the Rust compiler
66+
and its borrow checker. It seems like a solution introduces more problems:
67+
68+
![image](images/mediator-mut-problem.png)
69+
70+
1. Imagine that the control flow starts at point 1 (Checkbox) where the 1st
71+
**mutable** borrow happens.
72+
2. The mediator (Dialog) interacts with another object at point 2 (TextField).
73+
3. The TextField notifies the Dialog back about finishing a job and that leads
74+
to a **mutable** action at point 3... Bang!
75+
76+
The second mutable borrow breaks the compilation with an error
77+
(the first borrow was on the point 1).
78+
79+
## Cross-Referencing with `Rc<RefCell<..>>`
80+
81+
```bash
82+
cargo run --bin mediator-rc-refcell
83+
```
84+
85+
`Rc<RefCell<..>>` hides objects from compiler eyes inside of an opaque smart pointer.
86+
In this case, borrow checks move into the runtime that means panicking in case of
87+
borrow rules violation.
88+
89+
There is an example of a [Station Manager example in Go][4]. Trying to make it
90+
with Rust leads to mimicking a typical OOP through reference counting and
91+
borrow checking with mutability in runtime (which has quite unpredictable
92+
behavior in runtime with panics here and there).
93+
94+
Key points:
95+
96+
1. All trait methods are **read-only**: immutable `self` and immutable parameters.
97+
2. `Rc`, `RefCell` are extensively used under the hood to take responsibility
98+
for the mutable borrowing from compiler to runtime. Invalid implementation
99+
will lead to panic in runtime.
100+
27101
## Top-Down Ownership
28102

29-
The key point is thinking in terms of OWNERSHIP.
103+
```bash
104+
cargo run --bin mediator-top-down
105+
```
106+
107+
☝ The key point is thinking in terms of OWNERSHIP.
108+
109+
![Ownership](images/mediator-rust-approach.jpg)
30110

31111
1. A mediator takes ownership of all components.
32-
2. A component doesn't preserve a reference to a mediator. Instead, it gets the reference via a method call.
112+
2. A component doesn't preserve a reference to a mediator. Instead, it gets the
113+
reference via a method call.
33114

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

49-
3. Control flow starts from `fn main()` where the mediator receives external events/commands.
50-
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).
130+
3. Control flow starts from `fn main()` where the mediator receives external
131+
events/commands.
132+
4. `Mediator` trait for the interaction between components
133+
(`notify_about_arrival`, `notify_about_departure`) is not the same as its
134+
external API for receiving external events (`accept`, `depart` commands from
135+
the main loop).
51136

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

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

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

75-
1. [Mediator Pattern in Rust](https://github.com/fadeevab/mediator-pattern-rust)
76-
2. [Mediator in Go (Example)](https://refactoring.guru/design-patterns/mediator/go/example)
161+
[1]: https://refactoring.guru/design-patterns/mediator
162+
[2]: https://github.com/rust-unofficial/patterns/issues/233
163+
[3]: https://chercher.tech/rust/mediator-design-pattern-rust
164+
[4]: https://refactoring.guru/design-patterns/mediator/go/example
165+
[5]: https://crates.io/crates/cursive
56.7 KB
Loading
44.7 KB
Loading
42.8 KB
Loading
41.4 KB
Loading
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
edition = "2021"
3-
name = "mediator"
3+
name = "mediator-rc-refcell"
44
version = "0.1.0"
55

66
[[bin]]
7-
name = "mediator"
7+
name = "mediator-rc-refcell"
88
path = "main.rs"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Mediator with `Rc<RefCell<..>>`
2+
3+
## How To Run
4+
5+
```bash
6+
cargo run --bin mediator-rc-refcell
7+
```
8+
9+
## Mimicking a Typical OOP
10+
11+
`Rc<RefCell<..>>` hides objects from compiler eyes inside of an opaque smart pointer.
12+
In this case, borrow checks move into the runtime that means panicking in case of
13+
borrow rules violation.
14+
15+
There is an example of a [Station Manager example in Go][4]. Trying to make it
16+
with Rust leads to mimicking a typical OOP through reference counting and
17+
borrow checking with mutability in runtime (which has quite unpredictable
18+
behavior in runtime with panics here and there).
19+
20+
Key points:
21+
22+
1. All methods are read-only: immutable `self` and parameters.
23+
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.
24+
25+
See the full article: [README.md](../README.md).
26+
27+
[4]: https://refactoring.guru/design-patterns/mediator/go/example

0 commit comments

Comments
 (0)