One of the fundamental software behavioral patterns discussed in the Gang of Four’s Design Patterns, the observer pattern is very useful for maintaining one-to-many relationships between objects.1 As such, it is typically implemented in OOP languages such as C++, Java, Rust, etc. This article will explore the motivation behind using the observer pattern, and an implementation will be provided in Rust.
Motivation for Use
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.
Implementation
While the observer pattern can be implemented in multiple languages, this article will demonstrate it using Rust. The implementation consists of two entities: a subject and an observer. The original source code from which the following implementation is derived can be found in Chapter 5 of Idiomatic Rust: Code like a Rustacean by Brenden Matthews.2
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.
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.
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:
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 observer1observed subject with state="init" in observer2
Conclusion
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.
All code for this and other posts can be found on GitHub. Be sure to subscribe for post notifications, and check out my other work on my personal GitHub profile.
References
- Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. pp. 293ff. ISBN 0-201-63361-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 ↩︎