Observer Design Pattern in Kotlin

1. Definition

The Observer Design Pattern defines a one-to-many dependency between objects so that when one object's state changes, all its dependents are notified and updated automatically. The pattern is used to achieve a decoupled architecture where changes in one object can be notified to other objects without knowing who or what those objects are.

2. Problem Statement

Imagine developing a weather monitoring application where multiple displays (like a current conditions display, forecast display, etc.) need to update whenever the weather data changes. If we hard-code the dependencies between the weather data and the displays, the system will be tightly coupled, inflexible, and hard to maintain.

3. Solution

The Observer pattern provides a solution by introducing 'observers' (the displays) that subscribe to the 'subject' (the weather data). When the weather data changes, it notifies all its observers. Thus, we can add or remove displays without modifying the weather data code.

4. Real-World Use Cases

1. News publishers notifying subscribers of a new article.

2. Event management systems in GUI libraries.

3. Stock market systems where investors are notified of price changes.

5. Implementation Steps

1. Define an Observer interface for objects that should be notified of changes.

2. Define a Subject interface to manage subscribers and notify them.

3. Concrete subjects and observers implement the above interfaces.

6. Implementation in Kotlin

// Step 1: Observer Interface
interface Observer {
    fun update(temp: Float, humidity: Float, pressure: Float)
}
// Step 2: Subject Interface
interface Subject {
    fun registerObserver(o: Observer)
    fun removeObserver(o: Observer)
    fun notifyObservers()
}
// Step 3: Concrete implementation of Subject - WeatherData
class WeatherData : Subject {
    private val observers = mutableListOf<Observer>()
    var temperature: Float = 0.0f
    var humidity: Float = 0.0f
    var pressure: Float = 0.0f
    override fun registerObserver(o: Observer) { observers.add(o) }
    override fun removeObserver(o: Observer) { observers.remove(o) }
    override fun notifyObservers() {
        observers.forEach { it.update(temperature, humidity, pressure) }
    }
    fun measurementsChanged() = notifyObservers()
    fun setMeasurements(temp: Float, humidity: Float, pressure: Float) {
        this.temperature = temp
        this.humidity = humidity
        this.pressure = pressure
        measurementsChanged()
    }
}
// Step 4: Concrete Observer - CurrentConditionsDisplay
class CurrentConditionsDisplay(private val weatherData: Subject) : Observer {
    init { weatherData.registerObserver(this) }
    override fun update(temp: Float, humidity: Float, pressure: Float) {
        println("Current conditions: $temp degrees and $humidity% humidity")
    }
}
// Testing the implementation
fun main() {
    val weatherData = WeatherData()
    val currentDisplay = CurrentConditionsDisplay(weatherData)
    weatherData.setMeasurements(28.5f, 65f, 1012.5f)
}

Output:

Current conditions: 28.5 degrees and 65% humidity

Explanation:

1. We start by defining the Observer and Subject interfaces, setting the blueprint for observers and subjects respectively.

2. WeatherData, the concrete subject, manages the list of observers and notifies them whenever the measurements change.

3. CurrentConditionsDisplay is a concrete observer that prints the current conditions whenever it's updated.

4. In our test, we change the weather data measurements, which triggers an update to all registered observers, thus displaying the current conditions.

7. When to use?

Use the Observer Pattern when:

1. The change of a state in one object must be reflected in another without making the objects tightly coupled.

2. You want to provide a dynamic subscription model where subscribers can join or leave at any point.

3. You want to create scalable systems where new observers can be added without modifying existing code.


Comments