Notes: Go Design Patterns - Observer Pattern

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

ยท

4 min read

Table of contents

No heading

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()
}
ย