The Observer Pattern using Trait Callbacks
In this lesson, you will learn how to build decoupled event systems where objects notify a list of "observers" whenever their internal state changes.
The Problem: Tight Coupling
In a typical application, you might want a UserDatabase to notify a Logger and an EmailService whenever a new user signs up. If you hard-code these calls, your UserDatabase becomes impossible to maintain as you add more services. Instead, we use the Observer Pattern.
The Observer Interface
We define a trait that any "observer" must implement. This ensures the subject doesn't need to know the concrete type of who is listening—only that they implement the on_user_created method.
trait UserObserver {
fn on_user_created(&self, username: &str);
}
Implementing the Subject
The UserDatabase maintains a list of objects that implement our trait. Because we want to store different types of observers in the same list, we store them as Trait Objects (Box<dyn UserObserver>).
struct UserDatabase {
observers: Vec<Box<dyn UserObserver>>,
}
impl UserDatabase {
fn add_observer(&mut self, observer: Box<dyn UserObserver>) {
self.observers.push(observer);
}
fn create_user(&self, username: &str) {
// Perform logic...
// Notify all registered observers
for observer in &self.observers {
observer.on_user_created(username);
}
}
}
Why This Works
By using a trait callback, the UserDatabase treats all observers uniformly. You could create a SlackNotifier or a MetricsCollector tomorrow, and as long as they implement UserObserver, the UserDatabase will work with them without a single line of code changing in its own implementation. This is the definition of building for extension: your system logic remains closed for modification but open for new functionality.
When the state inside the subject changes, it simply iterates through its internal collection of trait objects and invokes the callback, passing along the relevant data. This creates a clean, event-driven flow where the "Subject" doesn't care who is listening, only that they satisfy the contract defined by your trait.
Key Takeaway: The Observer pattern uses trait objects to decouple event sources from their consumers, allowing you to add unlimited side-effects to a system without modifying the source code of the subject.