diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0f4a78..56b3598 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 @@ -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 diff --git a/Cargo.toml b/Cargo.toml index af72350..526310b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/README.md b/README.md index 7e1942a..cc4ee55 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/behavioral/mediator/README.md b/behavioral/mediator/README.md index adbd0b8..7190998 100644 --- a/behavioral/mediator/README.md +++ b/behavioral/mediator/README.md @@ -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 @@ -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>` + +```bash +cargo run --bin mediator-rc-refcell +``` + +`Rc>` 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. @@ -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"); @@ -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 diff --git a/behavioral/mediator/images/mediator-mut-problem.png b/behavioral/mediator/images/mediator-mut-problem.png new file mode 100755 index 0000000..35bb182 Binary files /dev/null and b/behavioral/mediator/images/mediator-mut-problem.png differ diff --git a/behavioral/mediator/images/mediator-rust-approach.jpg b/behavioral/mediator/images/mediator-rust-approach.jpg new file mode 100755 index 0000000..c16d101 Binary files /dev/null and b/behavioral/mediator/images/mediator-rust-approach.jpg differ diff --git a/behavioral/mediator/images/problem.png b/behavioral/mediator/images/problem.png new file mode 100755 index 0000000..568ea6c Binary files /dev/null and b/behavioral/mediator/images/problem.png differ diff --git a/behavioral/mediator/images/solution.png b/behavioral/mediator/images/solution.png new file mode 100755 index 0000000..f4b745d Binary files /dev/null and b/behavioral/mediator/images/solution.png differ diff --git a/behavioral/mediator/Cargo.toml b/behavioral/mediator/mediator-rc-refcell/Cargo.toml similarity index 55% rename from behavioral/mediator/Cargo.toml rename to behavioral/mediator/mediator-rc-refcell/Cargo.toml index e0127d3..9d310a8 100644 --- a/behavioral/mediator/Cargo.toml +++ b/behavioral/mediator/mediator-rc-refcell/Cargo.toml @@ -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" diff --git a/behavioral/mediator/mediator-rc-refcell/README.md b/behavioral/mediator/mediator-rc-refcell/README.md new file mode 100644 index 0000000..97fea52 --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/README.md @@ -0,0 +1,27 @@ +# Mediator with `Rc>` + +## How To Run + +```bash +cargo run --bin mediator-rc-refcell +``` + +## Mimicking a Typical OOP + +`Rc>` 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 \ No newline at end of file diff --git a/behavioral/mediator/mediator-rc-refcell/main.rs b/behavioral/mediator/mediator-rc-refcell/main.rs new file mode 100644 index 0000000..7271249 --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/main.rs @@ -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(); +} diff --git a/behavioral/mediator/mediator-rc-refcell/train_station.rs b/behavioral/mediator/mediator-rc-refcell/train_station.rs new file mode 100644 index 0000000..7abfa9b --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/train_station.rs @@ -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>, + train_queue: RefCell>, + train_on_platform: RefCell>, +} + +impl StationManager { + pub fn register(&mut self, train: Rc) { + 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(); + } + } +} diff --git a/behavioral/mediator/mediator-rc-refcell/trains/freight_train.rs b/behavioral/mediator/mediator-rc-refcell/trains/freight_train.rs new file mode 100644 index 0000000..67f8283 --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/trains/freight_train.rs @@ -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>, +} + +impl FreightTrain { + pub fn new(name: String, mediator: Rc>) -> 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); + } +} diff --git a/behavioral/mediator/mediator-rc-refcell/trains/mod.rs b/behavioral/mediator/mediator-rc-refcell/trains/mod.rs new file mode 100644 index 0000000..47837b9 --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/trains/mod.rs @@ -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); +} diff --git a/behavioral/mediator/mediator-rc-refcell/trains/passenger_train.rs b/behavioral/mediator/mediator-rc-refcell/trains/passenger_train.rs new file mode 100644 index 0000000..daf42d4 --- /dev/null +++ b/behavioral/mediator/mediator-rc-refcell/trains/passenger_train.rs @@ -0,0 +1,35 @@ +use std::{cell::RefCell, rc::Rc}; + +use super::Train; +use crate::train_station::Mediator; + +pub struct PassengerTrain { + name: String, + mediator: Rc>, +} + +impl PassengerTrain { + pub fn new(name: String, mediator: Rc>) -> Self { + Self { name, mediator } + } +} + +impl Train for PassengerTrain { + fn name(&self) -> &String { + &self.name + } + + fn arrive(&self) { + if !self.mediator.borrow().notify_about_arrival(self) { + println!("Passenger train {}: Arrival blocked, waiting", self.name); + return; + } + + println!("Passenger train {}: Arrived", self.name); + } + + fn depart(&self) { + println!("Passenger train {}: Leaving", self.name); + self.mediator.borrow().notify_about_departure(self); + } +} diff --git a/behavioral/mediator/mediator-top-down/Cargo.toml b/behavioral/mediator/mediator-top-down/Cargo.toml new file mode 100644 index 0000000..4c9ae0f --- /dev/null +++ b/behavioral/mediator/mediator-top-down/Cargo.toml @@ -0,0 +1,12 @@ +[package] +edition = "2021" +name = "mediator-top-down" +version = "0.1.0" + +[[bin]] +name = "mediator" +path = "main.rs" + +[[bin]] +name = "mediator-top-down" +path = "main.rs" diff --git a/behavioral/mediator/mediator-top-down/README.md b/behavioral/mediator/mediator-top-down/README.md new file mode 100644 index 0000000..3c81a80 --- /dev/null +++ b/behavioral/mediator/mediator-top-down/README.md @@ -0,0 +1,72 @@ +# Mediator with Top-Down Ownership + +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. + +## How To Run + +```bash +cargo run --bin mediator-top-down +``` + +## Execution Result + +``` +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! +``` + +## Top-Down Ownership + +The key point is thinking in terms of 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. + + ```rust + // 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). + + ```rust + 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"); + ``` + +![Top-Down Ownership](https://github.com/fadeevab/mediator-pattern-rust/raw/main/images/mediator-rust-approach.jpg) + +## References + +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) diff --git a/behavioral/mediator/main.rs b/behavioral/mediator/mediator-top-down/main.rs similarity index 100% rename from behavioral/mediator/main.rs rename to behavioral/mediator/mediator-top-down/main.rs diff --git a/behavioral/mediator/train_station.rs b/behavioral/mediator/mediator-top-down/train_station.rs similarity index 100% rename from behavioral/mediator/train_station.rs rename to behavioral/mediator/mediator-top-down/train_station.rs diff --git a/behavioral/mediator/trains/freight_train.rs b/behavioral/mediator/mediator-top-down/trains/freight_train.rs similarity index 100% rename from behavioral/mediator/trains/freight_train.rs rename to behavioral/mediator/mediator-top-down/trains/freight_train.rs diff --git a/behavioral/mediator/trains/mod.rs b/behavioral/mediator/mediator-top-down/trains/mod.rs similarity index 100% rename from behavioral/mediator/trains/mod.rs rename to behavioral/mediator/mediator-top-down/trains/mod.rs diff --git a/behavioral/mediator/trains/passenger_train.rs b/behavioral/mediator/mediator-top-down/trains/passenger_train.rs similarity index 100% rename from behavioral/mediator/trains/passenger_train.rs rename to behavioral/mediator/mediator-top-down/trains/passenger_train.rs