Observer Pattern
Observer is a behavioral design pattern that allows some objects to notify other objects about changes in their state.
The Observer pattern provides a way to subscribe and unsubscribe to and from these events for any object that implements a subscriber interface.
Golang Implementation
Example: E-Commerce Website pushes notification
In the e-commerce website, items go out of stock from time to time. There can be customers who are interested in a particular item that went out of stock. There are three solutions to this problem:
- The customer keeps checking the availability of the item at some frequency.
- E-commerce bombards customers with all new items available, which are in stock.
- The customer subscribes only to the particular item he is interested in and gets notified if the item is available. Also, multiple customers can subscribe to the same product.
Option 3 is most viable, and this is what the Observer pattern is all about. The major components of the observer pattern are:
- Subject, the instance which publishes an event when anything happens.
- Observer, which subscribes to the subject events and gets notified when they happen.
subject.go
package main
type Subject interface {
register(observer Observer)
deregister(observer Observer)
notifyAll()
}
item.go
package main
import "fmt"
type Item struct {
observerList []Observer
name string
inStock bool
}
func newItem(name string) *Item {
return &Item{
name: name,
}
}
func (i *Item) updateAvailability() {
fmt.Printf("Item %s is now in stock\n", i.name)
i.inStock = true
i.notifyAll()
}
func (i *Item) register(o Observer) {
i.observerList = append(i.observerList, o)
}
func (i *Item) deregister(o Observer) {
i.observerList = removeFromslice(i.observerList, o)
}
func (i *Item) notifyAll() {
for _, observer := range i.observerList {
observer.update(i.name)
}
}
func removeFromslice(observerList []Observer, observerToRemove Observer) []Observer {
observerListLength := len(observerList)
for i, observer := range observerList {
if observerToRemove.getID() == observer.getID() {
observerList[observerListLength-1], observerList[i] = observerList[i], observerList[observerListLength-1]
return observerList[:observerListLength-1]
}
}
return observerList
}
observer.go
package main
type Observer interface {
update(string)
getID() string
}
customer.go
package main
import "fmt"
type Customer struct {
id string
}
func (c *Customer) update(itemName string) {
fmt.Printf("Sending email to customer %s for item %s\n", c.id, itemName)
}
func (c *Customer) getID() string {
return c.id
}
main.go
package main
func main() {
shirtItem := newItem("Nike Shirt")
observerFirst := &Customer{id: "abc@gmail.com"}
observerSecond := &Customer{id: "xyz@gmail.com"}
shirtItem.register(observerFirst)
shirtItem.register(observerSecond)
shirtItem.updateAvailability()
}
// Item Nike Shirt is now in stock
// Sending email to customer abc@gmail.com for item Nike Shirt
// Sending email to customer xyz@gmail.com for item Nike Shirt
Rust Implementation
In Rust, a convenient way to define a subscriber is to have a function as a callable object with complex logic passing it to a event publisher.
Example:
In this Observer example, Subscribers are either a lambda function or an explicit function subscribed to the event. Explicit function objects could be also unsubscribed (although, there could be limitations for some function types).
editor.rs
use crate::observer::{Event, Publisher};
/// Editor has its own logic and it utilizes a publisher
/// to operate with subscribers and events.
#[derive(Default)]
pub struct Editor {
publisher: Publisher,
file_path: String,
}
impl Editor {
pub fn events(&mut self) -> &mut Publisher {
&mut self.publisher
}
pub fn load(&mut self, path: String) {
self.file_path = path.clone();
self.publisher.notify(Event::Load, path);
}
pub fn save(&self) {
self.publisher.notify(Event::Save, self.file_path.clone());
}
}
observer.rs
use std::collections::HashMap;
/// An event type.
#[derive(PartialEq, Eq, Hash, Clone)]
pub enum Event {
Load,
Save,
}
/// A subscriber (listener) has type of a callable function.
pub type Subscriber = fn(file_path: String); // fn is keyword for function, here Subscriber is a function type, functions of this type takes a string and returns nothing.
/// Publisher sends events to subscribers (listeners).
#[derive(Default)]
pub struct Publisher {
events: HashMap<Event, Vec<Subscriber>>,
}
impl Publisher {
pub fn subscribe(&mut self, event_type: Event, listener: Subscriber) {
self.events.entry(event_type.clone()).or_default();
self.events.get_mut(&event_type).unwrap().push(listener);
}
pub fn unsubscribe(&mut self, event_type: Event, listener: Subscriber) {
self.events
.get_mut(&event_type)
.unwrap()
.retain(|&x| x != listener);
}
pub fn notify(&self, event_type: Event, file_path: String) {
let listeners = self.events.get(&event_type).unwrap();
for listener in listeners {
listener(file_path.clone());
}
}
}
main.rs
use editor::Editor;
use observer::Event;
mod editor;
mod observer;
fn save_listener(file_path: String) {
let email = "admin@example.com".to_string();
println!("Email to {}: Save file {}", email, file_path);
}
fn main() {
let mut editor = Editor::default();
editor.events().subscribe(Event::Load, |file_path| { // lambda function as input paramter: |args| { body }
let log = "/path/to/log/file.txt".to_string();
println!("Save log to {}: Load file {}", log, file_path);
});
editor.events().subscribe(Event::Save, save_listener); // another function as input parameter
editor.load("test1.txt".into());
editor.load("test2.txt".into());
editor.save();
editor.events().unsubscribe(Event::Save, save_listener);
editor.save();
}
// Save log to /path/to/log/file.txt: Load file test1.txt
// Save log to /path/to/log/file.txt: Load file test2.txt
// Email to admin@example.com: Save file test2.txt