Woojiq
Woojiq

True Observer Pattern with Unsubscribe mechanism using Rust

2023-02-17

  1. Will learn the design patterns and become cool, honestly
  2. I like Rust, cause it doesn't have full OOP... how to implement this OOP feature?
  3. Um, what's the problem?
  4. Sweeter bolder better
  5. Observer
  6. Println!("works, lol!")
  7. What we achieved
  8. Hell Yes
  9. Revisions

In memory of all the Rustaceans who stopped learning OOP patterns due to the inability to implement an unsubscribe mechanism in the Observer pattern.

Will learn the design patterns and become cool, honestly

Idk, how it is with healthy people, but I usually want to finish something, even if I don't need it right now. E.g, read the entire "Programming Rust" book without practicing the knowledge gained, just read it. The main thing is that I read it and can mark it as completed. The same thing happened once when I wanted to learn about all design patterns. Of course not All, I found the resource Refactoring Guru and set myself the goal: "understand all patterns and implement them". And now I have a course at the university where we are taught the most popular ones, so I have a lot of time to kill without regret. But my goal sounds boring, doesn't it? So to rack my brain a bit I wanted to implement all patterns in a not True OOP language. Rust. Actually, I recently started learning Rust and thought it'd be a good idea. After coding 5 of them I realized that "Refactoring guru" wasn't giving me the full picture of design patterns. I read a bit of Reddit and found the book "Head First Design Patterns". After one chapter I realized that this book is what I need. And now the key objective of this article - the Observer pattern - enters the scene. There were no problems with it, it is a very convenient and useful pattern, but I wouldn't have written so much of a preface if I could easily implement this pattern on Rust.

I like Rust, cause it doesn't have full OOP... how to implement this OOP feature?

The first month with Rust made me wonder how such a perfect thing is available in our horrible world: no full OOP, error handling, none, match, etc. But for some reason 🙃, when you want to implement a pattern, as it is shown in the book, you suddenly start to lack inheritance and abstract classes... Almost everything can be done using generics, but it isn't interesting, I want it to be like in the book:) It turned out that porting Java to Rust isn't an easy task. Well, it is simple, but up to a certain point. And this point is a comparison of two trait objects. Not sure that you can say trait objects, since trait isn't the same as interface. Therefore &dyn Lol == &dyn Lol doesn't work, but I'd like to. Fortunately, Rust becomes very nice as long you don't insult they, and accept their rules of the game.

In this article, I'll tell you how I gathered bits of information from the web and found the best way to compare two trait objects (don't tell me it is obvious, I'll panic). And since I needed it to implement the Observer pattern, I'll show its implementation as if Rust were the True OOP language.

Um, what's the problem?

Observer is a simple pattern, that can be understood and used in 15 minutes. It works perfectly with C#, Java, and other garbage languages 🤗. However, all the online resources about Rust I've found are missing one feature of the Observer pattern that pissed me off - Unsubscribe mechanism. Article owners completely ignore it or implement it through generics (boring and not very flexible). So I decided to dive in and implement it myself as the discoverer. In order to write an unsubscribe mechanism, we need to somehow compare two trait objects. We don't need to do a deep comparison by value because all we need to know at unsubscribe is whether the two objects are the same by reference.

The most I've come across is this discussion but it's long and not exactly what we need. There is a smaller version of the discussion in the blog. But don't get me wrong, double dynamic dispatch is useful if you want to implement a PartialEq for a trait, but there is an easier solution for the Observer pattern since we don't need to compare field by field, we need to compare by reference.

Sweeter bolder better

🤥
In the observer pattern, especially for unsubscribe, there is no need to compare complete objects (field by field), all we need to know is whether the objects point to the same memory location. In early versions of this article, I used uuid crate for this task, but with the help of community I learned that raw pointers can simplify code. It's also worth noting that using double dynamic dispatch in the case of the Observer pattern is completely wrong, even though I thought the opposite :) There is a possibility that all the struct fields will be the same, but technically they are different observers since they were created separately. It will look something like this:

fn main() {
    let a = &A::new() as &dyn Foo;
    let b = &A::new() as &dyn Foo;
    assert_ne!(a as *const dyn Foo, b as *const dyn Foo);
    assert_eq!(a as *const dyn Foo, a as *const dyn Foo);
}
trait Foo {}
struct A;
impl A {
    fn new() -> Self {}
}
impl Foo for A {}

