The Observer Pattern

As previously mentioned in the introduction, the primary purpose for the observer pattern is for establishing an idiomatic, reusable pattern for managing one-to-many object dependencies. This is commonly seen in event-driven programming, i.e., GUI applications.

The working principle is that an object (which will be referred to as the subject) holds a list of objects that depend on it; these objects will be referred to as observers. If a change occurs that affects the state of the subject, and this change is something that the observers should be aware of, the subject will actively notify the observers of the change. This notification process typically involves the subject calling one of the observers’ methods.

This notify/listen implementation seems pretty similar to another popular pattern — the publish-subscribe pattern — but the big difference is that pub-sub will have some sort of middleware or intermediary that handles the transmission of notifications. The observer pattern has no such middleware; communication between the observers and the subject is direct.

To implement the observer pattern in Rust, let’s create a Rust crate called “observer”. I will be using the 2024 edition of the Rust standard.

~ cargo new --bin --edition 2024 --verbose observer

Now, let’s set up our project structure.

observer
├── src
│ ├── main.rs
│ ├── observer.rs
│ └── subject.rs
└── Cargo.toml

In our crate, we have the main.rs file along with observer.rs containing the trait definitions for an observer and subject.rs containing the structure definitions for the subject.

observer.rs
Rust
pub trait Observer {
type Subject;
fn observe(&self, subject: &Self::Subject);
}
pub trait Observable {
type Observer;
fn update(&self);
fn attach(&mut self, observer: Self::Observer);
fn detach(&mut self, observer: Self::Observer);
}

Using Rust’s trait system, we define two traits in observer.rs: Observer for objects that observe other objects and Observable for objects that are able to be observed. The Observer trait maintains a type called Subject; this is how an observer keeps track of the subject it is observing. Likewise, the Observable trait maintains a type called Observer, which refers to an object that implements the Observer trait. The subject will maintain a list of observers so that it may update them whenever a state change or event occurs.

The Observable::update() function will be used to invoke the subject’s observers to perform their observation tasks. Given that this is a trait declaration, subjects that implement this trait must provide their own implementations of the update function, meaning the logic can be as simple or as complex as required by the use case. Observable::attach(...) and Observable::detach(...) allow the subject to choose which observers are allowed to observe it. An observer can be detached from the subject at any time; once detachment occurs, that observer will no longer be “notified” that the subject’s state changed.

Below is an example design of a basic Subject that implements the Observable trait.

subject.rs
Rust
use crate::observer::{Observable, Observer};
use std::sync::{Arc, Weak};
pub struct Subject {
observers: Vec<Weak<dyn Observer<Subject = Self>>>,
state: String,
}
impl Observable for Subject {
type Observer = Arc<dyn Observer<Subject = Self>>;
fn update(&self) {
self.observers
.iter()
.flat_map(|o| o.upgrade())
.for_each(|o| o.observe(self));
}
fn attach(&mut self, observer: Self::Observer) {
self.observers.push(Arc::downgrade(&observer));
}
fn detach(&mut self, observer: Self::Observer) {
self.observers
.retain(|f| !f.ptr_eq(&Arc::downgrade(&observer)));
}
}
impl Subject {
pub fn new(state: &str) -> Self {
Self {
observers: vec![],
state: state.into(),
}
}
pub fn state(&self) -> &str {
self.state.as_ref()
}
}

The subject maintains a vector of weak pointers to its observers along with a state string. The state is just for demonstration purposes; in this implementation, observers are able to observe state once the subject invokes update. Implementations are defined for the functions declared by the Observable trait as well: attach pushes a weak pointer to an observer to the observers vector, and detach invokes retain to remove all instances of an observer. The update function iterates over the vector of observers by getting an iterator, upgrading the weak pointers to std::rc::Rc pointers, and then calling the observe function on each observer.

Now, let’s put this code into action with a working example. In our main.rs file, let’s add the following:

main.rs
Rust
mod observer;
mod subject;
use crate::observer::{Observable, Observer};
use crate::subject::Subject;
use std::sync::Arc;
struct ExampleObserver {
name: String,
}
impl ExampleObserver {
fn new(name: &str) -> Arc<Self> {
Arc::new(Self { name: name.into() })
}
}
impl Observer for ExampleObserver {
type Subject = Subject;
fn observe(&self, subject: &Self::Subject) {
println!(
"observed subject with state={:?} in {}",
subject.state(),
self.name
);
}
}
fn main() {
let mut subject = Subject::new("init");
let observer1 = ExampleObserver::new("observer1");
let observer2 = ExampleObserver::new("observer2");
subject.attach(observer1.clone());
subject.attach(observer2.clone());
subject.update();
subject.detach(observer1);
subject.detach(observer2);
}

We define an ExampleObserver which implements the Observer trait. In this instance, the observer’s observe function simply prints the current state of its associated subject to the terminal.

In the main function, we create a subject with an initial state value of "init". We then instantiate two ExampleObserver objects that are subsequently “attached” to subject. Once attached, subject invokes update, and then subject detaches the observers from itself.

Compiling and executing this code should result in this output:

~ cargo run --release
Compiling observer v0.1.0 (/home/user/shared_ptr/patterns/observer)
Finished `release` profile [optimized] target(s) in 0.25s
Running `target/release/observer`
observed subject with state="init" in observer1
observed subject with state="init" in observer2

The observer pattern is a classic software design pattern that is useful for maintaining one-to-many dependencies between objects. It is an extremely common pattern in systems that require responsiveness and flexibility, especially event-driven systems such as user interfaces. Like many software design patterns, it can be an invaluable addition to your development workflow to enable fast and efficient implementation of applicable systems.

  1. Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. pp. 293ffISBN 0-201-63361-2. ↩︎
  2. Matthews, B.. (2024). Rust Design Patterns. Manning.. Retrieved from http://books.google.co.in/books?id=34Wz0AEACAAJ&dq=isbn:9781633437463&hl=&source=gbs_api ↩︎


Discover more from shared_ptr

Subscribe now to keep reading and get access to the full archive.

Continue reading