Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Observer — Behavioral Design Pattern

What is it?

The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are notified automatically. Observers register interest in a subject and react to events without the subject knowing anything about their concrete types.

It sits in the Behavioral family because it defines how objects communicate — the subject broadcasts state; observers decide what to do with it.


When to use it

  • Multiple components need to react to events produced by a single source.
  • You want to decouple the event producer from its consumers so either side can change independently.
  • The number or type of subscribers is not known at compile time and may change at runtime.

Avoid it when the notification fan-out is always to one fixed receiver — direct method calls are simpler and clearer.


Go's approach vs. OOP languages

Classical OOP defines abstract Subject and Observer base classes. Go replaces both with interfaces and first-class functions:

Mechanism Purpose
Observer interface Declares the subscriber contract (Name, Notify)
Subject interface Declares the publisher contract (RegisterObserver, Notify, GetObserverList)
PriceTracker (concrete subject) Maintains the observer registry and fans out price events
ObservationHook A predicate that each observer registers alongside itself, filtering which events it receives
Concrete observers (RetailInvestor, StopLossAlert, AuditLogger) Each encapsulates one reaction to a price event

Structure of this implementation

observer/
├── observer/
│   ├── observer.go          # Observer interface and NotificationSignal
│   └── retail_investor.go   # Concrete observers
├── subject/
│   └── subject.go           # Subject interface and ObservationHook
├── domain/
│   └── stock_price_tracker.go  # Concrete subject (PriceTracker)
└── main.go                  # Usage demo

The observer interface — observer/observer.go

type Observer interface {
    Name() string
    Notify(NotificationSignal) error
}

Any type that implements Name and Notify can subscribe to a PriceTracker — no embedding or registration outside of the call site.

The subject interface — subject/subject.go

type Subject interface {
    Notify(string, float64) error
    GetObserverList() []observer.Observer
    RegisterObserver(observer.Observer, ObservationHook) error
}

ObservationHook — conditional subscriptions

Each observer registers with a hook — a predicate func(ticker string, price float64) bool. The subject evaluates the hook before dispatching, so an observer only fires when its condition is met:

// Only notified when TCS drops below ₹2895.50
priceTracker.RegisterObserver(&retail, subject.NewCustomObservationHook(
    func(ticker string, amt float64) bool {
        return ticker == "TCS" && amt < 2895.50
    },
))

// Notified on every price event
priceTracker.RegisterObserver(&auditLogger, subject.NewGenericObservationHook())

This is an extension of the classic pattern — instead of all observers receiving every event, each declares its own interest condition at registration time.

Concrete observers — observer/retail_investor.go

Type Reaction
RetailInvestor Logs the price event for the investor
StopLossAlert Fires an alert when a price crosses a configured threshold
AuditLogger Records every event unconditionally for audit purposes

Concrete subject — domain/stock_price_tracker.go

PriceTracker holds observers in a map[Observer]ObservationHook. On Notify, it iterates the map, evaluates each hook, and calls ob.Notify only for satisfied hooks. Duplicate registration is rejected with an error.


Runtime behavior

priceTracker.Notify("TCS", 2890.00)
→ hook for RetailInvestor satisfied (TCS < 2895.50) → [RetailInvestor] notified
→ hook for StopLossAlert not satisfied (ticker != INFY) → skipped
→ hook for AuditLogger always satisfied → [AuditLogger] notified

priceTracker.Notify("INFY", 1280.00)
→ hook for RetailInvestor not satisfied (ticker != TCS) → skipped
→ hook for StopLossAlert satisfied (INFY < 1300) → [StopLossAlert] notified
→ hook for AuditLogger always satisfied → [AuditLogger] notified

Adding a new observer

  1. Define a new struct (e.g., DividendAlert).
  2. Implement Name() string and Notify(NotificationSignal) error.
  3. Register it with priceTracker.RegisterObserver(&alert, hook).

No changes to PriceTracker, the Subject interface, or any existing observer — Open/Closed Principle in practice.


Key interview talking points

Q: How does Observer differ from Strategy? Strategy selects one algorithm and executes it in place of another. Observer broadcasts one event to many independent subscribers. Strategy is about substitution; Observer is about decoupled fan-out.

Q: How does Observer differ from Pub/Sub? In Observer, the subject holds a direct reference to each observer — it's synchronous and in-process. In Pub/Sub, a message broker sits between publisher and subscriber, enabling async delivery, persistence, and cross-process communication. Observer is a simpler, tighter coupling; Pub/Sub is Observer scaled out.

Q: What is the risk of a naive Observer implementation? Memory leaks — if observers register but never deregister, the subject holds references indefinitely. This implementation uses an explicit map, so removing an observer is straightforward to add via a DeregisterObserver method.

Q: Why use a map instead of a slice for the observer registry? O(1) duplicate detection on registration and O(1) removal — both are O(n) with a slice. The tradeoff is that iteration order is non-deterministic, which is acceptable here since observers are independent.

Q: How would you make PriceTracker thread-safe? Protect the observers map with a sync.RWMutex — a read lock in Notify (while iterating), a write lock in RegisterObserver. Each observer's Notify could still run concurrently by fanning out into goroutines with a WaitGroup.

Q: Where does ObservationHook sit in classic GoF? It is not part of the original GoF Observer. It is a filter layer added between the subject and observers, similar to the Event Filter or Conditional Observer extension — useful when the subject emits high-frequency events and observers only care about a subset.