Simple Mastery: Unveiling the Intricacies of Binding Properties in Efficient Rust Programming

Photo by Lukas: https://www.pexels.com/photo/multicolored-mixing-console-306088/

Introduction

Especially in multi-threaded applications it can be necessary to synchronize properties between objects, or at least be notified of changes on certain properties. This is where the Binding Properties comes in, which is basically a form of the Observer pattern.

In this pattern, observers can subscribe to a special ‘event’-handler on an object. When a property changes, all the subscribers, if any, will be notified.

The flow is as follows:

  1. An object with bindable properties is created
  2. One or more observers subscribe to this object
  3. If a property changes in this object, the subscribers get notified and handle the change accordingly

Implementation in Rust

In this example we will build a simple Person struct. In our world, a person just has a name and an age. This struct has a number, or as the case may be no, observers which will be notified any time a property changes:

use std::sync::{Arc, Mutex};
use std::thread;



pub struct Person {
    name: String,
    age: i32,
    observers:Vec<Arc<Mutex<dyn Fn(&str)+Send+Sync>>>
}

impl Person {
    pub fn new(name: String, age: i32) -> Person {
        Person {
            name,
            age,
            observers:Vec::new()
        }
    }

    pub fn get_name(&self) -> &String {
        &self.name
    }

    pub fn set_name(&mut self, name: String) {
        self.name = name;
        self.notify_observers("name");
    }

    pub fn get_age(&self) -> i32 {
        self.age
    }

    pub fn set_age(&mut self, age: i32) {
        self.age = age;
        self.notify_observers("age");
    }

    pub fn subscribe<F>(&mut self, f: F)
        where F: Fn(&str) + 'static + Send+Sync
    {
        self.observers.push(Arc::new(Mutex::new(f)));
    }

    fn notify_observers(&self, property_name: &str) {
        for observer in &self.observers {
            if let Ok(observer)=observer.lock(){
                observer(property_name);
            }
        }
    }
}

Notice how the observers are wrapped in an Arc and a Mutex because:

  1. The Arc makes sure multiple references can safely hold references to an observer. It also makes sure that the observer stays alive as long as there is at least one reference to it.
  2. Using the Mutex lock ensures exclusive access to the observer, preventing data-races if multiple threads try to notify the observers.

Testing time

Let’s test our simple setup:

fn main() {
    let person = Arc::new(Mutex::new(Person::new("Test".to_string(), 55)));

    // Clone the Arc to pass a reference to the new thread
    let person_clone = Arc::clone(&person);
    let handle = thread::spawn(move || {
        // Lock the Mutex to access the data
        let mut person = person_clone.lock().unwrap();
        person.subscribe(move |property_name| {
            println!("subthread: {} changed", property_name);
        });
        person.set_name("Jane".to_string());
        person.set_age(21);
    });
    person.lock().unwrap().subscribe(move |property_name| {
        println!("main thread: {} changed", property_name);
    });
    person.lock().unwrap().set_name("John".to_string());
    handle.join().unwrap();
}

A short explanation:

  1. We create a Person struct wrapped in a Mutex and an Arc to make sure it can be safely shared among threads.
  2. We clone this struct
  3. Next, we spawn a thread and then in this thread we:
    • create an unlocked clone of the original person
    • Add an observer to it
    • Change the person’s properties.
  4. We also add an observer in the main thread, and change a property.
  5. Then we wait for the thread to end using a call to the join() method.

As you can see, the observer gets notified about the change in properties. One thing to note is that this only works if we change the properties through the set_ and get_ methods.

Conclusion

This was not a very straightforward pattern to implement, mainly due to problems with borrowing and ownership. The example is very simple. Possible enhancnements would be:

  1. Make sure the observer-function also gets the new value of the property
  2. If it is possible, implement this pattern using macros.

Both enhancements will be the subject of further investigations on this blog.

Leave a Reply

Your email address will not be published. Required fields are marked *