Now if you were only interested in comparing trait objects as pointers you can close this article, below I'll show a complete implementation of the Observer pattern with lots of traits and no KISS.

Observer

I assume you know what Observer is. In two words: some objects subscribe (they are Observers) to news from other objects (they are Subjects). When Subjects change their state, they call a certain Observers method and pass information. I'll implement the pattern using an example from the book "Head First".

The task sounds like this: The three players in the system are the weather station (the physical device that acquires the actual weather data), the WeatherData object (that tracks the data coming from the Weather Station and updates the displays), and the display that shows users the current weather conditions. The WeatherData object knows how to talk to the physical Weather Station, to get updated data. The WeatherData object then updates its displays for the three different display elements: Current Conditions (shows temperature, humidity, and pressure), Weather Statistics, and a simple forecast.

Uml diagram

There are two ways to implement the pattern: "push Observer" and "pull Observer". "Push" is when Subjects pass their data to the common interface method update(data1, data2). "Pull" is when Subjects pass themselves to the common interface method, so every Observer can get data that it needs via API update(&I). The second option is considered better, because you won't need to change the signature of all methods when adding a new parameter. I show you "Pull".

First, we need to define a trait for Subjects - objects that produce some data. In our case, observers want to get weather data, so we also create some API for them:

trait Subject {
    fn register_observer(&mut self, observer: Weak<RefCell<dyn Observer>>);
    fn remove_observer(&mut self, observer: Rc<RefCell<dyn Observer>>);
    fn notify_observer(&mut self);

    fn get_temperature(&self) -> f32;
    fn get_humidity(&self) -> f32;
}

Rc<RefCell<dyn Observer>>: we want to notify followers and they probably want to change their state too, so we need interior mutability.
Weak<RefCell<dyn Observer>>: subjects do not own their observers, so we have only weak references to them. The observer can be removed at runtime. If this happens, we will simply remove the non-existent reference.

Create one Subject that will report the current temperature:

#[derive(Default)]
struct WeatherData {
    temperature: f32,
    humidity: f32,
    pressure: f32,
    observers: Vec<Weak<RefCell<dyn Observer>>>,
}

And its implementation. Actually set_measurements is only needed to test our application, in real life data will be obtained from another source:

impl WeatherData {
    fn new() -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(WeatherData::default()))
    }
    fn set_measurements(&mut self, temp: f32, hum: f32, pres: f32) {
        ...
        self.measurements_changed();
    }
    fn measurements_changed(&mut self) {
        self.notify_observer();
    }
}

Subject implementation for WeatherData:

impl Subject for WeatherData {
    fn register_observer(&mut self, observer: Weak<RefCell<dyn Observer>>) {
        self.observers.push(observer);
    }

    fn remove_observer(&mut self, observer: Rc<RefCell<dyn Observer>>) {
        self.observers.retain(|obj| {
            obj.upgrade().map_or(false, |left_obs| {
                &*left_obs.borrow() as *const dyn Observer
                    != &*observer.borrow() as *const dyn Observer
            })
        })
    }

    fn notify_observer(&mut self) {
        // Removing references to dropped observers
        self.observers.retain(|obj| obj.upgrade().is_some());

        for observer in self.observers.iter() {
            observer
                .upgrade()
                .expect("old links have been deleted")
                .borrow_mut()
                .update(self.temperature, self.humidity, self.pressure);
        }
    }
    
    fn get_temperature(&self) -> f32 {
        self.temperature
    }
    fn get_humidity(&self) -> f32 {
        self.humidity
    }
}

