Menu
Coddy logo textTech

Observer Pattern

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 89 of 107.

The Observer pattern defines a one-to-many relationship between objects. When one object (the subject) changes state, all its dependents (observers) are notified automatically. This is ideal for event systems, notifications, or any scenario where multiple components need to react to changes.

In Go, we implement this pattern using an interface for observers and a subject struct that manages subscriptions:

type Observer interface {
    Update(event string)
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Subscribe(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Notify(event string) {
    for _, observer := range s.observers {
        observer.Update(event)
    }
}

Any type implementing the Observer interface can subscribe to receive notifications:

type EmailAlert struct{ Address string }

func (e EmailAlert) Update(event string) {
    fmt.Printf("Email to %s: %s\n", e.Address, event)
}

type Logger struct{}

func (l Logger) Update(event string) {
    fmt.Println("Log:", event)
}

The subject notifies all observers without knowing their concrete types:

subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})

subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placed

This pattern decouples the subject from its observers, making it easy to add new observer types without modifying existing code. It's commonly used in event-driven architectures, GUI frameworks, and pub/sub messaging systems.

challenge icon

Challenge

Easy

Let's build a stock price alert system using the Observer pattern! You'll create a system where multiple alert services automatically receive notifications whenever a stock price changes—perfect for demonstrating how observers can react to events without the subject knowing their concrete types.

You'll organize your code across three files:

  • observer.go: Define your observer interface and concrete observer types.

    Create an Observer interface with a method Update(stockName string, price float64) that receives stock information when prices change.

    Implement two observer types:

    • PriceAlert with a Threshold field (float64)—when updated, it returns ALERT: [stockName] at $[price] crossed threshold $[threshold] if the price is above the threshold, or WATCHING: [stockName] at $[price] (threshold: $[threshold]) if below
    • PriceLogger with a LogName field (string)—when updated, it returns [logName] recorded [stockName]: $[price]

    Both observers should have a method that returns their formatted response string.

  • stock.go: Create your subject that manages observers and notifies them of price changes.

    Build a Stock struct with fields for Name (string), Price (float64), and a slice of observers. Add these methods:

    • Subscribe(o Observer) to add an observer
    • SetPrice(price float64) []string that updates the price and notifies all observers, returning a slice of all their responses

    Also create a NewStock(name string, initialPrice float64) *Stock constructor.

  • main.go: Set up your stock with observers and simulate price changes.

    Read the stock name and initial price. Then read a count of observers to create. For each observer, read its type (alert or logger) and its configuration (threshold for alerts, log name for loggers). Subscribe each observer to the stock.

    Finally, read a new price, update the stock, and print each observer's response on its own line.

The following inputs will be provided:

  • Line 1: Stock name
  • Line 2: Initial price
  • Line 3: Number of observers
  • For each observer: type (alert or logger), then configuration value
  • Final line: New price to set

Format all prices with two decimal places.

For example, given:

GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50

Your output should be:

ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00

And given:

AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25

Your output should be:

DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)

Notice how the Stock doesn't know whether it's notifying alerts or loggers—it just calls Update on each observer. This decoupling makes it easy to add new observer types without modifying the Stock code!

Cheat sheet

The Observer pattern defines a one-to-many relationship where a subject notifies multiple observers automatically when its state changes. This decouples the subject from its observers, making it ideal for event systems and notifications.

Basic Structure

Define an observer interface and a subject that manages subscriptions:

type Observer interface {
    Update(event string)
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Subscribe(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Notify(event string) {
    for _, observer := range s.observers {
        observer.Update(event)
    }
}

Implementing Observers

Any type implementing the Observer interface can subscribe:

type EmailAlert struct{ Address string }

func (e EmailAlert) Update(event string) {
    fmt.Printf("Email to %s: %s\n", e.Address, event)
}

type Logger struct{}

func (l Logger) Update(event string) {
    fmt.Println("Log:", event)
}

Usage

Subscribe observers and notify them of changes:

subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})

subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placed

The subject notifies all observers without knowing their concrete types, making it easy to add new observer types without modifying existing code.

Try it yourself

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Read stock name
	stockName, _ := reader.ReadString('\n')
	stockName = strings.TrimSpace(stockName)

	// Read initial price
	initialPriceStr, _ := reader.ReadString('\n')
	initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)

	// Read number of observers
	numObserversStr, _ := reader.ReadString('\n')
	numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))

	// TODO: Create a new Stock using NewStock constructor

	// TODO: Loop through each observer
	for i := 0; i < numObservers; i++ {
		// Read observer type ("alert" or "logger")
		observerType, _ := reader.ReadString('\n')
		observerType = strings.TrimSpace(observerType)

		// Read configuration value
		configValue, _ := reader.ReadString('\n')
		configValue = strings.TrimSpace(configValue)

		// TODO: Create appropriate observer based on type
		// - For "alert": parse configValue as float64 threshold, create PriceAlert
		// - For "logger": use configValue as LogName, create PriceLogger
		// TODO: Subscribe the observer to the stock
	}

	// Read new price
	newPriceStr, _ := reader.ReadString('\n')
	newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)

	// TODO: Set the new price and get responses

	// TODO: Print each response on its own line
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming