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:
- An object with bindable properties is created
- One or more observers subscribe to this object
- 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:
- 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. - 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:
- We create a
Person
struct wrapped in aMutex
and anArc
to make sure it can be safely shared among threads. - We clone this struct
- 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.
- We also add an observer in the main thread, and change a property.
- 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:
- Make sure the observer-function also gets the new value of the property
- If it is possible, implement this pattern using macros.
Both enhancements will be the subject of further investigations on this blog.