21
NovObserver Design Pattern
Understanding the Observer Design Pattern
Understanding the Observer Design Pattern is a vital concept in software design. It allows an item, known as the subject, to automatically notify and update many dependent objects, known as observers when its status changes. This keeps the system coherent, decoupled, and responsive to state changes, which is especially beneficial in event-driven architectures and real-time systems.
In this design pattern tutorial, we will look at the Observer Design Pattern, covering the questions "What is the Observer Design Pattern?" and "When should I use the Observer Design Pattern?" We will also look at examples of the Observer Design Pattern in action. So, let's understand first: "What is the Observer Design Pattern?"
What is the Observer Design Pattern?
- The Observer Design Pattern is a behavioral design pattern that provides a one-to-many dependency between items, ensuring that when one object (the subject) changes state, all of its dependents (observers) are automatically notified and updated.
- The pattern will explain a mechanism for a group of objects to interact in response to changes in the state of one object (the subject).
- Changes in the subject's state trigger the behavior of the observers.
- It captures the behavior of the dependent objects (observers) and provides a clear separation between the subject and its observers.
- This distinction encourages a modular and maintainable architecture.
- The pattern encourages a loose link between the subject and its observers.
- The subject is not required to know the specific classes of its observers, and observers can be added or withdrawn without impacting the subject.
- The primary mechanism in the Observer Pattern is to notify observers when a change happens.
- This notification system allows for the dynamic and coordinated behavior of many objects in response to changes in the subject.
Observer Design Pattern - UML Diagram & Implementation
The UML class diagram for the implementation of the Observer Design Pattern is given below:
The classes, interfaces, and objects in the above UML class diagram are as follows:
1. Subject
- The subject keeps a list of observers.
- It provides methods for dynamically registering and unregistering observers, as well as defining a method for notifying observers of changes in state.
2. Observer
- The observer interface provides a consistent means for concrete observers to receive topic changes.
- Concrete observers implement this interface, which allows them to respond to changes in the subject's state.
3. Concrete Subject
- ConcreteSubjects are precise implementations of a subject.
- They store the actual state or data that observers wish to track.
- When this state changes, specific individuals alert their observers.
- For example, if a university is the subject, different universities in various cities would be concrete subjects.
4. ConcreteObserver
- Concrete Observer implements the observer interface.
- They associate with a specific issue and respond when notified of a state change.
- When the subject's state changes, the concrete observer's update() function is called, allowing it to perform the relevant actions.
- For example, a fitness tracker on your smartwatch functions as a concrete observer, reacting to data from a health monitoring device.
Real Life Example:
TemperatureSensor Class
- It is the subject that maintains a list of observers and notifies them when the temperature changes.
- It is responsible for attaching, detaching, and notifying observers.
- It includes a method for setting the temperature, which triggers notifications to all registered observers.
ConcreteSensor Class
- It is a specific implementation of the TemperatureSensor class.
- It manages the actual temperature value and provides concrete methods for updating the temperature and notifying observers.
DisplayUnit Interface
- It is the common interface for display units that need to receive temperature updates.
- It specifies how to update the display with new temperature data.
ConcreteDisplayUnit A and B Classes
- They are specific implementations of the DisplayUnit interface.
- They provide detailed implementations for displaying the temperature in different formats or locations.
Key Points
- The Observer pattern is useful because it allows for a flexible relationship between the temperature sensor and display units.
- It is beneficial because the sensor interacts with the observer interface, which makes it easy to add or remove display units.
- It ensures that all registered display units are updated consistently whenever the temperature changes.
- It is a good approach for creating systems where multiple components need to respond to changes in a central data source without being tightly coupled to it.
Example
using System;
using System.Collections.Generic;
// Observer Interface
public interface IObserver
{
void Update(float temperature);
}
// Subject Class
public class TemperatureSensor
{
private readonly List<IObserver> _observers = new List<IObserver>();
private float _temperature;
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
public void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_temperature);
}
}
public void SetTemperature(float temperature)
{
_temperature = temperature;
NotifyObservers();
}
}
// Concrete Observer Class
public class DisplayUnit : IObserver
{
private readonly string _displayName;
public DisplayUnit(string name)
{
_displayName = name;
}
public void Update(float temperature)
{
Console.WriteLine($"{_displayName} displays temperature: {temperature}°C");
}
}
// Main Class to Demonstrate the Pattern
public class Program
{
public static void Main(string[] args)
{
var sensor = new TemperatureSensor();
var unitA = new DisplayUnit("Unit A");
var unitB = new DisplayUnit("Unit B");
sensor.Attach(unitA);
sensor.Attach(unitB);
// Update temperature and notify observers
sensor.SetTemperature(22.5f);
sensor.SetTemperature(25.0f);
}
}
import java.util.ArrayList;
import java.util.List;
// Observer Interface
interface Observer {
void update(float temperature);
}
// Subject Class
class TemperatureSensor {
private final List<Observer> observers = new ArrayList<>();
private float temperature;
public void attach(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature);
}
}
public void setTemperature(float temperature) {
this.temperature = temperature;
notifyObservers();
}
}
// Concrete Observer Class
class DisplayUnit implements Observer {
private final String displayName;
public DisplayUnit(String name) {
this.displayName = name;
}
@Override
public void update(float temperature) {
System.out.println(displayName + " displays temperature: " + temperature + "°C");
}
}
// Main Class to Demonstrate the Pattern
public class Main {
public static void main(String[] args) {
TemperatureSensor sensor = new TemperatureSensor();
DisplayUnit unitA = new DisplayUnit("Unit A");
DisplayUnit unitB = new DisplayUnit("Unit B");
// Attach observers
sensor.attach(unitA);
sensor.attach(unitB);
// Change temperature and notify observers
sensor.setTemperature(22.5f);
sensor.setTemperature(25.0f);
}
}
# Observer Interface
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temperature: float):
pass
# Subject Class
class TemperatureSensor:
def __init__(self):
self._observers = []
self._temperature = 0.0
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify_observers(self):
for observer in self._observers:
observer.update(self._temperature)
def set_temperature(self, temperature: float):
self._temperature = temperature
self.notify_observers()
# Concrete Observer Class
class DisplayUnit(Observer):
def __init__(self, name: str):
self._display_name = name
def update(self, temperature: float):
print(f"{self._display_name} displays temperature: {temperature}°C")
# Main function to demonstrate the pattern
def main():
sensor = TemperatureSensor()
unit_a = DisplayUnit("Unit A")
unit_b = DisplayUnit("Unit B")
sensor.attach(unit_a)
sensor.attach(unit_b)
sensor.set_temperature(22.5)
sensor.set_temperature(25.0)
if __name__ == "__main__":
main()
// Observer Interface
interface Observer {
update(temperature: number): void;
}
// Subject Class
class TemperatureSensor {
private observers: Observer[] = [];
private temperature: number = 0;
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer);
}
notifyObservers(): void {
this.observers.forEach(observer => observer.update(this.temperature));
}
setTemperature(temperature: number): void {
this.temperature = temperature;
this.notifyObservers();
}
}
// Concrete Observer Class
class DisplayUnit implements Observer {
private displayName: string;
constructor(name: string) {
this.displayName = name;
}
update(temperature: number): void {
console.log(`${this.displayName} displays temperature: ${temperature}°C`);
}
}
// Main function to demonstrate the pattern
function main() {
const sensor = new TemperatureSensor();
const unitA = new DisplayUnit("Unit A");
const unitB = new DisplayUnit("Unit B");
sensor.attach(unitA);
sensor.attach(unitB);
sensor.setTemperature(22.5);
sensor.setTemperature(25.0);
}
main();
// Observer Interface
class Observer {
update(temperature) {
throw new Error("Method 'update()' must be implemented.");
}
}
// Subject Class
class TemperatureSensor {
constructor() {
this.observers = [];
this.temperature = 0;
}
attach(observer) {
this.observers.push(observer);
}
detach(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notifyObservers() {
this.observers.forEach(observer => observer.update(this.temperature));
}
setTemperature(temperature) {
this.temperature = temperature;
this.notifyObservers();
}
}
// Concrete Observer Class
class DisplayUnit extends Observer {
constructor(name) {
super();
this.displayName = name;
}
update(temperature) {
console.log(`${this.displayName} displays temperature: ${temperature}°C`);
}
}
// Main function to demonstrate the pattern
function main() {
const sensor = new TemperatureSensor();
const unitA = new DisplayUnit("Unit A");
const unitB = new DisplayUnit("Unit B");
sensor.attach(unitA);
sensor.attach(unitB);
sensor.setTemperature(22.5);
sensor.setTemperature(25.0);
}
main();
Output
Unit A displays temperature: 22.5°C
Unit B displays temperature: 22.5°C
Unit A displays temperature: 25.0°C
Unit B displays temperature: 25.0°C
Explanation
- This code demonstrates the Observer pattern. It is where the TemperatureSensor class manages a list of observers and notifies them when the temperature changes.
- It is that the DisplayUnit class receives these updates and shows the new temperature.
- In the Main class, it is that two display units are created, attached to the sensor, and updated with new temperatures.
When to use it?
The Observer Design Pattern is useful in the following scenarios:
- When you have multiple dependent objects: It is effective when changes in one object need to be reflected in others automatically.
- Event handling: It is ideal for event-driven systems where an event in one part of the system triggers actions in other parts.
- Loose coupling: It is beneficial when you want to reduce dependencies between objects, allowing them to interact without needing to know each other’s details.
- Real-time updates: It is helpful when multiple objects need to be notified and updated in real-time when another object changes.
When not to use the Observer Design Pattern?
The Observer Design Pattern should be avoided in these situations:
- Performance concerns: It is not suitable if there are a large number of observers, as it can lead to performance issues due to frequent notifications.
- Complex dependencies: It is not ideal when observers have complex or intricate dependencies that could lead to unpredictable updates or cascading changes.
- Memory usage: It is not effective if managing many observers consumes excessive memory or resources.
- Unnecessary overhead: It is not needed if the system’s requirements do not involve dynamic or real-time updates where the pattern’s benefits would be significant.