Golang - Observer Pattern vs Channels

Golang - Observer Pattern vs Channels

Introduction

The Observer Pattern is a popular design pattern that allows an object (known as the subject) to notify its dependent objects (known as observers) automatically when a change occurs in the subject. In Golang, there are two main ways to implement the Observer Pattern: using interfaces and using channels. In this article, we'll compare the classic Observer Pattern with Golang channels, and discuss the benefits and drawbacks of each approach.

Classic Observer Pattern

In the classic Observer Pattern, the subject maintains a list of observers, and provides methods to register and remove observers from the list. When a change occurs in the subject, it calls the update() method on all registered observers, passing in the updated data as a parameter.

Here's an example implementation of the classic Observer Pattern in Golang using interfaces:

type subject interface {
    registerObserver(observer)
    removeObserver(observer)
    notifyObservers()
}

type observer interface {
    update()
}

type weatherStation struct {
    temperature float64
    observers   []observer
}

type temperatureDisplay struct{}

func (w *weatherStation) registerObserver(o observer) {
    w.observers = append(w.observers, o)
}

func (w *weatherStation) removeObserver(o observer) {
    for i, observer := range w.observers {
        if observer == o {
            w.observers = append(w.observers[:i], w.observers[i+1:]...)
            break
        }
    }
}

func (w *weatherStation) notifyObservers() {
    for _, observer := range w.observers {
        observer.update()
    }
}

func (t *temperatureDisplay) update() {
    fmt.Printf("Temperature: %.2f\\\\n", w.temperature)
}

In this example, the weatherStation struct is the subject, and it maintains a list of observers. The temperatureDisplay struct is an observer that displays the current temperature. The update() method in temperatureDisplay simply prints the current temperature.

Golang Channels

In Golang, channels provide a simple and efficient way to implement the Observer Pattern. In this approach, the subject maintains a list of channels, with each channel representing an observer. When a change occurs in the subject, it sends the updated data to each observer's channel.

Here's an example implementation of the Observer Pattern in Golang using channels:

type subject struct {
    observers []chan float64
}

type observer struct {
    c chan float64
}

func (s *subject) registerObserver() chan float64 {
    c := make(chan float64)
    s.observers = append(s.observers, c)
    return c
}

func (s *subject) removeObserver(c chan float64) {
    for i, observer := range s.observers {
        if observer == c {
            s.observers = append(s.observers[:i], s.observers[i+1:]...)
            break
        }
    }
}

func (s *subject) notifyObservers(temperature float64) {
    for _, observer := range s.observers {
        go func(c chan float64) {
            c <- temperature
        }(observer)
    }
}

In this example, the subject struct is the subject, and it maintains a list of channels, with each channel representing an observer. The observer struct has a single channel, which is used to receive updates from the subject.

Comparing the Two Approaches

Both the classic Observer Pattern and Golang channels provide efficient ways to implement the Observer Pattern. However, there are some key differences between the two approaches.

Ease of Implementation

The classic Observer Pattern using interfaces is straightforward to implement, and it provides a clear separation between the subject and its observers. However, it can be more verbose than using channels, and it requires a bit more boilerplate to set up the interface methods.

Using channels to implement the Observer Pattern is also straightforward, and it eliminates the need for the interface methods. Additionally, it provides a lightweight and efficient way to notify observers of changes in the subject.

Flexibility

The classic Observer Pattern using interfaces provides more flexibility, as it allows for more complex interactions between the subject and its observers. For example, observers can be notified of changes in specific properties of the subject, rather than just receiving a general update.

Using channels to implement the Observer Pattern provides less flexibility, as it only allows for a general update to be sent to all observers. However, this simplicity can be an advantage, as it makes the implementation more lightweight and easier to manage.

Concurrency

Using channels to implement the Observer Pattern is more concurrent, as it allows for updates to be sent to observers concurrently using goroutines. This ensures that the method doesn't block while waiting for the observer to receive the update.

The classic Observer Pattern using interfaces can be less concurrent, as it requires the subject to call the update() method on each observer sequentially. This can lead to potential blocking if an observer is slow to process the update.

Conclusion

Both the classic Observer Pattern and Golang channels provide efficient ways to implement the Observer Pattern. The classic Observer Pattern using interfaces provides more flexibility and a clear separation between the subject and its observers, while using channels provides a lightweight and concurrent way to notify observers of changes in the subject. Ultimately, the choice between the two approaches depends on the specific requirements of the application, and the trade-offs between flexibility, ease of implementation, and concurrency.


References

Did you find this article valuable?

Support Matthias Bruns by becoming a sponsor. Any amount is appreciated!