Notes: Go Design Patterns - Observer Pattern
This blog contains the implementation of the Observer design pattern with e-commerce out of stock availability notifier example
Table of contents
No headings in the article.
Observer Pattern as the name suggests is based on observation of something, maybe a state, maybe an event, or something else. It is a behavioral pattern.
In this pattern, we have two actors, publishers and subscribers. Publishers' job is to maintain the list of subscribers by adding or removing the subscribers and also notify them about the required changes that these subscribers expect. So, publishers' interface can look something like this:
type publisher interface {
register(subscriber subscriber)
deregister(subscriber subscriber)
notifyAll()
}
Now, as soon as the publisher notifies subscribers, they need to act on it. So, subscribers can have a simple interface like this:
type subscriber interface {
trigger(context context.Context)
}
Let's try to understand this with an example. Let's build a notifier for Out of Stock products for an e-commerce store. If there is any item that is out of stock, users will be able to subscribe to that item and as soon as it gets available, they'll be notified and users will be able to act on that.
First, we can have an event manager/ publisher who will contain information of product which is being subscribed and the list of its subscribers. It can be something like this:
type stockCheckEventManager struct {
subscribers stockCheckSubscriberMap
productID string
productName string
inStock bool
}
This will implement publisher
interface we mentioned in the beginning.
Next is subscribers. Subscribers are users, so they can be represented like this:
type stockCheckSubscriberObject struct {
userID string
userName string
}
This will implement subscriber
interface mentioned in the beginning.
I'm skipping subscriber addition and removal part as it's not that important. Now, let's look into how things will move.
Now, to start the whole process, first, we call updateStockAvailability
method as soon as stock is available again.
func (em *stockCheckEventManager) updateStockAvailability() {
fmt.Printf("item %v is now in stock\n", em.productName)
em.inStock = true
em.notifyAll()
}
This then calls notifyAll
method to notify all the subscribers:
func (em *stockCheckEventManager) notifyAll() {
if em.inStock {
ctx := context.WithValue(context.Background(), productIDKey, em.productID)
for _, subscriber := range em.subscribers {
subscriber.trigger(ctx)
}
}
}
Now, notifyAll
method goes through all the subscribers and executes the trigger
method:
func (s *stockCheckSubscriberObject) trigger(ctx context.Context) {
productID := ctx.Value(productIDKey)
fmt.Printf("Product: %v back in Stock. Trigger subsequent events like sending mail, browser notification etc for user: %v\n", productID, s.userID)
}
And this ends our execution flow.
Full working code:
package main
import (
"context"
"fmt"
)
type contextKey string
const productIDKey contextKey = "productID"
// central manager interface
type publisher interface {
register(subscriber subscriber)
deregister(subscriber subscriber)
notifyAll()
}
// observer interface
type subscriber interface {
trigger(context context.Context)
}
type stockCheckSubscriber interface {
subscriber
getKey() string
}
type stockCheckSubscriberMap map[string]stockCheckSubscriber
// for subscribers, make sure key is subscriber related
// to avoid subscriber duplication
type stockCheckEventManager struct {
subscribers stockCheckSubscriberMap
productID string
productName string
inStock bool
}
func newStockCheckEventManager(productID string, productName string) *stockCheckEventManager {
subscribers := make(stockCheckSubscriberMap)
return &stockCheckEventManager{
subscribers: subscribers,
productID: productID,
productName: productName,
inStock: false,
}
}
func (em *stockCheckEventManager) register(subscriber subscriber) {
stockCheckSubscriber := subscriber.(stockCheckSubscriber)
em.subscribers[stockCheckSubscriber.getKey()] = stockCheckSubscriber
fmt.Printf("registered %v as subscriber\n", stockCheckSubscriber.getKey())
}
func (em *stockCheckEventManager) deregister(subscriber subscriber) {
stockCheckSubscriber := subscriber.(stockCheckSubscriber)
delete(em.subscribers, stockCheckSubscriber.getKey())
fmt.Printf("deregistered %v as subscriber\n", stockCheckSubscriber.getKey())
}
func (em *stockCheckEventManager) notify(subscribers map[string]subscriber) {
if em.inStock {
ctx := context.WithValue(context.Background(), productIDKey, em.productID)
for _, subscriber := range subscribers {
subscriber.trigger(ctx)
}
}
}
func (em *stockCheckEventManager) notifyAll() {
if em.inStock {
ctx := context.WithValue(context.Background(), productIDKey, em.productID)
for _, subscriber := range em.subscribers {
subscriber.trigger(ctx)
}
}
}
func (em *stockCheckEventManager) updateStockAvailability() {
fmt.Printf("item %v is now in stock\n", em.productName)
em.inStock = true
em.notifyAll()
}
/************************************
**** Stock Check Subscriber Code ****
*************************************/
type stockCheckSubscriberObject struct {
userID string
userName string
}
func newStockCheckSubscribers(userID string, userName string) *stockCheckSubscriberObject {
return &stockCheckSubscriberObject{
userID: userID,
userName: userName,
}
}
func (s *stockCheckSubscriberObject) trigger(ctx context.Context) {
productID := ctx.Value(productIDKey)
fmt.Printf("Product: %v back in Stock. Trigger subsequent events like sending mail, browser notification etc for user: %v", productID, s.userID)
}
func (s *stockCheckSubscriberObject) getKey() string {
return s.userID
}
func main() {
stockCheckSubscriber1 := newStockCheckSubscribers("156", "kshitij")
stockCheckSubscriber2 := newStockCheckSubscribers("528", "nidhi")
stockCheckSubscriber3 := newStockCheckSubscribers("339", "coco")
newStockCheckManager1 := newStockCheckEventManager("107", "shirt")
newStockCheckManager2 := newStockCheckEventManager("108", "skirt")
newStockCheckManager1.register(stockCheckSubscriber1)
newStockCheckManager1.register(stockCheckSubscriber2)
newStockCheckManager2.register(stockCheckSubscriber2)
newStockCheckManager2.register(stockCheckSubscriber3)
newStockCheckManager1.updateStockAvailability()
newStockCheckManager2.deregister(stockCheckSubscriber2)
newStockCheckManager2.updateStockAvailability()
}