Each time before removing or notifying observers we delete nonexistent references (converting from Weak to Rc returns None: weak.upgrade().is_some(). All implementations of the Observer pattern that I've seen have had no option to unsubscribe from subject. I did it 🙂. In some cases, the generic vector observers: Vec<impl Foo> was used, and in some cases, this option was completely ignored, which is pointless, since it is one of the power features of the pattern.

Let's understand what's going on here:

self.observers.retain(|obj| {
    obj.upgrade().map_or(false, |left_obs| {
        &*left_obs.borrow() as *const dyn Observer
            != &*observer.borrow() as *const dyn Observer
    })
})

We are removing a particular observer from our list of subscribers. retain leaves only those elements in the vector for which we return true. After checking that the pointer to observer still exists - upgrade, we borrow the value from RefCell, and since it doesn't return &dyn Observer, but returns Ref<'_, T>, we need to dereference that value manually. To get the actual pointer, we need to convert our dyn Observer to &dyn Observer, and only after all these operations does Rust perform the cast to *const dyn Observer.

Let's move on to the Observer trait. Note that we pass only a reference to a Subject object to get only the data we need (Pull Observer):

trait Observer {
    fn update(&mut self, subject: &dyn Subject);
}

And one more trait but this is for our specific task (and to freak out). There is a lot of code outside of the scope of this article, most of which is boilerplate code for our current specific task:

trait DisplayElement {
    fn display(&self);
}

This trait will help us display the current weather (or not only the weather) in different formats.

And structure that has Observer and DisplayElement traits together:

struct CurrentConditionsDisplay {
    weather_data: Weak<RefCell<dyn Subject>>,
    temperature: f32,
    humidity: f32,
}
impl CurrentConditionsDisplay {
    fn new(weather_data: Rc<RefCell<dyn Subject>>) -> Rc<RefCell<Self>> {
        let obj = Rc::new(RefCell::new(Self {
            weather_data: Rc::downgrade(&weather_data),
            temperature: 0.0,
            humidity: 0.0,
        }));
        weather_data
            .borrow_mut()
            .register_observer(Rc::downgrade(&obj) as Weak<RefCell<dyn Observer>>);
        obj
    }
}

Everything happens here: a new object is created and moved to two smart pointers so that you don't have to do it manually every time in the main; subscribe to the weather feed. We keep a reference to dyn Subject so that you can unsubscribe from news in the future, but I did not write such a method, we will do it manually later.

And the last part of Observer pattern, nothing special:

impl Observer for CurrentConditionsDisplay {
    fn update(&mut self, subject: &dyn Subject) {
        self.temperature = subject.get_temperature();
        self.humidity = subject.get_humidity();
        self.display()
    }
}
impl DisplayElement for CurrentConditionsDisplay {
    fn display(&self) {
        println!(
            "Current conditions: {} F degrees and {} humidity.",
            self.temperature, self.humidity
        );
    }
}

Println!("works, lol!")

And in the old tradition of design patterns, we only test this with print statements. But in this case, it will not be very easy to do, because we have a lot of smart pointers, so we also have to be smart 👁👄👁 to turn them into something human.

let weather = WeatherData::new();
let display1 = CurrentConditionsDisplay::new(weather.clone());
let display2 = StatisticsDisplay::new(weather.clone());

println!("Set #1:");
weather.borrow_mut().set_measurements(10.0, 15.0, 20.0);

display1
    .borrow()
    .weather_data
    .upgrade()
    .unwrap()
    .borrow_mut()
    .remove_observer(display1.clone());
...

To unsubscribe from the news, we had to first borrow data from RefCell, then try to convert Weak -> Rc, then borrow the object again as mutable, because after unsubscribing the notifier changes its state. And only then call an unsubscribe with a cloned Rc pointer.

We run it and see that the unsubscribe works, now the first object does not receive new weather data:

Set #1:
Current conditions: 10 F degrees and 15 humidity.
Avg/Min/Max temperature = 10/10/10.
Set #2:
Avg/Min/Max temperature = 15/10/20.
Set #3:

"Avg/Min/Max" - this is the output of the second object StatisticsDisplay, the code of which I did not show because it is similar to the CurrentConditionsDisplay. I also removed it from the news before Set #3.

What we achieved

Hell Yes

Huh, I don't know why I showed so much code when I could just comment out the code and put it on the Rust Playground. Feel free to submit corrections. Link to the code in the Revisions section. Maybe because I wanted to write my first article, which will receive many corrections before it becomes "correct". Because at the time of writing the article, I have only been studying this grail for 2 months. Therefore, if you saw any syntax error (I hate English articles), or my ignorance in some Rust issue, please contact: Reddit post or yurii.shymon@gmail.com.

Cheers 🦆


Revisions