Here is a simple implementation of the Observer pattern in Rust. First, the code, and then a small bit of explanation of the implementation itself.
The project structure:
Macushla:rust-observer z0ltan$ tree . ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs └── main.rs 1 directory, 4 files
The client code:
extern crate rust_observer; use rust_observer::*; fn main() { let mut foo = EvenCounter::new(); let (bar, baz, quux) = (Box::new(EvenObserver::new("bar".to_string())), Box::new(EvenObserver::new("baz".to_string())), Box::new(EvenObserver::new("quux".to_string()))); foo.register(bar); foo.register(baz); foo.register(quux); foo.run(); }
Nothing special here – just creating an Observable
object of type EvenCounter
, and then three instances of EvenObserver
, which implements the Observer
type.
The idea is to have the observers get notified whenever the observable object’s counter is an even number (it runs the counter in an infinite loop).
And finally, the actual implementation:
/// the observable type pub trait Observable<T> { fn register(&mut self, observer: Box<Observer<Item = T>>); } pub trait Observer { type Item; fn notify(&self, val: &Self::Item); } /// the actual structs which implement the Observable and Observer /// traits /// the specific Observable pub struct EvenCounter { counter: u32, observers: Vec<Box<Observer<Item = u32>>>, } impl EvenCounter { pub fn new() -> Self { EvenCounter { counter: 0u32, observers: Vec::new(), } } pub fn run(&mut self) { loop { use std::thread; use std::time::Duration; thread::sleep(Duration::from_millis(self.get_rand_duration())); self.counter += 1; if self.counter % 2 == 0 { for observer in self.observers.iter() { observer.notify(&self.counter); } } } } fn get_rand_duration(&self) -> u64 { if cfg!(target_os = "windows") { 500u64 } else { use std::process::Command; use std::str::FromStr; let rand_cmd = Command::new("sh") .arg("-c") .arg("echo $(( $RANDOM%1000 + 1 ))") .output() .expect("failed to get OS RNG"); u64::from_str(&String::from_utf8(rand_cmd.stdout).unwrap().trim()).unwrap() } } } impl Observable<u32> for EvenCounter { fn register(&mut self, observer: Box<Observer<Item = u32>>) { self.observers.push(observer); } } /// the specific Observer type pub struct EvenObserver { name: String, } impl EvenObserver { pub fn new(name: String) -> Self { EvenObserver { name: name } } fn name(&self) -> &str { &self.name } } impl Observer for EvenObserver { type Item = u32; fn notify(&self, val: &Self::Item) { println!("{} got {}", self.name(), val); } }
As you can see, the types themselves are implemented as traits, and then the specific concrete types used for the example implement these traits.
The Observable
type is quite simple – it is generic over the type that its observers can get notified about. Registration of observers is done through the required method, register
. Also note the Box
type being used here instead of a real trait object. Sometimes it’s much easier just to box
things up than bothering about lifetimes and such.
The Observer
type is more interesting. It has a single method, notify
which the observable object uses to call the observers. The type of value that is retrieved from the observable object is made an “Associate Type” of the Observer
trait. I find this to be a much cleaner approach than defining the trait itself to be parameterized over a generic type.
A simple test run:
Macushla:rust-observer z0ltan$ cargo run Compiling rust_observer v0.1.0 (file:///Users/z0ltan/Projects/Playground/DesignPatterns/Observer/Rust/rust-observer) Finished dev [unoptimized + debuginfo] target(s) in 0.61 secs Running `target/debug/rust_observer` bar got 2 baz got 2 quux got 2 bar got 4 baz got 4 quux got 4 bar got 6 baz got 6 quux got 6 bar got 8 baz got 8 quux got 8 bar got 10 baz got 10 quux got 10 bar got 12 baz got 12 quux got 12 bar got 14 baz got 14 quux got 14 ^C
Very satisfying indeed!