Sling Academy
Home/Golang/Implementing Observer Patterns Using Interfaces in Go

Implementing Observer Patterns Using Interfaces in Go

Last updated: November 26, 2024

In software design, the Observer pattern is a behavioral design pattern where an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. The Observer pattern is used to create a one-to-many dependency between objects where if one object changes state, all its dependents are notified.

Basic Implementation of Observer Pattern

Let's start with a basic implementation of the Observer pattern in Go. We'll create an example of a simple weather station that observers can listen to.

package main

import "fmt"

// The Subject interface that maintains the list of observers.
type Subject interface {
    RegisterObserver(o Observer)
    RemoveObserver(o Observer)
    NotifyObservers()
}

// The Observer interface that should be implemented by observers.
type Observer interface {
    Update(temp, humidity, pressure float64)
}

// WeatherData holds data and the observers.
type WeatherData struct {
  temperature, humidity, pressure float64
  observers []Observer
}

// RegisterObserver adds an observer to the list.
func (wd *WeatherData) RegisterObserver(o Observer) {
  wd.observers = append(wd.observers, o)
}

// RemoveObserver removes an observer from the list.
func (wd *WeatherData) RemoveObserver(o Observer) {
  for i, observer := range wd.observers {
    if observer == o {
      wd.observers = append(wd.observers[:i], wd.observers[i+1:]...)
    }
  }
}

// NotifyObservers calls the Update method of each observer.
func (wd *WeatherData) NotifyObservers() {
  for _, observer := range wd.observers {
    observer.Update(wd.temperature, wd.humidity, wd.pressure)
  }
}

Intermediate Implementation with Concrete Observers

Next, let us create concrete observers that will react to changes in weather data. We can implement multiple observers with different responsibilities.

// CurrentConditionsDisplay implements the Observer interface.
type CurrentConditionsDisplay struct {
  temperature, humidity float64
}

func (cc *CurrentConditionsDisplay) Update(temp, humidity, pressure float64) {
  cc.temperature = temp
  cc.humidity = humidity
  fmt.Printf("Current conditions: %.2fF degrees and %.2f%% humidity\n", temp, humidity)
}

// StatisticsDisplay implements the Observer interface.
type StatisticsDisplay struct {
  maxTemp, minTemp, tempSum float64
  numReadings int
}

func (sd *StatisticsDisplay) Update(temp, humidity, pressure float64) {
  sd.tempSum += temp
  sd.numReadings++
  if temp > sd.maxTemp {
    sd.maxTemp = temp
  }
  if sd.minTemp == 0 || temp < sd.minTemp {
    sd.minTemp = temp
  }

  fmt.Printf("Avg/Max/Min temperature = %.2f/%.2f/%.2f\n", 
    sd.tempSum/float64(sd.numReadings), sd.maxTemp, sd.minTemp)
}

Advanced Implementation

Finally, we can see how all these components integrate together to form a complete Observer design pattern-based system.

func main() {
  weatherData := &WeatherData{}

  currentDisplay := &CurrentConditionsDisplay{}
  statsDisplay := &StatisticsDisplay{}

  weatherData.RegisterObserver(currentDisplay)
  weatherData.RegisterObserver(statsDisplay)

  // Simulate new weather measurements
  weatherData.temperature = 80
  weatherData.humidity = 65
  weatherData.pressure = 30.4
  weatherData.NotifyObservers()

  // Updated weather measurements
  weatherData.temperature = 82
  weatherData.humidity = 70
  weatherData.pressure = 29.2
  weatherData.NotifyObservers()
}

In this code, we've implemented an Observer pattern where our WeatherData acts as the subject, and the CurrentConditionsDisplay and StatisticsDisplay act as different observers. When weather data is updated through the NotifyObservers method, each observer updates accordingly.

This design pattern's advantage is that it keeps the subject and observers loosely coupled. The subject only knows the list of observers, while observers only know the subject's updates when they get triggered via their Update method.

Next Article: Using Interfaces to Abstract File and Network I/O in Go

Previous Article: Exploring the `error` Interface: Building Custom Error Types in Go

Series: Structs and Interfaces in Go

Golang

Related Articles

You May Also Like

  • How to remove HTML tags in a string in Go
  • How to remove special characters in a string in Go
  • How to remove consecutive whitespace in a string in Go
  • How to count words and characters in a string in Go
  • Relative imports in Go: Tutorial & Examples
  • How to run Python code with Go
  • How to generate slug from title in Go
  • How to create an XML sitemap in Go
  • How to redirect in Go (301, 302, etc)
  • Using Go with MongoDB: CRUD example
  • Auto deploy Go apps with CI/ CD and GitHub Actions
  • Fixing Go error: method redeclared with different receiver type
  • Fixing Go error: copy argument must have slice type
  • Fixing Go error: attempted to use nil slice
  • Fixing Go error: assignment to constant variable
  • Fixing Go error: cannot compare X (type Y) with Z (type W)
  • Fixing Go error: method has pointer receiver, not called with pointer
  • Fixing Go error: assignment mismatch: X variables but Y values
  • Fixing Go error: array index must be non-negative integer constant