Introduction
Today, we will dive into understanding the Observer Design Pattern with a Golang code example. This pattern is a cornerstone of software design, promoting flexibility and reuse in code by managing dependencies effectively.
What Is the Observer Pattern?
The Observer pattern defines a one-to-many dependency between objects, ensuring that when one object (the Subject) changes its state, all its dependents (Observers) are automatically notified. This pattern is pivotal in creating an efficient and decoupled architecture.
When to Use the Observer Pattern?
- When multiple objects (subscribers) need to react to state changes of another object (Subject).
- When you want to decouple the Subject from its Observers.
- When the number of Observers can change dynamically.
- When you want to avoid polling or tight coupling between components.
A Use Case for Backend Development
Consider a user activity tracking feature where user operations, like registration and payments, need to be logged. You also want notifications for these actions. Without a design pattern, the code can become entangled and hard to manage.
type EmailService struct {
emails []string
}
func (e *EmailService) SendEmail(userName string, message string) error {
return nil
}
type DBService struct {
db *sql.DB
}
func (d *DBService) SaveUserActivity(userName string, activity string) error {
return nil
}
type TopUpHandler struct {
emailService *EmailService
dbService *DBService
}
func (h *TopUpHandler) HandleTopUp(userName string, amount float64) error {
// ...
// top up logic
// ...
if err := h.dbService.SaveUserActivity(userName, "topup"); err != nil {
return err
}
if err := h.emailService.SendEmail(userName, "top up successful"); err != nil {
return err
}
return nil
}
Problems With This Approach
- Adding new notification means manually updating every handler to include calls like
h.slackService.SendMessage(). - Removing an existing notification requires removing code from each handler.
- High risk of inconsistency across handlers if updates are missed.
This tightly couples handlers with notifications, leading to complex redundant code.
Implementing Observer Pattern
With the Observer pattern, we first identify the Subject and Observers. Here, the Subject is ActivityService, and Observers include DBService and EmailService. The relevant state change is the top-up operation.
Interface Definitions
// Subject
type Subject interface {
RegisterObserver(observer ...Observer)
DeregisterObserver(observer Observer)
NotifyAll(msg string) error
}
// Observer
type Observer interface {
Update(msg string) error
}
These are the minimal contracts for implementing an Observer pattern. Let’s look at concrete implementations.
Subject
type ActivityService struct {
observers []Observer
mux sync.Mutex
}
func NewActivityService() *ActivityService {
return &ActivityService{
observers: make([]Observer, 0),
mux: sync.Mutex{},
}
}
func (s *ActivityService) RegisterObserver(observer ...Observer) {
s.mux.Lock()
defer s.mux.Unlock()
s.observers = append(s.observers, observer...)
}
func (s *ActivityService) DeregisterObserver(observer Observer) {
s.mux.Lock()
defer s.mux.Unlock()
for i, o := range s.observers {
if o == observer {
s.observers = append(s.observers[:i], s.observers[i+1:]...)
break
}
}
}
func (s *ActivityService) NotifyAll(msg string) error {
s.mux.Lock()
defer s.mux.Unlock()
for _, observer := range s.observers {
if err := observer.Update(msg); err != nil {
return err
}
}
return nil
}
Observers
type MailObserver struct {
emails []string
}
func (m *MailObserver) Update(msg string) error {
fmt.Printf("MailObserver: %s\n", msg)
return nil
}
type LogObserver struct {
db *sql.DB
}
func (l *LogObserver) Update(msg string) error {
fmt.Printf("LogObserver: %s\n", msg)
return nil
}
Putting It All Together
func main() {
// init subject
activityService := NewActivityService()
// init observer
mailObserver := &MailObserver{}
logObserver := &LogObserver{}
// subscribe observer to subject
activityService.RegisterObserver(mailObserver, logObserver)
}
Reacting to State Changes
For a user top-up, you can notify all observers with a single call: activityService.NotifyAll("john just topped up $20").
Adapting to Change
To replace email notifications with Slack messages:
type SlackObserver struct {
channelIDs []string
token string
}
func (s *SlackObserver) Update(msg string) error {
fmt.Printf("SlackObserver: %s\n", msg)
return nil
}
slackObserver := &SlackObserver{}
activityService.DeregisterObserver(mailObserver)
activityService.RegisterObserver(slackObserver)
This flexibility emphasizes the pattern’s power. And changes don’t require modifying the core logic of handlers.
You can find the complete code example here.
Practice
Feeling inspired? Try implementing a Crypto Price Watcher:
Concept
A Go program polls for Bitcoin/Ethereum prices every 2 seconds, notifying Observers (like Discord/Telegram bots) on price changes.
Components:
- Subject:
PriceFeed - Observers:
DiscordNotifier,TelegramNotifier,EmailNotifier
Features:
- Demonstrates real-time updates and observer dynamics.
Feel free to tweak the example with print statements for simulation. An example can be found in my GitHub repository.
The output of the example code is as follows:
Let me know if you are interested in reading more Golang design pattern posts like this.
Enjoyed this article? Support my work with a coffee ☕ on Ko-fi.